diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt index a3b5ce39eb..5dfb0eab9b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt @@ -26,3 +26,8 @@ fun Throwable.is401() = fun Throwable.isTokenError() = this is Failure.ServerError && (error.code == MatrixError.M_UNKNOWN_TOKEN || error.code == MatrixError.M_MISSING_TOKEN) + +fun Throwable.shouldBeRetried(): Boolean { + return this is Failure.NetworkConnection + || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED) +} 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..c2a847dd0b 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 @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.crypto.sas import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest /** * https://matrix.org/docs/spec/client_server/r0.5.0#key-verification-framework @@ -39,6 +40,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 @@ -50,7 +55,9 @@ interface SasVerificationService { */ fun beginKeyVerification(method: String, userId: String, deviceID: String): String? - fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback?) + fun requestKeyVerificationInDMs(userId: String, roomId: String): PendingVerificationRequest + + fun declineVerificationRequestInDMs(otherUserId: String, otherDeviceId: String, transactionId: String, roomId: String) fun beginKeyVerificationInDMs(method: String, transactionId: String, @@ -59,11 +66,33 @@ interface SasVerificationService { otherDeviceId: String, callback: MatrixCallback?): String? + /** + * Returns false if the request is unknwown + */ + fun readyPendingVerificationInDMs(otherUserId: String, roomId: String, transactionId: String): Boolean + // fun transactionUpdated(tx: SasVerificationTransaction) interface SasVerificationListener { fun transactionCreated(tx: SasVerificationTransaction) fun transactionUpdated(tx: SasVerificationTransaction) - fun markedAsManuallyVerified(userId: String, deviceId: String) + fun markedAsManuallyVerified(userId: String, deviceId: String) {} + + fun verificationRequestCreated(pr: PendingVerificationRequest) {} + fun verificationRequestUpdated(pr: PendingVerificationRequest) {} + } + + companion object { + + private const val TEN_MINTUTES_IN_MILLIS = 10 * 60 * 1000 + private const val FIVE_MINTUTES_IN_MILLIS = 5 * 60 * 1000 + + fun isValidRequest(age: Long?): Boolean { + if (age == null) return false + val now = System.currentTimeMillis() + val tooInThePast = now - TEN_MINTUTES_IN_MILLIS + val tooInTheFuture = now + FIVE_MINTUTES_IN_MILLIS + return age in tooInThePast..tooInTheFuture + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTransaction.kt index 9610daf294..1f7bef558f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTransaction.kt @@ -47,4 +47,6 @@ interface SasVerificationTransaction { * both short codes do match */ fun userHasVerifiedShortCode() + + fun shortCodeDoesNotMatch() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt index 60d333ec96..1939b1f0e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt @@ -73,6 +73,7 @@ object EventType { const val KEY_VERIFICATION_MAC = "m.key.verification.mac" const val KEY_VERIFICATION_CANCEL = "m.key.verification.cancel" const val KEY_VERIFICATION_DONE = "m.key.verification.done" + const val KEY_VERIFICATION_READY = "m.key.verification.ready" // Relation Events const val REACTION = "m.reaction" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index ba3b5ded78..fe110b7b9c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -89,4 +89,6 @@ interface RoomService { fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback>): Cancelable + + fun getExistingDirectRoomWithUser(otherUserId: String) : Room? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationAcceptContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationAcceptContent.kt index 54403ab67f..d4fb3a44fd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationAcceptContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationAcceptContent.kt @@ -50,7 +50,7 @@ internal data class MessageVerificationAcceptContent( return true } - override fun toEventContent() = this.toContent() + override fun toEventContent() = toContent() companion object : VerificationInfoAcceptFactory { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationCancelContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationCancelContent.kt index fe2c958703..651a25e175 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationCancelContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationCancelContent.kt @@ -34,7 +34,7 @@ data class MessageVerificationCancelContent( override val transactionID: String? get() = relatesTo?.eventId - override fun toEventContent() = this.toContent() + override fun toEventContent() = toContent() override fun isValid(): Boolean { if (transactionID.isNullOrBlank() || code.isNullOrBlank()) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationDoneContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationDoneContent.kt index 965fcb79bb..224c5c52d3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationDoneContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationDoneContent.kt @@ -17,6 +17,8 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.internal.crypto.verification.VerificationInfo @@ -24,5 +26,11 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationInfo internal data class MessageVerificationDoneContent( @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? ) : VerificationInfo { - override fun isValid() = true + + override val transactionID: String? + get() = relatesTo?.eventId + + override fun isValid() = transactionID?.isNotEmpty() == true + + override fun toEventContent(): Content? = toContent() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationKeyContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationKeyContent.kt index 2dacb19871..777e0e7318 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationKeyContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationKeyContent.kt @@ -44,7 +44,7 @@ internal data class MessageVerificationKeyContent( return true } - override fun toEventContent() = this.toContent() + override fun toEventContent() = toContent() companion object : VerificationInfoKeyFactory { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationMacContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationMacContent.kt index f08625791f..05e614dee9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationMacContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationMacContent.kt @@ -33,7 +33,7 @@ internal data class MessageVerificationMacContent( override val transactionID: String? get() = relatesTo?.eventId - override fun toEventContent() = this.toContent() + override fun toEventContent() = toContent() override fun isValid(): Boolean { if (transactionID.isNullOrBlank() || keys.isNullOrBlank() || mac.isNullOrEmpty()) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationReadyContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationReadyContent.kt new file mode 100644 index 0000000000..4e237ba1c9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationReadyContent.kt @@ -0,0 +1,57 @@ +/* + * 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.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.RelationType +import im.vector.matrix.android.api.session.events.model.toContent +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent +import im.vector.matrix.android.internal.crypto.verification.MessageVerificationReadyFactory +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoReady + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationReadyContent( + @Json(name = "from_device") override val fromDevice: String? = null, + @Json(name = "methods") override val methods: List? = null, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerificationInfoReady { + + override val transactionID: String? + get() = relatesTo?.eventId + + override fun toEventContent() = toContent() + + override fun isValid(): Boolean { + if (transactionID.isNullOrBlank() || methods.isNullOrEmpty() || fromDevice.isNullOrEmpty()) { + return false + } + return true + } + + companion object : MessageVerificationReadyFactory { + override fun create(tid: String, methods: List, fromDevice: String): VerificationInfoReady { + return MessageVerificationReadyContent( + fromDevice = fromDevice, + methods = methods, + relatesTo = RelationDefaultContent( + RelationType.REFERENCE, + tid + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationStartContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationStartContent.kt index f928e21a82..a33decc821 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationStartContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationStartContent.kt @@ -62,5 +62,5 @@ internal data class MessageVerificationStartContent( return true } - override fun toEventContent() = this.toContent() + override fun toEventContent() = toContent() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt index 4243c6a464..65e47473d0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt @@ -180,6 +180,9 @@ internal abstract class CryptoModule { @Binds abstract fun bindEncryptEventTask(encryptEventTask: DefaultEncryptEventTask): EncryptEventTask + @Binds + abstract fun bindSendVerificationMessageTask(sendDefaultSendVerificationMessageTask: DefaultSendVerificationMessageTask): SendVerificationMessageTask + @Binds abstract fun bindClaimOneTimeKeysForUsersDeviceTask(claimOneTimeKeysForUsersDevice: DefaultClaimOneTimeKeysForUsersDevice) : ClaimOneTimeKeysForUsersDeviceTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationReady.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationReady.kt new file mode 100644 index 0000000000..ca3b1e0075 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationReady.kt @@ -0,0 +1,38 @@ +/* + * 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.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoReady + +/** + * Requests a key verification with another user's devices. + */ +@JsonClass(generateAdapter = true) +internal data class KeyVerificationReady( + @Json(name = "from_device") override val fromDevice: String?, + // TODO add qr? + @Json(name = "methods") override val methods: List? = listOf(KeyVerificationStart.VERIF_METHOD_SAS), + @Json(name = "transaction_id") override var transactionID: String? = null +) : SendToDeviceObject, VerificationInfoReady { + + override fun toSendToDeviceObject() = this + + override fun isValid(): Boolean { + return !transactionID.isNullOrBlank() && !fromDevice.isNullOrBlank() && !methods.isNullOrEmpty() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationRequest.kt index 51eed84412..4511db324a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationRequest.kt @@ -37,7 +37,7 @@ internal data class KeyVerificationRequest( val timestamp: Int, @Json(name = "transaction_id") - var transactionID: String? = null + override var transactionID: String? = null ) : SendToDeviceObject, VerificationInfo { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt index e8c0334539..e25ed10a6a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt @@ -43,6 +43,7 @@ data class KeyVerificationStart( companion object { const val VERIF_METHOD_SAS = "m.sas.v1" + const val VERIF_METHOD_SCAN = "m.qr_code.scan.v1" } override fun isValid(): Boolean { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RequestVerificationDMTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RequestVerificationDMTask.kt deleted file mode 100644 index 00b7d7320a..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RequestVerificationDMTask.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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 com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.session.crypto.CryptoService -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.send.SendState -import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.room.RoomAPI -import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory -import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater -import im.vector.matrix.android.internal.session.room.send.SendResponse -import im.vector.matrix.android.internal.task.Task -import javax.inject.Inject - -internal interface RequestVerificationDMTask : Task { - data class Params( - val event: Event, - val cryptoService: CryptoService - ) - - fun createParamsAndLocalEcho(roomId: String, from: String, methods: List, to: String, cryptoService: CryptoService): Params -} - -internal class DefaultRequestVerificationDMTask @Inject constructor( - private val localEchoUpdater: LocalEchoUpdater, - private val localEchoEventFactory: LocalEchoEventFactory, - private val encryptEventTask: DefaultEncryptEventTask, - private val monarchy: Monarchy, - private val roomAPI: RoomAPI) - : RequestVerificationDMTask { - - override fun createParamsAndLocalEcho(roomId: String, from: String, methods: List, to: String, cryptoService: CryptoService) - : RequestVerificationDMTask.Params { - val event = localEchoEventFactory.createVerificationRequest(roomId, from, to, methods) - .also { localEchoEventFactory.saveLocalEcho(monarchy, it) } - return RequestVerificationDMTask.Params( - event, - cryptoService - ) - } - - override suspend fun execute(params: RequestVerificationDMTask.Params): SendResponse { - val event = handleEncryption(params) - val localID = event.eventId!! - - try { - localEchoUpdater.updateSendState(localID, SendState.SENDING) - val executeRequest = executeRequest { - apiCall = roomAPI.send( - localID, - roomId = event.roomId ?: "", - content = event.content, - eventType = event.type // message or room.encrypted - ) - } - localEchoUpdater.updateSendState(localID, SendState.SENT) - return executeRequest - } catch (e: Throwable) { - localEchoUpdater.updateSendState(localID, SendState.UNDELIVERED) - throw e - } - } - - private suspend fun handleEncryption(params: RequestVerificationDMTask.Params): Event { - val roomId = params.event.roomId ?: "" - if (params.cryptoService.isRoomEncrypted(roomId)) { - try { - return encryptEventTask.execute(EncryptEventTask.Params( - roomId, - params.event, - listOf("m.relates_to"), - params.cryptoService - )) - } catch (throwable: Throwable) { - // We said it's ok to send verification request in clear - } - } - return params.event - } -} 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..2167940aaa --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt @@ -0,0 +1,151 @@ +/* + * 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.crypto.sas.SasVerificationService +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.UserId +import im.vector.matrix.android.internal.task.Task +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) { + // TODO ignore initial sync or back pagination? + + 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. + + if (!SasVerificationService.isValidRequest(event.ageLocalTs + ?: event.originServerTs)) return@forEach Unit.also { + Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated") + } + + // 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) + } + } + + Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}") + 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/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendVerificationMessageTask.kt index cf1b2d233a..eacdc977d8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendVerificationMessageTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendVerificationMessageTask.kt @@ -15,64 +15,29 @@ */ package im.vector.matrix.android.internal.crypto.tasks -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.crypto.CryptoService -import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.LocalEcho -import im.vector.matrix.android.api.session.events.model.UnsignedData import im.vector.matrix.android.api.session.room.send.SendState -import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI -import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater import im.vector.matrix.android.internal.session.room.send.SendResponse import im.vector.matrix.android.internal.task.Task import javax.inject.Inject -internal interface SendVerificationMessageTask : Task { +internal interface SendVerificationMessageTask : Task { data class Params( - val type: String, val event: Event, val cryptoService: CryptoService? ) - - fun createParamsAndLocalEcho(type: String, - roomId: String, - content: Content, - cryptoService: CryptoService?) : Params } internal class DefaultSendVerificationMessageTask @Inject constructor( private val localEchoUpdater: LocalEchoUpdater, - private val localEchoEventFactory: LocalEchoEventFactory, private val encryptEventTask: DefaultEncryptEventTask, - private val monarchy: Monarchy, - @UserId private val userId: String, private val roomAPI: RoomAPI) : SendVerificationMessageTask { - override fun createParamsAndLocalEcho(type: String, roomId: String, content: Content, cryptoService: CryptoService?): SendVerificationMessageTask.Params { - val localID = LocalEcho.createLocalEchoId() - val event = Event( - roomId = roomId, - originServerTs = System.currentTimeMillis(), - senderId = userId, - eventId = localID, - type = type, - content = content, - unsignedData = UnsignedData(age = null, transactionId = localID) - ).also { - localEchoEventFactory.saveLocalEcho(monarchy, it) - } - return SendVerificationMessageTask.Params( - type, - event, - cryptoService - ) - } - - override suspend fun execute(params: SendVerificationMessageTask.Params): SendResponse { + override suspend fun execute(params: SendVerificationMessageTask.Params): String { val event = handleEncryption(params) val localID = event.eventId!! @@ -87,7 +52,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( ) } localEchoUpdater.updateSendState(localID, SendState.SENT) - return executeRequest + return executeRequest.eventId } catch (e: Throwable) { localEchoUpdater.updateSendState(localID, SendState.UNDELIVERED) throw e diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASVerificationTransaction.kt index 5eff26a5bb..349d6a79ca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASVerificationTransaction.kt @@ -33,7 +33,8 @@ internal class DefaultIncomingSASVerificationTransaction( private val cryptoStore: IMXCryptoStore, deviceFingerprint: String, transactionId: String, - otherUserID: String + otherUserID: String, + val autoAccept: Boolean = false ) : SASVerificationTransaction( setDeviceVerificationAction, credentials, @@ -76,6 +77,10 @@ internal class DefaultIncomingSASVerificationTransaction( this.startReq = startReq state = SasVerificationTxState.OnStarted this.otherDeviceId = startReq.fromDevice + + if (autoAccept) { + performAccept() + } } override fun performAccept() { 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..f5075cbb23 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 @@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState import im.vector.matrix.android.api.session.crypto.sas.safeValueOf 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.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.DeviceListManager @@ -37,12 +38,7 @@ import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.rest.* import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore -import im.vector.matrix.android.internal.crypto.tasks.DefaultRequestVerificationDMTask import im.vector.matrix.android.internal.session.SessionScope -import im.vector.matrix.android.internal.session.room.send.SendResponse -import im.vector.matrix.android.internal.task.TaskConstraints -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -60,11 +56,9 @@ internal class DefaultSasVerificationService @Inject constructor( private val myDeviceInfoHolder: Lazy, private val deviceListManager: DeviceListManager, private val setDeviceVerificationAction: SetDeviceVerificationAction, - private val requestVerificationDMTask: DefaultRequestVerificationDMTask, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val sasTransportRoomMessageFactory: SasTransportRoomMessageFactory, - private val sasTransportToDeviceFactory: SasTransportToDeviceFactory, - private val taskExecutor: TaskExecutor + private val sasTransportToDeviceFactory: SasTransportToDeviceFactory ) : VerificationTransaction.Listener, SasVerificationService { private val uiHandler = Handler(Looper.getMainLooper()) @@ -75,6 +69,12 @@ internal class DefaultSasVerificationService @Inject constructor( // map [sender : [transaction]] private val txMap = HashMap>() + /** + * Map [sender: [PendingVerificationRequest]] + * For now we keep all requests (even terminated ones) during the lifetime of the app. + */ + private val pendingRequests = HashMap>() + // Event received from the sync fun onToDeviceEvent(event: Event) { GlobalScope.launch(coroutineDispatchers.crypto) { @@ -120,8 +120,11 @@ internal class DefaultSasVerificationService @Inject constructor( EventType.KEY_VERIFICATION_MAC -> { onRoomMacReceived(event) } + EventType.KEY_VERIFICATION_READY -> { + onRoomReadyReceived(event) + } EventType.KEY_VERIFICATION_DONE -> { - // TODO? + onRoomDoneReceived(event) } EventType.MESSAGE -> { if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.type) { @@ -175,6 +178,30 @@ internal class DefaultSasVerificationService @Inject constructor( } } + private fun dispatchRequestAdded(tx: PendingVerificationRequest) { + uiHandler.post { + listeners.forEach { + try { + it.verificationRequestCreated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + private fun dispatchRequestUpdated(tx: PendingVerificationRequest) { + uiHandler.post { + listeners.forEach { + try { + it.verificationRequestUpdated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) { setDeviceVerificationAction.handle(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED, deviceID, @@ -190,8 +217,50 @@ internal class DefaultSasVerificationService @Inject constructor( } fun onRoomRequestReceived(event: Event) { - // TODO 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 + } + + // We don't want to block here + GlobalScope.launch { + if (checkKeysAreDownloaded(senderId, requestInfo.fromDevice) == null) { + Timber.e("## SAS Verification device ${requestInfo.fromDevice} is not knwon") + } + } + + // Remember this request + val requestsForUser = pendingRequests[senderId] + ?: ArrayList().also { + pendingRequests[event.senderId] = it + } + + val pendingVerificationRequest = PendingVerificationRequest( + ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(), + isIncoming = true, + otherUserId = senderId, // requestInfo.toUserId, + transactionId = event.eventId, + requestInfo = requestInfo + ) + requestsForUser.add(pendingVerificationRequest) + dispatchRequestAdded(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) { @@ -205,8 +274,9 @@ internal class DefaultSasVerificationService @Inject constructor( if (startReq?.isValid()?.not() == true) { Timber.e("## received invalid verification request") if (startReq.transactionID != null) { - sasTransportRoomMessageFactory.createTransport(event.roomId - ?: "", cryptoService, null).cancelTransaction( + sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId + ?: "", event.roomId + ?: "", null).cancelTransaction( startReq.transactionID ?: "", otherUserId!!, startReq.fromDevice ?: event.getSenderKey()!!, @@ -217,11 +287,13 @@ internal class DefaultSasVerificationService @Inject constructor( } handleStart(otherUserId, startReq as VerificationInfoStart) { - it.transport = sasTransportRoomMessageFactory.createTransport(event.roomId - ?: "", cryptoService, it) + it.transport = sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId + ?: "", event.roomId + ?: "", it) }?.let { - sasTransportRoomMessageFactory.createTransport(event.roomId - ?: "", cryptoService, null).cancelTransaction( + sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId + ?: "", event.roomId + ?: "", null).cancelTransaction( startReq.transactionID ?: "", otherUserId!!, startReq.fromDevice ?: event.getSenderKey()!!, @@ -263,7 +335,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) @@ -286,6 +358,10 @@ internal class DefaultSasVerificationService @Inject constructor( // Ok we can create if (KeyVerificationStart.VERIF_METHOD_SAS == startReq.method) { Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}") + // If there is a corresponding request, we can auto accept + // as we are the one requesting in first place (or we accepted the request) + val autoAccept = getExistingVerificationRequest(otherUserId)?.any { it.transactionId == startReq.transactionID } + ?: false val tx = DefaultIncomingSASVerificationTransaction( // this, setDeviceVerificationAction, @@ -293,7 +369,8 @@ internal class DefaultSasVerificationService @Inject constructor( cryptoStore, myDeviceInfoHolder.get().myDevice.fingerprint()!!, startReq.transactionID!!, - otherUserId).also { txConfigure(it) } + otherUserId, + autoAccept).also { txConfigure(it) } addTransaction(tx) tx.acceptVerificationEvent(otherUserId, startReq) } else { @@ -311,11 +388,16 @@ 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) } + var keys = deviceListManager.downloadKeys(listOf(otherUserId), false) + if (keys.getUserDeviceIds(otherUserId)?.contains(fromDevice) == true) { + return keys + } else { + // force download + keys = deviceListManager.downloadKeys(listOf(otherUserId), true) + return keys.takeIf { keys.getUserDeviceIds(otherUserId)?.contains(fromDevice) == true } + } } catch (e: Exception) { null } @@ -333,6 +415,10 @@ internal class DefaultSasVerificationService @Inject constructor( // TODO should we cancel? return } + getExistingVerificationRequest(event.senderId ?: "", cancelReq.transactionID)?.let { + updatePendingRequest(it.copy(cancelConclusion = safeValueOf(cancelReq.code))) + // Should we remove it from the list? + } handleOnCancel(event.senderId!!, cancelReq) } @@ -352,14 +438,20 @@ internal class DefaultSasVerificationService @Inject constructor( private fun handleOnCancel(otherUserId: String, cancelReq: VerificationInfoCancel) { Timber.v("## SAS onCancelReceived otherUser:$otherUserId reason:${cancelReq.reason}") - val existing = getExistingTransaction(otherUserId, cancelReq.transactionID!!) - if (existing == null) { - Timber.e("## Received invalid cancel request") - return + + val existingTransaction = getExistingTransaction(otherUserId, cancelReq.transactionID!!) + val existingRequest = getExistingVerificationRequest(otherUserId, cancelReq.transactionID!!) + + if (existingRequest != null) { + // Mark this request as cancelled + updatePendingRequest(existingRequest.copy( + cancelConclusion = safeValueOf(cancelReq.code) + )) } - if (existing is SASVerificationTransaction) { - existing.cancelledReason = safeValueOf(cancelReq.code) - existing.state = SasVerificationTxState.OnCancelled + + if (existingTransaction is SASVerificationTransaction) { + existingTransaction.cancelledReason = safeValueOf(cancelReq.code) + existingTransaction.state = SasVerificationTxState.OnCancelled } } @@ -456,6 +548,44 @@ 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 onRoomDoneReceived(event: Event) { + val doneReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + + if (doneReq == null || doneReq.isValid().not() || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid Done request") + // TODO should we cancel? + return + } + + handleDoneReceived(event.senderId, doneReq) + } + private fun onMacReceived(event: Event) { val macReq = event.getClearContent().toModel()!! @@ -481,12 +611,42 @@ internal class DefaultSasVerificationService @Inject constructor( } } + private fun handleReadyReceived(senderId: String, readyReq: VerificationInfoReady) { + val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == readyReq.transactionID } + if (existingRequest == null) { + Timber.e("## SAS Received Ready for unknown request txId:${readyReq.transactionID} fromDevice ${readyReq.fromDevice}") + return + } + updatePendingRequest(existingRequest.copy(readyInfo = readyReq)) + } + + private fun handleDoneReceived(senderId: String, doneInfo: VerificationInfo) { + val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneInfo.transactionID } + if (existingRequest == null) { + Timber.e("## SAS Received Done for unknown request txId:${doneInfo.transactionID}") + return + } + updatePendingRequest(existingRequest.copy(isSuccessful = true)) + } + override fun getExistingTransaction(otherUser: String, tid: String): VerificationTransaction? { synchronized(lock = txMap) { return txMap[otherUser]?.get(tid) } } + 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,28 +696,76 @@ internal class DefaultSasVerificationService @Inject constructor( } } - override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback?) { - requestVerificationDMTask.configureWith( - requestVerificationDMTask.createParamsAndLocalEcho( - roomId = roomId, - from = credentials.deviceId ?: "", - methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS), - to = userId, - cryptoService = cryptoService - ) - ) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: SendResponse) { - callback?.onSuccess(data.eventId) + override fun requestKeyVerificationInDMs(userId: String, roomId: String) + : PendingVerificationRequest { + Timber.i("## SAS Requesting verification to user: $userId in room $roomId") + + val requestsForUser = pendingRequests[userId] + ?: ArrayList().also { + pendingRequests[userId] = it } - override fun onFailure(failure: Throwable) { - callback?.onFailure(failure) + val transport = sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId + ?: "", roomId, null) + + // Cancel existing pending requests? + requestsForUser.forEach { existingRequest -> + existingRequest.transactionId?.let { tid -> + if (!existingRequest.isFinished) { + Timber.d("## SAS, cancelling pending requests to start a new one") + transport.cancelTransaction(tid, existingRequest.otherUserId, "", CancelCode.User) } } - constraints = TaskConstraints(true) - retryCount = 3 - }.executeBy(taskExecutor) + } + + val localID = LocalEcho.createLocalEchoId() + + val verificationRequest = PendingVerificationRequest( + ageLocalTs = System.currentTimeMillis(), + isIncoming = false, + localID = localID, + otherUserId = userId + ) + + transport.sendVerificationRequest(localID, userId, roomId) { syncedId, info -> + // We need to update with the syncedID + updatePendingRequest(verificationRequest.copy( + transactionId = syncedId, + requestInfo = info + )) + } + + requestsForUser.add(verificationRequest) + dispatchRequestAdded(verificationRequest) + + return verificationRequest + } + + override fun declineVerificationRequestInDMs(otherUserId: String, otherDeviceId: String, transactionId: String, roomId: String) { + sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId + ?: "", roomId, null).cancelTransaction(transactionId, otherUserId, otherDeviceId, CancelCode.User) + + getExistingVerificationRequest(otherUserId, transactionId)?.let { + updatePendingRequest(it.copy( + cancelConclusion = CancelCode.User + )) + } + } + + private fun updatePendingRequest(updated: PendingVerificationRequest) { + val requestsForUser = pendingRequests[updated.otherUserId] + ?: ArrayList().also { + pendingRequests[updated.otherUserId] = it + } + val index = requestsForUser.indexOfFirst { + it.transactionId == updated.transactionId + || it.transactionId == null && it.localID == updated.localID + } + if (index != -1) { + requestsForUser.removeAt(index) + } + requestsForUser.add(updated) + dispatchRequestUpdated(updated) } override fun beginKeyVerificationInDMs(method: String, transactionId: String, roomId: String, @@ -572,7 +780,8 @@ internal class DefaultSasVerificationService @Inject constructor( transactionId, otherUserId, otherDeviceId) - tx.transport = sasTransportRoomMessageFactory.createTransport(roomId, cryptoService, tx) + tx.transport = sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId + ?: "", roomId, tx) addTransaction(tx) tx.start() @@ -582,6 +791,36 @@ internal class DefaultSasVerificationService @Inject constructor( } } + override fun readyPendingVerificationInDMs(otherUserId: String, roomId: String, transactionId: String): Boolean { + Timber.v("## SAS readyPendingVerificationInDMs $otherUserId room:$roomId tx:$transactionId") + // Let's find the related request + val existingRequest = getExistingVerificationRequest(otherUserId, transactionId) + if (existingRequest != null) { + // we need to send a ready event, with matching methods + val transport = sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId + ?: "", roomId, null) + val methods = existingRequest.requestInfo?.methods?.intersect(listOf(KeyVerificationStart.VERIF_METHOD_SAS))?.toList() + if (methods.isNullOrEmpty()) { + Timber.i("Cannot ready this request, no common methods found txId:$transactionId") + // TODO buttons should not be shown in this case? + return false + } + // TODO this is not yet related to a transaction, maybe we should use another method like for cancel? + val readyMsg = transport.createReady(transactionId, credentials.deviceId ?: "", methods) + transport.sendToOther(EventType.KEY_VERIFICATION_READY, readyMsg, + SasVerificationTxState.None, + CancelCode.User, + null // TODO handle error? + ) + updatePendingRequest(existingRequest.copy(readyInfo = readyMsg)) + return true + } else { + Timber.e("## SAS readyPendingVerificationInDMs Verification not found") + // :/ should not be possible... unless live observer very slow + return false + } + } + /** * This string must be unique for the pair of users performing verification for the duration that the transaction is valid */ @@ -606,28 +845,4 @@ internal class DefaultSasVerificationService @Inject constructor( this.removeTransaction(tx.otherUserId, tx.transactionId) } } -// -// fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode, roomId: String? = null) { -// val cancelMessage = KeyVerificationCancel.create(transactionId, code) -// val contentMap = MXUsersDevicesMap() -// contentMap.setObject(userId, userDevice, cancelMessage) -// -// if (roomId != null) { -// -// } else { -// sendToDeviceTask -// .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) { -// this.callback = object : MatrixCallback { -// override fun onSuccess(data: Unit) { -// Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}") -// } -// -// override fun onFailure(failure: Throwable) { -// Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.") -// } -// } -// } -// .executeBy(taskExecutor) -// } -// } } 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..c416bd3be3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt @@ -0,0 +1,42 @@ +/* + * 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 ageLocalTs : Long, + 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 + + val isFinished: Boolean = isSuccessful || cancelConclusion != 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..4a12b05b49 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 @@ -169,6 +169,11 @@ internal abstract class SASVerificationTransaction( } // if not wait for it } + override fun shortCodeDoesNotMatch() { + Timber.v("## SAS short code do not match for id:$transactionId") + cancel(CancelCode.MismatchedSas) + } + override fun acceptVerificationEvent(senderId: String, info: VerificationInfo) { when (info) { is VerificationInfoStart -> onVerificationStart(info) @@ -222,13 +227,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/SasTransport.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransport.kt index ae5f55b662..60e0204800 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransport.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransport.kt @@ -17,6 +17,7 @@ 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.crypto.sas.SasVerificationTxState +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent /** * SAS verification can be performed using toDevice events or via DM. @@ -33,7 +34,9 @@ internal interface SasTransport { onErrorReason: CancelCode, onDone: (() -> Unit)?) - fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) + fun sendVerificationRequest(localID: String, otherUserId: String, roomId: String, callback: (String?, MessageVerificationRequestContent?) -> Unit) + + fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDevice: String, code: CancelCode) fun done(transactionId: String) /** @@ -58,4 +61,6 @@ internal interface SasTransport { shortAuthenticationStrings: List) : VerificationInfoStart fun createMac(tid: String, mac: Map, keys: String): VerificationInfoMac + + fun createReady(tid: String, fromDevice: String, methods: List): VerificationInfoReady } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportRoomMessage.kt index 91adbbd705..20c59def18 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportRoomMessage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportRoomMessage.kt @@ -15,31 +15,36 @@ */ package im.vector.matrix.android.internal.crypto.verification -import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.session.crypto.CryptoService +import android.content.Context +import androidx.lifecycle.Observer +import androidx.work.* +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.R import im.vector.matrix.android.api.session.crypto.sas.CancelCode import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.RelationType -import im.vector.matrix.android.api.session.events.model.toContent +import im.vector.matrix.android.api.session.events.model.* import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent -import im.vector.matrix.android.internal.crypto.tasks.DefaultSendVerificationMessageTask -import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask -import im.vector.matrix.android.internal.session.room.send.SendResponse -import im.vector.matrix.android.internal.task.TaskConstraints -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.TaskThread -import im.vector.matrix.android.internal.task.configureWith +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart +import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory +import im.vector.matrix.android.internal.worker.WorkManagerUtil +import im.vector.matrix.android.internal.worker.WorkerParamsFactory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import timber.log.Timber +import java.util.* +import java.util.concurrent.TimeUnit import javax.inject.Inject internal class SasTransportRoomMessage( + private val context: Context, + private val userId: String, + private val userDevice: String, private val roomId: String, - private val cryptoService: CryptoService, - private val tx: SASVerificationTransaction?, - private val sendVerificationMessageTask: SendVerificationMessageTask, - private val taskExecutor: TaskExecutor + private val monarchy: Monarchy, + private val localEchoEventFactory: LocalEchoEventFactory, + private val tx: SASVerificationTransaction? ) : SasTransport { override fun sendToOther(type: String, @@ -49,83 +54,167 @@ internal class SasTransportRoomMessage( onDone: (() -> Unit)?) { Timber.d("## SAS sending msg type $type") Timber.v("## SAS sending msg info $verificationInfo") - sendVerificationMessageTask.configureWith( - sendVerificationMessageTask.createParamsAndLocalEcho( - type, - roomId, - verificationInfo.toEventContent()!!, - cryptoService - ) - ) { - callbackThread = TaskThread.DM_VERIF - executionThread = TaskThread.DM_VERIF - constraints = TaskConstraints(true) - callback = object : MatrixCallback { - override fun onSuccess(data: SendResponse) { - if (onDone != null) { - onDone() - } else { - tx?.state = nextState - } - } + val event = createEventAndLocalEcho( + type = type, + roomId = roomId, + content = verificationInfo.toEventContent()!! + ) - override fun onFailure(failure: Throwable) { - Timber.e("## SAS verification [${tx?.transactionId}] failed to send toDevice in state : ${tx?.state}") - tx?.cancel(onErrorReason) - } + val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( + userId = userId, + event = event + )) + val enqueueInfo = enqueueSendWork(workerParams) + + // I cannot just listen to the given work request, because when used in a uniqueWork, + // The callback is called while it is still Running ... + +// Futures.addCallback(enqueueInfo.first.result, object : FutureCallback { +// override fun onSuccess(result: Operation.State.SUCCESS?) { +// if (onDone != null) { +// onDone() +// } else { +// tx?.state = nextState +// } +// } +// +// override fun onFailure(t: Throwable) { +// Timber.e("## SAS verification [${tx?.transactionId}] failed to send toDevice in state : ${tx?.state}, reason: ${t.localizedMessage}") +// tx?.cancel(onErrorReason) +// } +// }, listenerExecutor) + + val workLiveData = WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork") + + val observer = object : Observer> { + override fun onChanged(workInfoList: List?) { + workInfoList + ?.filter { it.state == WorkInfo.State.SUCCEEDED } + ?.firstOrNull { it.id == enqueueInfo.second } + ?.let { wInfo -> + if (wInfo.outputData.getBoolean("failed", false)) { + Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}") + tx?.cancel(onErrorReason) + } else { + if (onDone != null) { + onDone() + } else { + tx?.state = nextState + } + } + workLiveData.removeObserver(this) + } } - retryCount = 3 } - .executeBy(taskExecutor) + + // TODO listen to DB to get synced info + GlobalScope.launch(Dispatchers.Main) { + workLiveData.observeForever(observer) + } } - override fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) { - Timber.d("## SAS canceling transaction $transactionId for reason $code") - sendVerificationMessageTask.configureWith( - sendVerificationMessageTask.createParamsAndLocalEcho( - EventType.KEY_VERIFICATION_CANCEL, - roomId, - MessageVerificationCancelContent.create(transactionId, code).toContent(), - cryptoService - ) - ) { - callbackThread = TaskThread.DM_VERIF - executionThread = TaskThread.DM_VERIF - constraints = TaskConstraints(true) - retryCount = 3 - callback = object : MatrixCallback { - override fun onSuccess(data: SendResponse) { - Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}") - } + override fun sendVerificationRequest(localID: String, otherUserId: String, roomId: String, + callback: (String?, MessageVerificationRequestContent?) -> Unit) { + val info = MessageVerificationRequestContent( + body = context.getString(R.string.key_verification_request_fallback_message, userId), + fromDevice = userDevice, + toUserId = otherUserId, + methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS) + ) + val content = info.toContent() - override fun onFailure(failure: Throwable) { - Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.") - } + val event = createEventAndLocalEcho( + localID, + EventType.MESSAGE, + roomId, + content + ) + + val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( + userId = userId, + event = event + )) + + val workRequest = WorkManagerUtil.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerUtil.workConstraints) + .setInputData(workerParams) + .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS) + .build() + + WorkManager.getInstance(context) + .beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest) + .enqueue() + + // I cannot just listen to the given work request, because when used in a uniqueWork, + // The callback is called while it is still Running ... + + val workLiveData = WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork") + + val observer = object : Observer> { + override fun onChanged(workInfoList: List?) { + workInfoList + ?.filter { it.state == WorkInfo.State.SUCCEEDED } + ?.firstOrNull { it.id == workRequest.id } + ?.let { wInfo -> + if (wInfo.outputData.getBoolean("failed", false)) { + callback(null, null) + } else if (wInfo.outputData.getString(localID) != null) { + callback(wInfo.outputData.getString(localID), info) + } else { + callback(null, null) + } + workLiveData.removeObserver(this) + } } } - .executeBy(taskExecutor) + + // TODO listen to DB to get synced info + GlobalScope.launch(Dispatchers.Main) { + workLiveData.observeForever(observer) + } + } + + override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDevice: String, code: CancelCode) { + Timber.d("## SAS canceling transaction $transactionId for reason $code") + val event = createEventAndLocalEcho( + type = EventType.KEY_VERIFICATION_CANCEL, + roomId = roomId, + content = MessageVerificationCancelContent.create(transactionId, code).toContent() + ) + val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( + userId = this.userId, + event = event + )) + enqueueSendWork(workerParams) } override fun done(transactionId: String) { - sendVerificationMessageTask.configureWith( - sendVerificationMessageTask.createParamsAndLocalEcho( - EventType.KEY_VERIFICATION_DONE, - roomId, - MessageVerificationDoneContent( - relatesTo = RelationDefaultContent( - RelationType.REFERENCE, - transactionId - ) - ).toContent(), - cryptoService - ) - ) { - callbackThread = TaskThread.DM_VERIF - executionThread = TaskThread.DM_VERIF - constraints = TaskConstraints(true) - retryCount = 3 - } - .executeBy(taskExecutor) + val event = createEventAndLocalEcho( + type = EventType.KEY_VERIFICATION_DONE, + roomId = roomId, + content = MessageVerificationDoneContent( + relatesTo = RelationDefaultContent( + RelationType.REFERENCE, + transactionId + ) + ).toContent() + ) + val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( + userId = userId, + event = event + )) + enqueueSendWork(workerParams) + } + + private fun enqueueSendWork(workerParams: Data): Pair { + val workRequest = WorkManagerUtil.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerUtil.workConstraints) + .setInputData(workerParams) + .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS) + .build() + return WorkManager.getInstance(context) + .beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest) + .enqueue() to workRequest.id } override fun createAccept(tid: String, @@ -167,16 +256,40 @@ internal class SasTransportRoomMessage( ) ) } + + override fun createReady(tid: String, fromDevice: String, methods: List): VerificationInfoReady { + return MessageVerificationReadyContent( + fromDevice = fromDevice, + relatesTo = RelationDefaultContent( + type = RelationType.REFERENCE, + eventId = tid + ), + methods = methods + ) + } + + private fun createEventAndLocalEcho(localID: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { + return Event( + roomId = roomId, + originServerTs = System.currentTimeMillis(), + senderId = userId, + eventId = localID, + type = type, + content = content, + unsignedData = UnsignedData(age = null, transactionId = localID) + ).also { + localEchoEventFactory.saveLocalEcho(monarchy, it) + } + } } internal class SasTransportRoomMessageFactory @Inject constructor( - private val sendVerificationMessageTask: DefaultSendVerificationMessageTask, - private val taskExecutor: TaskExecutor) { + private val context: Context, + private val monarchy: Monarchy, + private val localEchoEventFactory: LocalEchoEventFactory) { - fun createTransport(roomId: String, - cryptoService: CryptoService, - tx: SASVerificationTransaction? + fun createTransport(userId: String, userDevice: String, roomId: String, tx: SASVerificationTransaction? ): SasTransportRoomMessage { - return SasTransportRoomMessage(roomId, cryptoService, tx, sendVerificationMessageTask, taskExecutor) + return SasTransportRoomMessage(context, userId, userDevice, roomId, monarchy, localEchoEventFactory, tx) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportToDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportToDevice.kt index 85e9099972..8d73695f4a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportToDevice.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportToDevice.kt @@ -19,6 +19,7 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.sas.CancelCode import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.rest.* import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask @@ -33,6 +34,11 @@ internal class SasTransportToDevice( private var taskExecutor: TaskExecutor ) : SasTransport { + override fun sendVerificationRequest(localID: String, otherUserId: String, roomId: String, + callback: (String?, MessageVerificationRequestContent?) -> Unit) { + // TODO "not implemented" + } + override fun sendToOther(type: String, verificationInfo: VerificationInfo, nextState: SasVerificationTxState, @@ -72,11 +78,11 @@ internal class SasTransportToDevice( // To device do not do anything here } - override fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) { + override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDevice: String, code: CancelCode) { Timber.d("## SAS canceling transaction $transactionId for reason $code") val cancelMessage = KeyVerificationCancel.create(transactionId, code) val contentMap = MXUsersDevicesMap() - contentMap.setObject(userId, userDevice, cancelMessage) + contentMap.setObject(otherUserId, otherUserDevice, cancelMessage) sendToDeviceTask .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) { this.callback = object : MatrixCallback { @@ -126,6 +132,14 @@ internal class SasTransportToDevice( messageAuthenticationCodes, shortAuthenticationStrings) } + + override fun createReady(tid: String, fromDevice: String, methods: List): VerificationInfoReady { + return KeyVerificationReady( + transactionID = tid, + fromDevice = fromDevice, + methods = methods + ) + } } internal class SasTransportToDeviceFactory @Inject constructor( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SendVerificationMessageWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SendVerificationMessageWorker.kt new file mode 100644 index 0000000000..55f57d80d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SendVerificationMessageWorker.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2020 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 android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.failure.shouldBeRetried +import im.vector.matrix.android.api.session.crypto.CryptoService +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask +import im.vector.matrix.android.internal.worker.WorkerParamsFactory +import im.vector.matrix.android.internal.worker.getSessionComponent +import timber.log.Timber +import javax.inject.Inject + +internal class SendVerificationMessageWorker constructor(context: Context, params: WorkerParameters) + : CoroutineWorker(context, params) { + + @JsonClass(generateAdapter = true) + internal data class Params( + val userId: String, + val event: Event + ) + + @Inject + lateinit var sendVerificationMessageTask: SendVerificationMessageTask + + @Inject + lateinit var cryptoService: CryptoService + + override suspend fun doWork(): Result { + val errorOutputData = Data.Builder().putBoolean("failed", true).build() + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.success(errorOutputData) + + val sessionComponent = getSessionComponent(params.userId) + ?: return Result.success(errorOutputData).also { + // TODO, can this happen? should I update local echo? + Timber.e("Unknown Session, cannot send message, userId:${params.userId}") + } + sessionComponent.inject(this) + val localId = params.event.eventId ?: "" + return try { + val eventId = sendVerificationMessageTask.execute( + SendVerificationMessageTask.Params( + event = params.event, + cryptoService = cryptoService + ) + ) + + Result.success(Data.Builder().putString(localId, eventId).build()) + } catch (exception: Throwable) { + if (exception.shouldBeRetried()) { + Result.retry() + } else { + Result.success(errorOutputData) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfo.kt index 44a65aa926..5a4838831c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfo.kt @@ -18,8 +18,9 @@ package im.vector.matrix.android.internal.crypto.verification import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceObject -internal interface VerificationInfo { +interface VerificationInfo { fun toEventContent(): Content? = null fun toSendToDeviceObject(): SendToDeviceObject? = null fun isValid() : Boolean + val transactionID: String? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoAccept.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoAccept.kt index 7f639c3bc7..2774a44d49 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoAccept.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoAccept.kt @@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.crypto.verification internal interface VerificationInfoAccept : VerificationInfo { - val transactionID: String? + override val transactionID: String? /** * The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoCancel.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoCancel.kt index 2970358a15..b9bd2cebee 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoCancel.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoCancel.kt @@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.crypto.verification internal interface VerificationInfoCancel : VerificationInfo { - val transactionID: String? + override val transactionID: String? /** * machine-readable reason for cancelling, see [CancelCode] */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoKey.kt index e5deb63b56..99edb53e79 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoKey.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoKey.kt @@ -20,7 +20,7 @@ package im.vector.matrix.android.internal.crypto.verification */ internal interface VerificationInfoKey : VerificationInfo { - val transactionID: String? + override val transactionID: String? /** * The device’s ephemeral public key, as an unpadded base64 string */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoMac.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoMac.kt index 01fc865250..ace1210986 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoMac.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoMac.kt @@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.crypto.verification internal interface VerificationInfoMac : VerificationInfo { - val transactionID: String? + override val transactionID: String? /** * A map of key ID to the MAC of the key, as an unpadded base64 string, calculated using the MAC key diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoReady.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoReady.kt new file mode 100644 index 0000000000..0c5312987e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoReady.kt @@ -0,0 +1,42 @@ +/* + * 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 + +/** + * A new event type is added to the key verification framework: m.key.verification.ready, + * which may be sent by the target of the m.key.verification.request message, upon receipt of the m.key.verification.request event. + * + * The m.key.verification.ready event is optional; the recipient of the m.key.verification.request event may respond directly + * with a m.key.verification.start event instead. + */ +interface VerificationInfoReady : VerificationInfo { + + override val transactionID: String? + + /** + * The ID of the device that sent the m.key.verification.ready message + */ + val fromDevice: String? + + /** + * An array of verification methods that the device supports + */ + val methods: List? +} + +internal interface MessageVerificationReadyFactory { + fun create(tid: String, methods: List, fromDevice: String): VerificationInfoReady +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt index 2248a239fb..d0cfe06815 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt @@ -28,7 +28,7 @@ internal interface VerificationInfoStart : VerificationInfo { * This string must be unique for the pair of users performing verification for the duration that the transaction is valid. * Alice’s device should record this ID and use it in future messages in this transaction. */ - val transactionID: String? + override val transactionID: String? /** * An array of key agreement protocols that Alice’s client understands. 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..ede5c42ad6 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, @@ -56,16 +49,14 @@ internal class VerificationMessageLiveObserver @Inject constructor( EventType.KEY_VERIFICATION_MAC, EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_READY, EventType.MESSAGE, EventType.ENCRYPTED) ) } - 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 +66,8 @@ internal class VerificationMessageLiveObserver @Inject constructor( } .toList() - // 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 - - 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) - } - } - } - } + roomVerificationUpdateTask.configureWith( + RoomVerificationUpdateTask.Params(events, sasVerificationService, cryptoService) + ).executeBy(taskExecutor) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index a208a7a720..6ad46e594a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -21,6 +21,7 @@ import dagger.Component import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.internal.crypto.CryptoModule +import im.vector.matrix.android.internal.crypto.verification.SendVerificationMessageWorker import im.vector.matrix.android.internal.di.MatrixComponent import im.vector.matrix.android.internal.di.SessionAssistedInjectModule import im.vector.matrix.android.internal.network.NetworkConnectivityChecker @@ -98,6 +99,8 @@ internal interface SessionComponent { fun inject(addHttpPusherWorker: AddHttpPusherWorker) + fun inject(sendVerificationMessageWorker: SendVerificationMessageWorker) + @Component.Factory interface Factory { fun create( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index b53fa3ce33..dae9ba3bfd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -71,6 +71,20 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona } } + override fun getExistingDirectRoomWithUser(otherUserId: String): Room? { + Realm.getInstance(monarchy.realmConfiguration).use { realm -> + val roomId = RoomSummaryEntity.where(realm) + .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + .findAll()?.let { dms -> + dms.firstOrNull { + it.otherMemberIds.contains(otherUserId) + } + } + ?.roomId ?: return null + return RoomEntity.where(realm, roomId).findFirst()?.let { roomFactory.create(roomId) } + } + } + override fun getRoomSummary(roomIdOrAlias: String): RoomSummary? { return monarchy .fetchCopyMap({ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt index 6527113054..026c5e9df5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt @@ -20,8 +20,7 @@ import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass -import im.vector.matrix.android.api.failure.Failure -import im.vector.matrix.android.api.failure.MatrixError +import im.vector.matrix.android.api.failure.shouldBeRetried import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.send.SendState @@ -77,11 +76,6 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam } } - private fun Throwable.shouldBeRetried(): Boolean { - return this is Failure.NetworkConnection - || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED) - } - private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) { localEchoUpdater.updateSendState(eventId, SendState.SENDING) executeRequest { 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..956e7d3a65 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 @@ -23,10 +23,7 @@ import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment -import im.vector.riotx.features.crypto.verification.SASVerificationIncomingFragment -import im.vector.riotx.features.crypto.verification.SASVerificationShortCodeFragment -import im.vector.riotx.features.crypto.verification.SASVerificationStartFragment -import im.vector.riotx.features.crypto.verification.SASVerificationVerifiedFragment +import im.vector.riotx.features.crypto.verification.* import im.vector.riotx.features.home.HomeDetailFragment import im.vector.riotx.features.home.HomeDrawerFragment import im.vector.riotx.features.home.LoadingFragment @@ -272,4 +269,24 @@ 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/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index e0b14af9d0..3ac9e7c7e9 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -25,6 +25,7 @@ import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.preference.UserAvatarPreference import im.vector.riotx.features.MainActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity +import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.HomeModule import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity @@ -133,6 +134,8 @@ interface ScreenComponent { fun inject(activity: SoftLogoutActivity) + fun inject(verificationBottomSheet: VerificationBottomSheet) + fun inject(permalinkHandlerActivity: PermalinkHandlerActivity) @Component.Factory 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..6536da73f5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/SpannableUtils.kt @@ -0,0 +1,58 @@ +/* + * 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.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/SASVerificationActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationActivity.kt index cf80bf98fc..222891eb3d 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationActivity.kt @@ -31,6 +31,7 @@ import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.SimpleFragmentActivity import im.vector.riotx.core.platform.WaitingViewData +// TODO Deprecated("replaced by bottomsheet UX") class SASVerificationActivity : SimpleFragmentActivity() { companion object { 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..122af8b625 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeFragment.kt @@ -0,0 +1,162 @@ +/* + * 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 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 -> + val otherUserId = state.otherUser?.id ?: return@withState + val txId = state.transactionId ?: return@withState + // UX echo + ButtonsVisibilityGroup.isInvisible = true + sasCodeWaitingPartnerText.isVisible = true + sharedViewModel.handle(VerificationAction.SASMatchAction(otherUserId, txId)) + } + + @OnClick(R.id.sas_request_cancel_button) + fun onDoNotMatchButtonTapped() = withState(viewModel) { state -> + val otherUserId = state.otherUser?.id ?: return@withState + val txId = state.transactionId ?: return@withState + // UX echo + ButtonsVisibilityGroup.isInvisible = true + sasCodeWaitingPartnerText.isVisible = true + sharedViewModel.handle(VerificationAction.SASDoNotMatchAction(otherUserId, txId)) + } +} 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..cf3a542430 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationCodeViewModel.kt @@ -0,0 +1,168 @@ +/* + * 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.di.HasScreenInjector +import im.vector.riotx.core.platform.EmptyAction +import im.vector.riotx.core.platform.VectorViewModel + +data class SASVerificationCodeViewState( + val transactionId: 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 -> + refreshStateFromTx(session.getSasVerificationService() + .getExistingTransaction(state.otherUser?.id ?: "", state.transactionId + ?: "")) + } + + 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")) + ) + } + } + null -> { + setState { + copy( + isWaitingFromOther = false, + emojiDescription = Fail(Throwable("Unknown Transaction")), + decimalDescription = Fail(Throwable("Unknown Transaction")) + ) + } + } + } + } + + override fun transactionCreated(tx: SasVerificationTransaction) { + transactionUpdated(tx) + } + + override fun transactionUpdated(tx: SasVerificationTransaction) = withState { state -> + if (tx.transactionId == state.transactionId) { + 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() + val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() + val matrixItem = session.getUser(args.otherUserId)?.toMatrixItem() + + return SASVerificationCodeViewState( + transactionId = args.verificationId, + otherUser = matrixItem + ) + } + } + + override fun handle(action: EmptyAction) { + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt index 61f5c5f9fe..cb96900ba7 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt @@ -29,6 +29,7 @@ import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject +// TODO Deprecated("replaced by bottomsheet UX") class SASVerificationIncomingFragment @Inject constructor( private var avatarRenderer: AvatarRenderer ) : VectorBaseFragment() { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationShortCodeFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationShortCodeFragment.kt index ec9a943449..64c1c4b1f0 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationShortCodeFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationShortCodeFragment.kt @@ -29,6 +29,7 @@ import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseFragment import javax.inject.Inject +// TODO Deprecated("replaced by bottomsheet UX") class SASVerificationShortCodeFragment @Inject constructor(): VectorBaseFragment() { private lateinit var viewModel: SasVerificationViewModel 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..4e9699e853 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 @@ -32,6 +32,7 @@ import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseFragment import javax.inject.Inject +// TODO Deprecated("replaced by bottomsheet UX") class SASVerificationStartFragment @Inject constructor(): VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_sas_verification_start @@ -91,7 +92,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/SASVerificationVerifiedFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationVerifiedFragment.kt index 17beb21aff..fe7f9861b0 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationVerifiedFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationVerifiedFragment.kt @@ -21,6 +21,7 @@ import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseFragment import javax.inject.Inject +// TODO Deprecated("replaced by bottomsheet UX") class SASVerificationVerifiedFragment @Inject constructor() : VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_sas_verification_verified 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..ca283e449d 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 @@ -27,6 +27,7 @@ import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.core.utils.LiveEvent import javax.inject.Inject +// TODO Deprecated("replaced by bottomsheet UX") class SasVerificationViewModel @Inject constructor() : ViewModel(), SasVerificationService.SasVerificationListener { 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..61598bd5fc --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt @@ -0,0 +1,235 @@ +/* + * 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 butterknife.Unbinder +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 timber.log.Timber +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 + + private var unBinder: Unbinder? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.bottom_sheet_verification, container, false) + unBinder = ButterKnife.bind(this, view) + return view + } + + override fun onDestroyView() { + super.onDestroyView() + unBinder?.unbind() + unBinder = null + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.requestLiveData.observe(viewLifecycleOwner, 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 + } + + // At this point there is no transaction for this request + + // Transaction has not yet started + if (it.pendingRequest?.cancelConclusion != null) { + // The request has been declined, we should dismiss + dismiss() + } + + // If it's an outgoing + if (it.pendingRequest == null || !it.pendingRequest.isIncoming) { + Timber.v("## SAS show bottom sheet for outgoing request") + if (it.pendingRequest?.isReady == true) { + Timber.v("## SAS show bottom sheet for outgoing and ready request") + // Show choose method fragment with waiting + showFragment(VerificationChooseMethodFragment::class, Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id + ?: "", it.pendingRequest.transactionId)) + }) + } else { + // Stay on the start fragment + showFragment(VerificationRequestFragment::class, Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationArgs( + it.otherUserMxItem?.id ?: "", + it.pendingRequest?.transactionId, + it.roomId)) + }) + } + } else if (it.pendingRequest.isIncoming) { + Timber.v("## SAS show bottom sheet for Incoming request") + // For incoming we can switch to choose method because ready is being sent or already sent + 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 + ) + } + } + } + + companion object { + fun withArgs(roomId: String, otherUserId: String, transactionId: String? = null): VerificationBottomSheet { + return VerificationBottomSheet().apply { + arguments = Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs( + otherUserId = otherUserId, + roomId = roomId, + verificationId = transactionId + )) + } + } + } + } +} + +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..e25a22e668 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -0,0 +1,172 @@ +/* + * 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 pr = session.getSasVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId) + + val sasTx = pr?.transactionId?.let { + session.getSasVerificationService().getExistingTransaction(args.otherUserId, it) + } + + 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)) + } + } + is VerificationAction.StartSASVerification -> { + val request = session.getSasVerificationService().getExistingVerificationRequest(otherUserId, 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) + ?.shortCodeDoesNotMatch() + } + 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..69abc77b6f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodFragment.kt @@ -0,0 +1,67 @@ +/* + * 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.parentFragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseFragment +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..2fcf94a036 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationChooseMethodViewModel.kt @@ -0,0 +1,102 @@ +/* + * 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.api.session.crypto.sas.SasVerificationService +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction +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.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), SasVerificationService.SasVerificationListener { + + override fun transactionCreated(tx: SasVerificationTransaction) {} + + override fun transactionUpdated(tx: SasVerificationTransaction) {} + + override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state -> + val pvr = session.getSasVerificationService().getExistingVerificationRequest(state.otherUserId, 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 + } + + init { + session.getSasVerificationService().addListener(this) + } + + override fun onCleared() { + super.onCleared() + session.getSasVerificationService().removeListener(this) + } + + 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() + val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() + val pvr = session.getSasVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId) + val qrAvailable = pvr?.readyInfo?.methods?.contains(KeyVerificationStart.VERIF_METHOD_SCAN) + ?: false + val emojiAvailable = pvr?.readyInfo?.methods?.contains(KeyVerificationStart.VERIF_METHOD_SAS) + ?: false + + return VerificationChooseMethodViewState(otherUserId = args.otherUserId, + transactionId = args.verificationId ?: "", + QRModeAvailable = qrAvailable, + SASMOdeAvailable = emojiAvailable + ) + } + } + + 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..fdf4b0cbd4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationConclusionFragment.kt @@ -0,0 +1,75 @@ +/* + * 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 androidx.core.view.isVisible +import butterknife.OnClick +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.parentFragmentViewModel +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 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.isVisible = false + 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..d6e4105fdd --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestFragment.kt @@ -0,0 +1,82 @@ +/* + * 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.parentFragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseFragment +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..b21ce4e3a5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationRequestViewModel.kt @@ -0,0 +1,102 @@ +/* + * 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.EmptyAction +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 { + 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 args = viewModelContext.args() + val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() + + val pr = session.getSasVerificationService() + .getExistingVerificationRequest(args.otherUserId, args.verificationId) + return session.getUser(args.otherUserId)?.let { + VerificationRequestViewState( + started = Success(false).takeIf { pr == null } + ?: Success(true).takeIf { pr?.isReady == true } + ?: Loading(), + matrixItem = it.toMatrixItem() + ) + } + } + } + + override fun handle(action: EmptyAction) {} + + 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/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt index 5d00b09204..768f188d7e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt @@ -65,5 +65,7 @@ sealed class RoomDetailAction : VectorViewModelAction { object ResendAll : RoomDetailAction() data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String, val otherdDeviceId: String) : RoomDetailAction() - data class DeclineVerificationRequest(val transactionId: String) : RoomDetailAction() + data class DeclineVerificationRequest(val transactionId: String, val otherUserId: String, val otherdDeviceId: String) : RoomDetailAction() + + data class RequestVerification(val userId: String) : RoomDetailAction() } 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..0efbe255fe 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 @@ -90,6 +90,7 @@ import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotx.features.command.Command +import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.getColorFromUserId import im.vector.riotx.features.home.room.detail.composer.TextComposerAction @@ -431,7 +432,8 @@ class RoomDetailFragment @Inject constructor( composerLayout.sendButton.setContentDescription(getString(descriptionRes)) avatarRenderer.render( - MatrixItem.UserItem(event.root.senderId ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar), + MatrixItem.UserItem(event.root.senderId + ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar), composerLayout.composerRelatedMessageAvatar ) composerLayout.expand { @@ -923,7 +925,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 +962,21 @@ class RoomDetailFragment @Inject constructor( } } } + is RoomDetailAction.RequestVerification -> { + Timber.v("## SAS RequestVerification action") + VerificationBottomSheet.withArgs( + roomDetailArgs.roomId, + data.userId + ).show(parentFragmentManager, "REQ") + } + is RoomDetailAction.AcceptVerificationRequest -> { + Timber.v("## SAS AcceptVerificationRequest action") + VerificationBottomSheet.withArgs( + roomDetailArgs.roomId, + data.otherUserId, + data.transactionId + ).show(parentFragmentManager, "REQ") + } } } } @@ -1114,7 +1131,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..6168a2bd5a 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 @@ -27,7 +27,6 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixPatterns -import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.isImageMessage @@ -49,7 +48,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 +182,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) } } @@ -398,7 +397,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro popDraft() } is ParsedCommand.VerifyUser -> { - session.getSasVerificationService().requestKeyVerificationInDMs(slashCommandResult.userId, room.roomId, null) + session.getSasVerificationService().requestKeyVerificationInDMs(slashCommandResult.userId, room.roomId) _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) popDraft() } @@ -796,18 +795,23 @@ 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 - ) + Timber.v("## SAS handleAcceptVerification ${action.otherUserId}, roomId:${room.roomId}, txId:${action.transactionId}") + if (session.getSasVerificationService().readyPendingVerificationInDMs(action.otherUserId, room.roomId, + action.transactionId)) { + _requestLiveData.postValue(LiveEvent(Success(action))) + } } private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) { - Timber.e("TODO implement $action") + session.getSasVerificationService().declineVerificationRequestInDMs( + action.otherUserId, + action.otherdDeviceId, + action.transactionId, + room.roomId) + } + + private fun handleRequestVerification(action: RoomDetailAction.RequestVerification) { + _requestLiveData.postValue(LiveEvent(Success(action))) } private fun observeSyncState() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt index 29b01120d1..696cc7d904 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -46,6 +46,7 @@ class EncryptionItemFactory @Inject constructor(private val stringProvider: Stri eventId = event.root.eventId ?: "?", senderId = event.root.senderId ?: "", sendState = event.root.sendState, + ageLocalTS = event.root.ageLocalTs, avatarUrl = event.senderAvatar, memberName = event.getDisambiguatedDisplayName(), showInformation = false, 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/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 049998f093..96a90f82c4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -73,6 +73,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses senderId = event.root.senderId ?: "", sendState = event.root.sendState, time = time, + ageLocalTS = event.root.ageLocalTs, avatarUrl = avatarUrl, memberName = formattedMemberName, showInformation = showInformation, 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/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt index 835a789107..fe5d0d03ca 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -28,6 +28,7 @@ data class MessageInformationData( val senderId: String, val sendState: SendState, val time: CharSequence? = null, + val ageLocalTS : Long?, val avatarUrl: String?, val memberName: CharSequence? = null, val showInformation: Boolean = true, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt index db7c4008c0..a0ef876f1a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt @@ -28,6 +28,7 @@ import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService import im.vector.matrix.android.internal.session.room.VerificationState import im.vector.riotx.R import im.vector.riotx.core.resources.ColorProvider @@ -108,6 +109,11 @@ abstract class VerificationRequestItem : AbsBaseMessageItem + + + + + + + + + + + + + + + + + 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..871e28b536 --- /dev/null +++ b/vector/src/main/res/layout/fragment_bottom_sas_verification_code.xml @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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..0c5ab7e6c2 --- /dev/null +++ b/vector/src/main/res/layout/fragment_verification_conclusion.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_verification_request.xml b/vector/src/main/res/layout/fragment_verification_request.xml new file mode 100644 index 0000000000..79c81eb399 --- /dev/null +++ b/vector/src/main/res/layout/fragment_verification_request.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + 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..6c3833a782 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 @@ -21,4 +31,22 @@ You accepted Verification Sent Verification Request + + + You + + Verify by scanning + + Ask the other user to scan this code, or %s to scan theirs + + open your camera + + Verify by Emoji + If you can’t scan the code above, verify by comparing a short, unique selection of emoji. + + QR code image + + Verify %s + Waiting for %s… + For extra security, verify %s by checking a one-time code on both your devices.\n\nFor maximum security, do this in person.