From 26b4b6e1942e6bfb57bcccfc1973223352121d63 Mon Sep 17 00:00:00 2001 From: Valere Date: Sun, 1 Dec 2019 12:44:29 +0100 Subject: [PATCH 01/70] Support verification using room transport --- CHANGES.md | 1 + .../crypto/sas/SasVerificationService.kt | 23 ++ .../crypto/sas/SasVerificationTransaction.kt | 2 +- .../api/session/events/model/EventType.kt | 1 + .../model/message/MessageRelationContent.kt | 26 ++ .../session/room/model/message/MessageType.kt | 1 + .../MessageVerificationAcceptContent.kt | 76 ++++ .../MessageVerificationCancelContent.kt | 58 +++ .../message/MessageVerificationDoneContent.kt | 28 ++ .../message/MessageVerificationKeyContent.kt | 61 ++++ .../message/MessageVerificationMacContent.kt | 57 +++ .../MessageVerificationRequestContent.kt | 35 ++ .../MessageVerificationStartContent.kt | 61 ++++ .../android/internal/crypto/CryptoModule.kt | 3 + .../internal/crypto/DefaultCryptoService.kt | 4 + .../model/rest/KeyVerificationAccept.kt | 38 +- .../model/rest/KeyVerificationCancel.kt | 25 +- .../crypto/model/rest/KeyVerificationKey.kt | 24 +- .../crypto/model/rest/KeyVerificationMac.kt | 43 +-- .../model/rest/KeyVerificationRequest.kt | 48 +++ .../crypto/model/rest/KeyVerificationStart.kt | 26 +- .../internal/crypto/tasks/EncryptEventTask.kt | 80 ++++ .../crypto/tasks/RequestVerificationDMTask.kt | 88 +++++ .../tasks/SendVerificationMessageTask.kt | 101 ++++++ ...aultIncomingSASVerificationTransaction.kt} | 66 ++-- ... DefaultOutgoingSASVerificationRequest.kt} | 75 ++-- .../DefaultSasVerificationService.kt | 342 ++++++++++++++---- .../SASVerificationTransaction.kt | 80 ++-- .../crypto/verification/SasTransport.kt | 53 +++ .../verification/SasTransportRoomMessage.kt | 129 +++++++ .../verification/SasTransportToDevice.kt | 115 ++++++ .../crypto/verification/VerifInfoAccept.kt | 57 +++ .../crypto/verification/VerifInfoCancel.kt | 30 ++ .../crypto/verification/VerifInfoKey.kt | 32 ++ .../crypto/verification/VerifInfoMac.kt | 38 ++ .../crypto/verification/VerifInfoStart.kt | 49 +++ .../crypto/verification/VerificationInfo.kt | 25 ++ .../VerificationMessageLiveObserver.kt | 113 ++++++ .../verification/VerificationTransaction.kt | 3 +- .../internal/network/RetrofitExtensions.kt | 2 +- .../android/internal/session/SessionModule.kt | 5 + .../session/room/send/DefaultSendService.kt | 3 +- .../room/send/LocalEchoEventFactory.kt | 18 + .../session/room/send/LocalEchoUpdater.kt | 3 +- .../src/main/res/values/strings_RiotX.xml | 3 + .../im/vector/riotx/core/di/FragmentModule.kt | 5 + .../vector/riotx/features/command/Command.kt | 4 +- .../riotx/features/command/CommandParser.kt | 11 + .../riotx/features/command/ParsedCommand.kt | 2 + .../home/room/detail/RoomDetailViewModel.kt | 20 +- .../timeline/factory/TimelineItemFactory.kt | 10 + .../helper/TimelineDisplayableEvents.kt | 8 +- .../features/settings/VectorPreferences.kt | 3 +- .../settings/VectorSettingsLabsFragment.kt | 9 +- vector/src/main/res/values/strings.xml | 1 + vector/src/main/res/values/strings_riotX.xml | 2 + .../src/main/res/xml/vector_settings_labs.xml | 1 - 57 files changed, 1939 insertions(+), 288 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageRelationContent.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationAcceptContent.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationCancelContent.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationDoneContent.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationKeyContent.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationMacContent.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationStartContent.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationRequest.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/EncryptEventTask.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RequestVerificationDMTask.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendVerificationMessageTask.kt rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/{IncomingSASVerificationTransaction.kt => DefaultIncomingSASVerificationTransaction.kt} (79%) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/{OutgoingSASVerificationRequest.kt => DefaultOutgoingSASVerificationRequest.kt} (79%) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransport.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportRoomMessage.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportToDevice.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoAccept.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoCancel.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoKey.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoMac.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoStart.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfo.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt diff --git a/CHANGES.md b/CHANGES.md index 21cf052d45..d90237b31b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -33,6 +33,7 @@ Changes in RiotX 0.9.0 (2019-12-05) Features ✨: - Account creation. It's now possible to create account on any homeserver with RiotX (#34) - Iteration of the login flow (#613) + - [SDK] MSC2241 / verification in DMs (#707) Improvements 🙌: - Send mention Pills from composer 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 88c0787b4d..902baae06f 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 @@ -16,19 +16,42 @@ package im.vector.matrix.android.api.session.crypto.sas +import im.vector.matrix.android.api.MatrixCallback + +/** + * https://matrix.org/docs/spec/client_server/r0.5.0#key-verification-framework + * + * Verifying keys manually by reading out the Ed25519 key is not very user friendly, and can lead to errors. + * SAS verification is a user-friendly key verification process. + * SAS verification is intended to be a highly interactive process for users, + * and as such exposes verification methods which are easier for users to use. + */ interface SasVerificationService { + fun addListener(listener: SasVerificationListener) fun removeListener(listener: SasVerificationListener) + /** + * Mark this device as verified manually + */ fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) fun getExistingTransaction(otherUser: String, tid: String): SasVerificationTransaction? + /** + * Shortcut for KeyVerificationStart.VERIF_METHOD_SAS + * @see beginKeyVerification + */ fun beginKeyVerificationSAS(userId: String, deviceID: String): String? + /** + * Request a key verification from another user using toDevice events. + */ fun beginKeyVerification(method: String, userId: String, deviceID: String): String? + fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback?) + // fun transactionUpdated(tx: SasVerificationTransaction) interface SasVerificationListener { 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 d24ccadb55..9610daf294 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 @@ -17,7 +17,7 @@ package im.vector.matrix.android.api.session.crypto.sas interface SasVerificationTransaction { - val state: SasVerificationTxState + var state: SasVerificationTxState val cancelledReason: CancelCode? 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 38c24fa89b..a8c6aed44e 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 @@ -72,6 +72,7 @@ object EventType { const val KEY_VERIFICATION_KEY = "m.key.verification.key" 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" // Relation Events const val REACTION = "m.reaction" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageRelationContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageRelationContent.kt new file mode 100644 index 0000000000..ec773916fd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageRelationContent.kt @@ -0,0 +1,26 @@ +/* + * 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.room.model.relation.RelationDefaultContent + +@JsonClass(generateAdapter = true) +internal data class MessageRelationContent( + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt index 8cef40f21a..d4e6d5ea71 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt @@ -26,6 +26,7 @@ object MessageType { const val MSGTYPE_VIDEO = "m.video" const val MSGTYPE_LOCATION = "m.location" const val MSGTYPE_FILE = "m.file" + const val MSGTYPE_VERIFICATION_REQUEST = "m.key.verification.request" const val FORMAT_MATRIX_HTML = "org.matrix.custom.html" // Add, in local, a fake message type in order to StickerMessage can inherit Message class // Because sticker isn't a message type but a event type without msgtype field 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 new file mode 100644 index 0000000000..bda4b9f0ac --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationAcceptContent.kt @@ -0,0 +1,76 @@ +/* + * 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.AcceptVerifInfoFactory +import im.vector.matrix.android.internal.crypto.verification.VerifInfoAccept +import timber.log.Timber + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationAcceptContent( + @Json(name = "hash") override val hash: String?, + @Json(name = "key_agreement_protocol") override val keyAgreementProtocol: String?, + @Json(name = "message_authentication_code") override val messageAuthenticationCode: String?, + @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List?, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?, + @Json(name = "commitment") override var commitment: String? = null +) : VerifInfoAccept { + + override val transactionID: String? + get() = relatesTo?.eventId + + override fun isValid(): Boolean { + if (transactionID.isNullOrBlank() + || keyAgreementProtocol.isNullOrBlank() + || hash.isNullOrBlank() + || commitment.isNullOrBlank() + || messageAuthenticationCode.isNullOrBlank() + || shortAuthenticationStrings.isNullOrEmpty()) { + Timber.e("## received invalid verification request") + return false + } + return true + } + + override fun toEventContent() = this.toContent() + + companion object : AcceptVerifInfoFactory { + + override fun create(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List): VerifInfoAccept { + return MessageVerificationAcceptContent( + hash, + keyAgreementProtocol, + messageAuthenticationCode, + shortAuthenticationStrings, + RelationDefaultContent( + RelationType.REFERENCE, + tid + ), + commitment + ) + } + } +} 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 new file mode 100644 index 0000000000..08fc3cbdbb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationCancelContent.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.matrix.android.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.crypto.sas.CancelCode +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.VerifInfoCancel + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationCancelContent( + @Json(name = "code") override val code: String? = null, + @Json(name = "reason") override val reason: String? = null, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? + +) : VerifInfoCancel { + + override val transactionID: String? + get() = relatesTo?.eventId + + override fun toEventContent() = this.toContent() + + override fun isValid(): Boolean { + if (transactionID.isNullOrBlank() || code.isNullOrBlank()) { + return false + } + return true + } + + companion object { + fun create(transactionId: String, reason: CancelCode): MessageVerificationCancelContent { + return MessageVerificationCancelContent( + reason.value, + reason.humanReadable, + RelationDefaultContent( + RelationType.REFERENCE, + transactionId + ) + ) + } + } +} 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 new file mode 100644 index 0000000000..965fcb79bb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationDoneContent.kt @@ -0,0 +1,28 @@ +/* + * 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.room.model.relation.RelationDefaultContent +import im.vector.matrix.android.internal.crypto.verification.VerificationInfo + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationDoneContent( + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerificationInfo { + override fun isValid() = true +} 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 new file mode 100644 index 0000000000..0b93e3299a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationKeyContent.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.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.VerifInfoKey +import im.vector.matrix.android.internal.crypto.verification.KeyVerifInfoFactory +import timber.log.Timber + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationKeyContent( + /** + * The device’s ephemeral public key, as an unpadded base64 string + */ + @Json(name = "key") override val key: String? = null, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerifInfoKey { + + override val transactionID: String? + get() = relatesTo?.eventId + + override fun isValid(): Boolean { + if (transactionID.isNullOrBlank() || key.isNullOrBlank()) { + Timber.e("## received invalid verification request") + return false + } + return true + } + + override fun toEventContent() = this.toContent() + + companion object : KeyVerifInfoFactory { + + override fun create(tid: String, pubKey: String): VerifInfoKey { + return MessageVerificationKeyContent( + pubKey, + RelationDefaultContent( + RelationType.REFERENCE, + tid + ) + ) + } + } +} 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 new file mode 100644 index 0000000000..92ea4bca52 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationMacContent.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.VerifInfoMac +import im.vector.matrix.android.internal.crypto.verification.VerifInfoMacFactory + +@JsonClass(generateAdapter = true) +internal data class MessageVerificationMacContent( + @Json(name = "mac") override val mac: Map? = null, + @Json(name = "keys") override val keys: String? = null, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerifInfoMac { + + override val transactionID: String? + get() = relatesTo?.eventId + + override fun toEventContent() = this.toContent() + + override fun isValid(): Boolean { + if (transactionID.isNullOrBlank() || keys.isNullOrBlank() || mac.isNullOrEmpty()) { + return false + } + return true + } + + companion object : VerifInfoMacFactory { + override fun create(tid: String, mac: Map, keys: String): VerifInfoMac { + return MessageVerificationMacContent( + mac, + keys, + RelationDefaultContent( + RelationType.REFERENCE, + tid + ) + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.kt new file mode 100644 index 0000000000..afefa39847 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.kt @@ -0,0 +1,35 @@ +/* + * 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.Content +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent + +@JsonClass(generateAdapter = true) +class MessageVerificationRequestContent( + @Json(name = "msgtype") override val type: String = MessageType.MSGTYPE_VERIFICATION_REQUEST, + @Json(name = "body") override val body: String, + @Json(name = "from_device") val fromDevice: String, + @Json(name = "methods") val methods: List, + @Json(name = "to") val to: String, + // @Json(name = "timestamp") val timestamp: Int, + @Json(name = "format") val format: String? = null, + @Json(name = "formatted_body") val formattedBody: String? = null, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null +) : MessageContent 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 new file mode 100644 index 0000000000..f6ec00ffb2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationStartContent.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.matrix.android.api.session.room.model.message + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.crypto.sas.SasMode +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.model.rest.KeyVerificationStart +import im.vector.matrix.android.internal.crypto.verification.SASVerificationTransaction +import im.vector.matrix.android.internal.crypto.verification.VerifInfoStart +import im.vector.matrix.android.internal.util.JsonCanonicalizer +import timber.log.Timber + +@JsonClass(generateAdapter = true) +data class MessageVerificationStartContent( + @Json(name = "from_device") override val fromDevice: String?, + @Json(name = "hashes") override val hashes: List?, + @Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List?, + @Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List?, + @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List?, + @Json(name = "method") override val method: String?, + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? +) : VerifInfoStart { + + override fun toCanonicalJson(): String? { + return JsonCanonicalizer.getCanonicalJson(MessageVerificationStartContent::class.java, this) + } + + override val transactionID: String? + get() = relatesTo?.eventId + + override fun isValid(): Boolean { + if ( + (transactionID.isNullOrBlank() || fromDevice.isNullOrBlank() || method != KeyVerificationStart.VERIF_METHOD_SAS || keyAgreementProtocols.isNullOrEmpty() || hashes.isNullOrEmpty()) + || !hashes.contains("sha256") || messageAuthenticationCodes.isNullOrEmpty() + || (!messageAuthenticationCodes.contains(SASVerificationTransaction.SAS_MAC_SHA256) && !messageAuthenticationCodes.contains(SASVerificationTransaction.SAS_MAC_SHA256_LONGKDF)) + || shortAuthenticationStrings.isNullOrEmpty() + || !shortAuthenticationStrings.contains(SasMode.DECIMAL)) { + Timber.e("## received invalid verification request") + return false + } + return true + } + + override fun toEventContent() = this.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 a12f6e40ce..4243c6a464 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 @@ -177,6 +177,9 @@ internal abstract class CryptoModule { @Binds abstract fun bindSendToDeviceTask(sendToDeviceTask: DefaultSendToDeviceTask): SendToDeviceTask + @Binds + abstract fun bindEncryptEventTask(encryptEventTask: DefaultEncryptEventTask): EncryptEventTask + @Binds abstract fun bindClaimOneTimeKeysForUsersDeviceTask(claimOneTimeKeysForUsersDevice: DefaultClaimOneTimeKeysForUsersDevice) : ClaimOneTimeKeysForUsersDeviceTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index c50b9e2e10..58ab8dda32 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -136,6 +136,10 @@ internal class DefaultCryptoService @Inject constructor( private val cryptoCoroutineScope: CoroutineScope ) : CryptoService { + init { + sasVerificationService.cryptoService = this + } + private val uiHandler = Handler(Looper.getMainLooper()) // MXEncrypting instance for each room. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationAccept.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationAccept.kt index 7be6f2042c..20d7682cb9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationAccept.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationAccept.kt @@ -17,13 +17,15 @@ 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.VerifInfoAccept +import im.vector.matrix.android.internal.crypto.verification.AcceptVerifInfoFactory import timber.log.Timber /** * Sent by Bob to accept a verification from a previously sent m.key.verification.start message. */ @JsonClass(generateAdapter = true) -data class KeyVerificationAccept( +internal data class KeyVerificationAccept( /** * string to identify the transaction. @@ -31,39 +33,41 @@ data class KeyVerificationAccept( * Alice’s device should record this ID and use it in future messages in this transaction. */ @Json(name = "transaction_id") - var transactionID: String? = null, + override var transactionID: String? = null, /** * The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device */ @Json(name = "key_agreement_protocol") - var keyAgreementProtocol: String? = null, + override var keyAgreementProtocol: String? = null, /** * The hash algorithm that Bob’s device has selected to use, out of the list proposed by Alice’s device */ - var hash: String? = null, + @Json(name = "hash") + override var hash: String? = null, /** * The message authentication code that Bob’s device has selected to use, out of the list proposed by Alice’s device */ @Json(name = "message_authentication_code") - var messageAuthenticationCode: String? = null, + override var messageAuthenticationCode: String? = null, /** * An array of short authentication string methods that Bob’s client (and Bob) understands. Must be a subset of the list proposed by Alice’s device */ @Json(name = "short_authentication_string") - var shortAuthenticationStrings: List? = null, + override var shortAuthenticationStrings: List? = null, /** * The hash (encoded as unpadded base64) of the concatenation of the device’s ephemeral public key (QB, encoded as unpadded base64) * and the canonical JSON representation of the m.key.verification.start message. */ - var commitment: String? = null -) : SendToDeviceObject { + @Json(name = "commitment") + override var commitment: String? = null +) : SendToDeviceObject, VerifInfoAccept { - fun isValid(): Boolean { + override fun isValid(): Boolean { if (transactionID.isNullOrBlank() || keyAgreementProtocol.isNullOrBlank() || hash.isNullOrBlank() @@ -76,13 +80,15 @@ data class KeyVerificationAccept( return true } - companion object { - fun create(tid: String, - keyAgreementProtocol: String, - hash: String, - commitment: String, - messageAuthenticationCode: String, - shortAuthenticationStrings: List): KeyVerificationAccept { + override fun toSendToDeviceObject() = this + + companion object : AcceptVerifInfoFactory { + override fun create(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List): VerifInfoAccept { return KeyVerificationAccept().apply { this.transactionID = tid this.keyAgreementProtocol = keyAgreementProtocol diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt index b5c45e9566..7ffffbbfa1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt @@ -18,40 +18,43 @@ package im.vector.matrix.android.internal.crypto.model.rest import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.internal.crypto.verification.VerifInfoCancel /** * To device event sent by either party to cancel a key verification. */ @JsonClass(generateAdapter = true) -data class KeyVerificationCancel( +internal data class KeyVerificationCancel( /** * the transaction ID of the verification to cancel */ @Json(name = "transaction_id") - var transactionID: String? = null, + override val transactionID: String? = null, /** * machine-readable reason for cancelling, see #CancelCode */ - var code: String? = null, + override var code: String? = null, /** * human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given. */ - var reason: String? = null -) : SendToDeviceObject { + override var reason: String? = null +) : SendToDeviceObject, VerifInfoCancel { companion object { fun create(tid: String, cancelCode: CancelCode): KeyVerificationCancel { - return KeyVerificationCancel().apply { - this.transactionID = tid - this.code = cancelCode.value - this.reason = cancelCode.humanReadable - } + return KeyVerificationCancel( + tid, + cancelCode.value, + cancelCode.humanReadable + ) } } - fun isValid(): Boolean { + override fun toSendToDeviceObject() = this + + override fun isValid(): Boolean { if (transactionID.isNullOrBlank() || code.isNullOrBlank()) { return false } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt index 4c6243fee3..458c12743f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt @@ -17,37 +17,33 @@ 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.KeyVerifInfoFactory +import im.vector.matrix.android.internal.crypto.verification.VerifInfoKey /** * Sent by both devices to send their ephemeral Curve25519 public key to the other device. */ @JsonClass(generateAdapter = true) -data class KeyVerificationKey( +internal data class KeyVerificationKey( /** * the ID of the transaction that the message is part of */ - @Json(name = "transaction_id") - @JvmField - var transactionID: String? = null, + @Json(name = "transaction_id") override var transactionID: String? = null, /** * The device’s ephemeral public key, as an unpadded base64 string */ - @JvmField - var key: String? = null + @Json(name = "key") override val key: String? = null -) : SendToDeviceObject { +) : SendToDeviceObject, VerifInfoKey { - companion object { - fun create(tid: String, key: String): KeyVerificationKey { - return KeyVerificationKey().apply { - this.transactionID = tid - this.key = key - } + companion object : KeyVerifInfoFactory { + override fun create(tid: String, pubKey: String): KeyVerificationKey { + return KeyVerificationKey(tid, pubKey) } } - fun isValid(): Boolean { + override fun isValid(): Boolean { if (transactionID.isNullOrBlank() || key.isNullOrBlank()) { return false } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationMac.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationMac.kt index 8732e366d2..d2c147e145 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationMac.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationMac.kt @@ -17,49 +17,32 @@ 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.VerifInfoMac +import im.vector.matrix.android.internal.crypto.verification.VerifInfoMacFactory /** * Sent by both devices to send the MAC of their device key to the other device. */ @JsonClass(generateAdapter = true) -data class KeyVerificationMac( - /** - * the ID of the transaction that the message is part of - */ - @Json(name = "transaction_id") - var transactionID: String? = null, +internal data class KeyVerificationMac( + @Json(name = "transaction_id") override val transactionID: String? = null, + @Json(name = "mac") override val mac: Map? = null, + @Json(name = "key") override val keys: String? = null - /** - * A map of key ID to the MAC of the key, as an unpadded base64 string, calculated using the MAC key - */ - @JvmField - var mac: Map? = null, +) : SendToDeviceObject, VerifInfoMac { - /** - * The MAC of the comma-separated, sorted list of key IDs given in the mac property, - * as an unpadded base64 string, calculated using the MAC key. - * For example, if the mac property gives MACs for the keys ed25519:ABCDEFG and ed25519:HIJKLMN, then this property will - * give the MAC of the string “ed25519:ABCDEFG,ed25519:HIJKLMN”. - */ - @JvmField - var keys: String? = null - -) : SendToDeviceObject { - - fun isValid(): Boolean { + override fun isValid(): Boolean { if (transactionID.isNullOrBlank() || keys.isNullOrBlank() || mac.isNullOrEmpty()) { return false } return true } - companion object { - fun create(tid: String, mac: Map, keys: String): KeyVerificationMac { - return KeyVerificationMac().apply { - this.transactionID = tid - this.mac = mac - this.keys = keys - } + override fun toSendToDeviceObject(): SendToDeviceObject? = this + + companion object : VerifInfoMacFactory { + override fun create(tid: String, mac: Map, keys: String): VerifInfoMac { + return KeyVerificationMac(tid, mac, keys) } } } 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 new file mode 100644 index 0000000000..14954a17cd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationRequest.kt @@ -0,0 +1,48 @@ +/* + * 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.VerificationInfo + +/** + * Requests a key verification with another user's devices. + */ +@JsonClass(generateAdapter = true) +data class KeyVerificationRequest( + + @Json(name = "from_device") + val fromDevice: String, + /** The verification methods supported by the sender. */ + val methods: List = listOf(KeyVerificationStart.VERIF_METHOD_SAS), + /** + * The POSIX timestamp in milliseconds for when the request was made. + * 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 timestamp: Int, + + @Json(name = "transaction_id") + var transactionID: String? = null + +) : SendToDeviceObject, VerificationInfo { + + override fun isValid(): Boolean { + // TODO + return true + } +} 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 081b19161a..f7cc10a12b 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 @@ -19,21 +19,27 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.crypto.sas.SasMode import im.vector.matrix.android.internal.crypto.verification.SASVerificationTransaction +import im.vector.matrix.android.internal.crypto.verification.VerifInfoStart +import im.vector.matrix.android.internal.util.JsonCanonicalizer import timber.log.Timber /** * Sent by Alice to initiate an interactive key verification. */ @JsonClass(generateAdapter = true) -class KeyVerificationStart : SendToDeviceObject { +class KeyVerificationStart : SendToDeviceObject, VerifInfoStart { + + override fun toCanonicalJson(): String? { + return JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, this) + } /** * Alice’s device ID */ @Json(name = "from_device") - var fromDevice: String? = null + override var fromDevice: String? = null - var method: String? = null + override var method: String? = null /** * String to identify the transaction. @@ -41,7 +47,7 @@ class KeyVerificationStart : SendToDeviceObject { * Alice’s device should record this ID and use it in future messages in this transaction. */ @Json(name = "transaction_id") - var transactionID: String? = null + override var transactionID: String? = null /** * An array of key agreement protocols that Alice’s client understands. @@ -49,13 +55,13 @@ class KeyVerificationStart : SendToDeviceObject { * Other methods may be defined in the future */ @Json(name = "key_agreement_protocols") - var keyAgreementProtocols: List? = null + override var keyAgreementProtocols: List? = null /** * An array of hashes that Alice’s client understands. * Must include “sha256”. Other methods may be defined in the future. */ - var hashes: List? = null + override var hashes: List? = null /** * An array of message authentication codes that Alice’s client understands. @@ -63,7 +69,7 @@ class KeyVerificationStart : SendToDeviceObject { * Other methods may be defined in the future. */ @Json(name = "message_authentication_codes") - var messageAuthenticationCodes: List? = null + override var messageAuthenticationCodes: List? = null /** * An array of short authentication string methods that Alice’s client (and Alice) understands. @@ -72,13 +78,13 @@ class KeyVerificationStart : SendToDeviceObject { * Other methods may be defined in the future */ @Json(name = "short_authentication_string") - var shortAuthenticationStrings: List? = null + override var shortAuthenticationStrings: List? = null companion object { const val VERIF_METHOD_SAS = "m.sas.v1" } - fun isValid(): Boolean { + override fun isValid(): Boolean { if (transactionID.isNullOrBlank() || fromDevice.isNullOrBlank() || method != VERIF_METHOD_SAS @@ -95,4 +101,6 @@ class KeyVerificationStart : SendToDeviceObject { } return true } + + override fun toSendToDeviceObject() = this } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/EncryptEventTask.kt new file mode 100644 index 0000000000..951bc6385a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/EncryptEventTask.kt @@ -0,0 +1,80 @@ +/* + * 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.events.model.Event +import im.vector.matrix.android.api.session.room.send.SendState +import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult +import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater +import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.awaitCallback +import javax.inject.Inject + +internal interface EncryptEventTask : Task { + data class Params(val roomId: String, + val event: Event, + /**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/ + val keepKeys: List? = null, + val crypto: CryptoService + ) +} + +internal class DefaultEncryptEventTask @Inject constructor( +// private val crypto: CryptoService + private val localEchoUpdater: LocalEchoUpdater +) : EncryptEventTask { + override suspend fun execute(params: EncryptEventTask.Params): Event { + if (!params.crypto.isRoomEncrypted(params.roomId)) return params.event + val localEvent = params.event + if (localEvent.eventId == null) { + throw IllegalArgumentException() + } + + localEchoUpdater.updateSendState(localEvent.eventId, SendState.ENCRYPTING) + + val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf() + params.keepKeys?.forEach { + localMutableContent.remove(it) + } + +// try { + awaitCallback { + params.crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) + }.let { result -> + val modifiedContent = HashMap(result.eventContent) + params.keepKeys?.forEach { toKeep -> + localEvent.content?.get(toKeep)?.let { + // put it back in the encrypted thing + modifiedContent[toKeep] = it + } + } + val safeResult = result.copy(eventContent = modifiedContent) + return localEvent.copy( + type = safeResult.eventType, + content = safeResult.eventContent + ) + } +// } catch (throwable: Throwable) { +// val sendState = when (throwable) { +// is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES +// else -> SendState.UNDELIVERED +// } +// localEchoUpdater.updateSendState(localEvent.eventId, sendState) +// throw throwable +// } + } +} 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 new file mode 100644 index 0000000000..57d225a193 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RequestVerificationDMTask.kt @@ -0,0 +1,88 @@ +/* + * 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 roomId: String, + val from: String, + val methods: List, + val to: String, + val cryptoService: CryptoService + ) +} + +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 suspend fun execute(params: RequestVerificationDMTask.Params): SendResponse { + val event = createRequestEvent(params) + val localID = event.eventId!! + + try { + localEchoUpdater.updateSendState(localID, SendState.SENDING) + val executeRequest = executeRequest { + apiCall = roomAPI.send( + localID, + roomId = params.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 createRequestEvent(params: RequestVerificationDMTask.Params): Event { + val event = localEchoEventFactory.createVerificationRequest(params.roomId, params.from, params.to, params.methods).also { + localEchoEventFactory.saveLocalEcho(monarchy, it) + } + if (params.cryptoService.isRoomEncrypted(params.roomId)) { + try { + return encryptEventTask.execute(EncryptEventTask.Params( + params.roomId, + event, + listOf("m.relates_to"), + params.cryptoService + )) + } catch (throwable: Throwable) { + // We said it's ok to send verification request in clear + } + } + return 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 new file mode 100644 index 0000000000..b850a1a1e6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendVerificationMessageTask.kt @@ -0,0 +1,101 @@ +/* + * 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.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 { + data class Params( + val type: String, + val roomId: String, + val content: Content, + val cryptoService: CryptoService? + ) +} + +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 suspend fun execute(params: SendVerificationMessageTask.Params): SendResponse { + val event = createRequestEvent(params) + val localID = event.eventId!! + + try { + localEchoUpdater.updateSendState(localID, SendState.SENDING) + val executeRequest = executeRequest { + apiCall = roomAPI.send( + localID, + roomId = params.roomId, + content = event.content, + eventType = event.type + ) + } + localEchoUpdater.updateSendState(localID, SendState.SENT) + return executeRequest + } catch (e: Throwable) { + localEchoUpdater.updateSendState(localID, SendState.UNDELIVERED) + throw e + } + } + + private suspend fun createRequestEvent(params: SendVerificationMessageTask.Params): Event { + val localID = LocalEcho.createLocalEchoId() + val event = Event( + roomId = params.roomId, + originServerTs = System.currentTimeMillis(), + senderId = userId, + eventId = localID, + type = params.type, + content = params.content, + unsignedData = UnsignedData(age = null, transactionId = localID) + ).also { + localEchoEventFactory.saveLocalEcho(monarchy, it) + } + + if (params.cryptoService?.isRoomEncrypted(params.roomId) == true) { + try { + return encryptEventTask.execute(EncryptEventTask.Params( + params.roomId, + event, + listOf("m.relates_to"), + params.cryptoService + )) + } catch (throwable: Throwable) { + // We said it's ok to send verification request in clear + } + } + return event + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/IncomingSASVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASVerificationTransaction.kt similarity index 79% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/IncomingSASVerificationTransaction.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASVerificationTransaction.kt index 6ed5be4881..aac2b49f57 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/IncomingSASVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASVerificationTransaction.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.crypto.verification import android.util.Base64 +import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.sas.CancelCode import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction @@ -23,33 +24,20 @@ import im.vector.matrix.android.api.session.crypto.sas.SasMode 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.internal.crypto.actions.SetDeviceVerificationAction -import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept -import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey -import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac -import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore -import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.util.JsonCanonicalizer import timber.log.Timber -internal class IncomingSASVerificationTransaction( - private val sasVerificationService: DefaultSasVerificationService, - private val setDeviceVerificationAction: SetDeviceVerificationAction, - private val credentials: Credentials, +internal class DefaultIncomingSASVerificationTransaction( + setDeviceVerificationAction: SetDeviceVerificationAction, + override val credentials: Credentials, private val cryptoStore: IMXCryptoStore, - private val sendToDeviceTask: SendToDeviceTask, - private val taskExecutor: TaskExecutor, deviceFingerprint: String, transactionId: String, - otherUserID: String) - : SASVerificationTransaction( - sasVerificationService, + otherUserID: String +) : SASVerificationTransaction( setDeviceVerificationAction, credentials, cryptoStore, - sendToDeviceTask, - taskExecutor, deviceFingerprint, transactionId, otherUserID, @@ -78,10 +66,10 @@ internal class IncomingSASVerificationTransaction( } } - override fun onVerificationStart(startReq: KeyVerificationStart) { - Timber.v("## SAS received verification request from state $state") + override fun onVerificationStart(startReq: VerifInfoStart) { + Timber.v("## SAS I: received verification request from state $state") if (state != SasVerificationTxState.None) { - Timber.e("## received verification request from invalid state") + Timber.e("## SAS I: received verification request from invalid state") // should I cancel?? throw IllegalStateException("Interactive Key verification already started") } @@ -92,7 +80,7 @@ internal class IncomingSASVerificationTransaction( override fun performAccept() { if (state != SasVerificationTxState.OnStarted) { - Timber.e("## Cannot perform accept from state $state") + Timber.e("## SAS Cannot perform accept from state $state") return } @@ -109,7 +97,7 @@ internal class IncomingSASVerificationTransaction( if (listOf(agreedProtocol, agreedHash, agreedMac).any { it.isNullOrBlank() } || agreedShortCode.isNullOrEmpty()) { // Failed to find agreement - Timber.e("## Failed to find agreement ") + Timber.e("## SAS Failed to find agreement ") cancel(CancelCode.UnknownMethod) return } @@ -118,15 +106,15 @@ internal class IncomingSASVerificationTransaction( val mxDeviceInfo = cryptoStore.getUserDevice(deviceId = otherDeviceId!!, userId = otherUserId) if (mxDeviceInfo?.fingerprint() == null) { - Timber.e("## Failed to find device key ") + Timber.e("## SAS Failed to find device key ") // TODO force download keys!! // would be probably better to download the keys // for now I cancel cancel(CancelCode.User) } else { - // val otherKey = info.identityKey() + // val otherKey = info.identityKey() // need to jump back to correct thread - val accept = KeyVerificationAccept.create( + val accept = transport.createAccept( tid = transactionId, keyAgreementProtocol = agreedProtocol!!, hash = agreedHash!!, @@ -138,13 +126,13 @@ internal class IncomingSASVerificationTransaction( } } - private fun doAccept(accept: KeyVerificationAccept) { + private fun doAccept(accept: VerifInfoAccept) { this.accepted = accept - Timber.v("## SAS accept request id:$transactionId") + Timber.v("## SAS incoming accept request id:$transactionId") // The hash commitment is the hash (using the selected hash algorithm) of the unpadded base64 representation of QB, // concatenated with the canonical JSON representation of the content of the m.key.verification.start message - val concat = getSAS().publicKey + JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, startReq!!) + val concat = getSAS().publicKey + startReq!!.toCanonicalJson() accept.commitment = hashUsingAgreedHashMethod(concat) ?: "" // we need to send this to other device now state = SasVerificationTxState.SendingAccept @@ -156,15 +144,15 @@ internal class IncomingSASVerificationTransaction( } } - override fun onVerificationAccept(accept: KeyVerificationAccept) { + override fun onVerificationAccept(accept: VerifInfoAccept) { Timber.v("## SAS invalid message for incoming request id:$transactionId") cancel(CancelCode.UnexpectedMessage) } - override fun onKeyVerificationKey(userId: String, vKey: KeyVerificationKey) { + override fun onKeyVerificationKey(userId: String, vKey: VerifInfoKey) { Timber.v("## SAS received key for request id:$transactionId") if (state != SasVerificationTxState.SendingAccept && state != SasVerificationTxState.Accepted) { - Timber.e("## received key from invalid state $state") + Timber.e("## SAS received key from invalid state $state") cancel(CancelCode.UnexpectedMessage) return } @@ -175,7 +163,7 @@ internal class IncomingSASVerificationTransaction( // sending Bob’s public key QB val pubKey = getSAS().publicKey - val keyToDevice = KeyVerificationKey.create(transactionId, pubKey) + val keyToDevice = transport.createKey(transactionId, pubKey) // we need to send this to other device now state = SasVerificationTxState.SendingKey this.sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, SasVerificationTxState.KeySent, CancelCode.User) { @@ -206,14 +194,16 @@ internal class IncomingSASVerificationTransaction( // emoji: generate six bytes by using HKDF. shortCodeBytes = getSAS().generateShortCode(sasInfo, 6) - Timber.e("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}") - Timber.e("************ BOB EMOJI CODE ${getShortCodeRepresentation(SasMode.EMOJI)}") + if (BuildConfig.LOG_PRIVATE_DATA) { + Timber.v("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}") + Timber.v("************ BOB EMOJI CODE ${getShortCodeRepresentation(SasMode.EMOJI)}") + } state = SasVerificationTxState.ShortCodeReady } - override fun onKeyVerificationMac(vKey: KeyVerificationMac) { - Timber.v("## SAS received mac for request id:$transactionId") + override fun onKeyVerificationMac(vKey: VerifInfoMac) { + Timber.v("## SAS I: received mac for request id:$transactionId") // Check for state? if (state != SasVerificationTxState.SendingKey && state != SasVerificationTxState.KeySent @@ -221,7 +211,7 @@ internal class IncomingSASVerificationTransaction( && state != SasVerificationTxState.ShortCodeAccepted && state != SasVerificationTxState.SendingMac && state != SasVerificationTxState.MacSent) { - Timber.e("## received key from invalid state $state") + Timber.e("## SAS I: received key from invalid state $state") cancel(CancelCode.UnexpectedMessage) return } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/OutgoingSASVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASVerificationRequest.kt similarity index 79% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/OutgoingSASVerificationRequest.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASVerificationRequest.kt index cade637cce..4362e897c8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/OutgoingSASVerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASVerificationRequest.kt @@ -21,34 +21,22 @@ import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationRe 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.internal.crypto.actions.SetDeviceVerificationAction -import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept -import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey -import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore -import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.util.JsonCanonicalizer import timber.log.Timber -internal class OutgoingSASVerificationRequest( - private val sasVerificationService: DefaultSasVerificationService, - private val setDeviceVerificationAction: SetDeviceVerificationAction, - private val credentials: Credentials, - private val cryptoStore: IMXCryptoStore, - private val sendToDeviceTask: SendToDeviceTask, - private val taskExecutor: TaskExecutor, +internal class DefaultOutgoingSASVerificationRequest( + setDeviceVerificationAction: SetDeviceVerificationAction, + credentials: Credentials, + cryptoStore: IMXCryptoStore, deviceFingerprint: String, transactionId: String, otherUserId: String, - otherDeviceId: String) - : SASVerificationTransaction( - sasVerificationService, + otherDeviceId: String +) : SASVerificationTransaction( setDeviceVerificationAction, credentials, cryptoStore, - sendToDeviceTask, - taskExecutor, deviceFingerprint, transactionId, otherUserId, @@ -78,14 +66,14 @@ internal class OutgoingSASVerificationRequest( } } - override fun onVerificationStart(startReq: KeyVerificationStart) { - Timber.e("## onVerificationStart - unexpected id:$transactionId") + override fun onVerificationStart(startReq: VerifInfoStart) { + Timber.e("## SAS O: onVerificationStart - unexpected id:$transactionId") cancel(CancelCode.UnexpectedMessage) } fun start() { if (state != SasVerificationTxState.None) { - Timber.e("## start verification from invalid state") + Timber.e("## SAS O: start verification from invalid state") // should I cancel?? throw IllegalStateException("Interactive Key verification already started") } @@ -111,10 +99,33 @@ internal class OutgoingSASVerificationRequest( ) } - override fun onVerificationAccept(accept: KeyVerificationAccept) { - Timber.v("## onVerificationAccept id:$transactionId") +// fun request() { +// if (state != SasVerificationTxState.None) { +// Timber.e("## start verification from invalid state") +// // should I cancel?? +// throw IllegalStateException("Interactive Key verification already started") +// } +// +// val requestMessage = KeyVerificationRequest( +// fromDevice = session.sessionParams.credentials.deviceId ?: "", +// methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS), +// timestamp = System.currentTimeMillis().toInt(), +// transactionID = transactionId +// ) +// +// sendToOther( +// EventType.KEY_VERIFICATION_REQUEST, +// requestMessage, +// SasVerificationTxState.None, +// CancelCode.User, +// null +// ) +// } + + override fun onVerificationAccept(accept: VerifInfoAccept) { + Timber.v("## SAS O: onVerificationAccept id:$transactionId") if (state != SasVerificationTxState.Started) { - Timber.e("## received accept request from invalid state $state") + Timber.e("## SAS O: received accept request from invalid state $state") cancel(CancelCode.UnexpectedMessage) return } @@ -123,7 +134,7 @@ internal class OutgoingSASVerificationRequest( || !KNOWN_HASHES.contains(accept.hash) || !KNOWN_MACS.contains(accept.messageAuthenticationCode) || accept.shortAuthenticationStrings!!.intersect(KNOWN_SHORT_CODES).isEmpty()) { - Timber.e("## received accept request from invalid state") + Timber.e("## SAS O: received accept request from invalid state") cancel(CancelCode.UnknownMethod) return } @@ -137,7 +148,7 @@ internal class OutgoingSASVerificationRequest( // and replies with a to_device message with type set to “m.key.verification.key”, sending Alice’s public key QA val pubKey = getSAS().publicKey - val keyToDevice = KeyVerificationKey.create(transactionId, pubKey) + val keyToDevice = transport.createKey(transactionId, pubKey) // we need to send this to other device now state = SasVerificationTxState.SendingKey sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, SasVerificationTxState.KeySent, CancelCode.User) { @@ -148,8 +159,8 @@ internal class OutgoingSASVerificationRequest( } } - override fun onKeyVerificationKey(userId: String, vKey: KeyVerificationKey) { - Timber.v("## onKeyVerificationKey id:$transactionId") + override fun onKeyVerificationKey(userId: String, vKey: VerifInfoKey) { + Timber.v("## SAS O: onKeyVerificationKey id:$transactionId") if (state != SasVerificationTxState.SendingKey && state != SasVerificationTxState.KeySent) { Timber.e("## received key from invalid state $state") cancel(CancelCode.UnexpectedMessage) @@ -163,7 +174,7 @@ internal class OutgoingSASVerificationRequest( // in Bob’s m.key.verification.key and the content of Alice’s m.key.verification.start message. // check commitment - val concat = vKey.key + JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, startReq!!) + val concat = vKey.key + startReq!!.toCanonicalJson() val otherCommitment = hashUsingAgreedHashMethod(concat) ?: "" if (accepted!!.commitment.equals(otherCommitment)) { @@ -190,14 +201,14 @@ internal class OutgoingSASVerificationRequest( } } - override fun onKeyVerificationMac(vKey: KeyVerificationMac) { - Timber.v("## onKeyVerificationMac id:$transactionId") + override fun onKeyVerificationMac(vKey: VerifInfoMac) { + Timber.v("## SAS O: onKeyVerificationMac id:$transactionId") if (state != SasVerificationTxState.OnKeyReceived && state != SasVerificationTxState.ShortCodeReady && state != SasVerificationTxState.ShortCodeAccepted && state != SasVerificationTxState.SendingMac && state != SasVerificationTxState.MacSent) { - Timber.e("## received key from invalid state $state") + Timber.e("## SAS O: received key from invalid state $state") cancel(CancelCode.UnexpectedMessage) return } 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 e0cd47e0e0..6552dca7ab 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 @@ -21,6 +21,7 @@ import android.os.Looper import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.crypto.CryptoService 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.SasVerificationTxState @@ -28,6 +29,7 @@ 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.toModel +import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.internal.crypto.DeviceListManager import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction @@ -35,24 +37,23 @@ 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.crypto.tasks.RequestVerificationDMTask import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask 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 import timber.log.Timber -import java.lang.Exception -import java.util.UUID +import java.util.* import javax.inject.Inject +import kotlin.collections.ArrayList import kotlin.collections.HashMap - -/** - * Manages all current verifications transactions with short codes. - * Short codes interactive verification is a more user friendly way of verifying devices - * that is still maintaining a good level of security (alternative to the 43-character strings compare method). - */ +import kotlin.collections.set @SessionScope internal class DefaultSasVerificationService @Inject constructor(private val credentials: Credentials, @@ -61,12 +62,18 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre private val deviceListManager: DeviceListManager, private val setDeviceVerificationAction: SetDeviceVerificationAction, private val sendToDeviceTask: SendToDeviceTask, + private val requestVerificationDMTask: DefaultRequestVerificationDMTask, private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val sasTransportRoomMessageFactory: SasTransportRoomMessageFactory, + private val sasToDeviceTransportFactory: SasToDeviceTransportFactory, private val taskExecutor: TaskExecutor) : VerificationTransaction.Listener, SasVerificationService { private val uiHandler = Handler(Looper.getMainLooper()) + // Cannot be injected in constructor as it creates a dependency cycle + lateinit var cryptoService: CryptoService + // map [sender : [transaction]] private val txMap = HashMap>() @@ -96,6 +103,39 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre } } + fun onRoomEvent(event: Event) { + GlobalScope.launch(coroutineDispatchers.crypto) { + when (event.getClearType()) { + EventType.KEY_VERIFICATION_START -> { + onRoomStartRequestReceived(event) + } + EventType.KEY_VERIFICATION_CANCEL -> { + onRoomCancelReceived(event) + } + EventType.KEY_VERIFICATION_ACCEPT -> { + onRoomAcceptReceived(event) + } + EventType.KEY_VERIFICATION_KEY -> { + onRoomKeyRequestReceived(event) + } + EventType.KEY_VERIFICATION_MAC -> { + onRoomMacReceived(event) + } + EventType.KEY_VERIFICATION_DONE -> { + // TODO? + } + EventType.MESSAGE -> { + if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.type) { + onRoomRequestReceived(event) + } + } + else -> { + // ignore + } + } + } + } + private var listeners = ArrayList() override fun addListener(listener: SasVerificationService.SasVerificationListener) { @@ -150,15 +190,64 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre } } + fun onRoomRequestReceived(event: Event) { + // TODO + Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}") + } + + private suspend fun onRoomStartRequestReceived(event: Event) { + val startReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + + val otherUserId = event.senderId + if (startReq?.isValid()?.not() == true) { + Timber.e("## received invalid verification request") + if (startReq.transactionID != null) { + sasTransportRoomMessageFactory.createTransport(event.roomId + ?: "", cryptoService).cancelTransaction( + startReq.transactionID ?: "", + otherUserId!!, + startReq.fromDevice ?: event.getSenderKey()!!, + CancelCode.UnknownMethod + ) + } + return + } + + handleStart(otherUserId, startReq as VerifInfoStart) { + it.transport = sasTransportRoomMessageFactory.createTransport(event.roomId + ?: "", cryptoService) + }?.let { + sasTransportRoomMessageFactory.createTransport(event.roomId + ?: "", cryptoService).cancelTransaction( + startReq.transactionID ?: "", + otherUserId!!, + startReq.fromDevice ?: event.getSenderKey()!!, + it + ) + } + } + private suspend fun onStartRequestReceived(event: Event) { + Timber.e("## SAS received Start request ${event.eventId}") val startReq = event.getClearContent().toModel()!! + Timber.v("## SAS received Start request $startReq") val otherUserId = event.senderId if (!startReq.isValid()) { - Timber.e("## received invalid verification request") + Timber.e("## SAS received invalid verification request") if (startReq.transactionID != null) { - cancelTransaction( - startReq.transactionID!!, +// cancelTransaction( +// startReq.transactionID!!, +// otherUserId!!, +// startReq.fromDevice ?: event.getSenderKey()!!, +// CancelCode.UnknownMethod +// ) + sasToDeviceTransportFactory.createTransport(null).cancelTransaction( + startReq.transactionID ?: "", otherUserId!!, startReq.fromDevice ?: event.getSenderKey()!!, CancelCode.UnknownMethod @@ -167,8 +256,22 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre return } // Download device keys prior to everything + handleStart(otherUserId, startReq) { + it.transport = sasToDeviceTransportFactory.createTransport(it) + }?.let { + sasToDeviceTransportFactory.createTransport(null).cancelTransaction( + startReq.transactionID ?: "", + otherUserId!!, + startReq.fromDevice ?: event.getSenderKey()!!, + it + ) + } + } + + private suspend fun handleStart(otherUserId: String?, startReq: VerifInfoStart, txConfigure: (SASVerificationTransaction) -> Unit): CancelCode? { + Timber.d("## SAS onStartRequestReceived ${startReq.transactionID!!}") if (checkKeysAreDownloaded(otherUserId!!, startReq) != null) { - Timber.v("## SAS onStartRequestReceived ${startReq.transactionID!!}") + Timber.v("## SAS onStartRequestReceived $startReq") val tid = startReq.transactionID!! val existing = getExistingTransaction(otherUserId, tid) val existingTxs = getExistingTransactionsForUser(otherUserId) @@ -176,43 +279,46 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre // should cancel both! Timber.v("## SAS onStartRequestReceived - Request exist with same if ${startReq.transactionID!!}") existing.cancel(CancelCode.UnexpectedMessage) - cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) + return CancelCode.UnexpectedMessage + // cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) } else if (existingTxs?.isEmpty() == false) { Timber.v("## SAS onStartRequestReceived - There is already a transaction with this user ${startReq.transactionID!!}") // Multiple keyshares between two devices: any two devices may only have at most one key verification in flight at a time. existingTxs.forEach { it.cancel(CancelCode.UnexpectedMessage) } - cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) + return CancelCode.UnexpectedMessage + // cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) } else { // Ok we can create if (KeyVerificationStart.VERIF_METHOD_SAS == startReq.method) { Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}") - val tx = IncomingSASVerificationTransaction( - this, + val tx = DefaultIncomingSASVerificationTransaction( +// this, setDeviceVerificationAction, credentials, cryptoStore, - sendToDeviceTask, - taskExecutor, myDeviceInfoHolder.get().myDevice.fingerprint()!!, startReq.transactionID!!, - otherUserId) + otherUserId).also { txConfigure(it) } addTransaction(tx) - tx.acceptToDeviceEvent(otherUserId, startReq) + tx.acceptVerificationEvent(otherUserId, startReq) } else { Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}") - cancelTransaction(tid, otherUserId, startReq.fromDevice - ?: event.getSenderKey()!!, CancelCode.UnknownMethod) + return CancelCode.UnknownMethod + // cancelTransaction(tid, otherUserId, startReq.fromDevice +// ?: event.getSenderKey()!!, CancelCode.UnknownMethod) } } } else { - cancelTransaction(startReq.transactionID!!, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) + return CancelCode.UnexpectedMessage +// cancelTransaction(startReq.transactionID!!, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) } + return null } private suspend fun checkKeysAreDownloaded(otherUserId: String, - startReq: KeyVerificationStart): MXUsersDevicesMap? { + startReq: VerifInfoStart): MXUsersDevicesMap? { return try { val keys = deviceListManager.downloadKeys(listOf(otherUserId), true) val deviceIds = keys.getUserDeviceIds(otherUserId) ?: return null @@ -222,17 +328,36 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre } } - private suspend fun onCancelReceived(event: Event) { + private fun onRoomCancelReceived(event: Event) { + val cancelReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + if (cancelReq == null || cancelReq.isValid().not()) { + // ignore + Timber.e("## SAS Received invalid key request") + // TODO should we cancel? + return + } + handleOnCancel(event.senderId!!, cancelReq) + } + + private fun onCancelReceived(event: Event) { Timber.v("## SAS onCancelReceived") val cancelReq = event.getClearContent().toModel()!! if (!cancelReq.isValid()) { // ignore - Timber.e("## Received invalid accept request") + Timber.e("## SAS Received invalid accept request") return } val otherUserId = event.senderId!! + handleOnCancel(otherUserId, cancelReq) + } + + private fun handleOnCancel(otherUserId: String, cancelReq: VerifInfoCancel) { Timber.v("## SAS onCancelReceived otherUser:$otherUserId reason:${cancelReq.reason}") val existing = getExistingTransaction(otherUserId, cancelReq.transactionID!!) if (existing == null) { @@ -245,65 +370,119 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre } } - private suspend fun onAcceptReceived(event: Event) { - val acceptReq = event.getClearContent().toModel()!! + private fun onRoomAcceptReceived(event: Event) { + Timber.d("## SAS Received Accept via DM $event") + val accept = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + ?: return + handleAccept(accept, event.senderId!!) + } + private fun onAcceptReceived(event: Event) { + Timber.d("## SAS Received Accept $event") + val acceptReq = event.getClearContent().toModel() ?: return + handleAccept(acceptReq, event.senderId!!) + } + + private fun handleAccept(acceptReq: VerifInfoAccept, senderId: String) { if (!acceptReq.isValid()) { // ignore - Timber.e("## Received invalid accept request") + Timber.e("## SAS Received invalid accept request") return } - val otherUserId = event.senderId!! + val otherUserId = senderId val existing = getExistingTransaction(otherUserId, acceptReq.transactionID!!) if (existing == null) { - Timber.e("## Received invalid accept request") + Timber.e("## SAS Received invalid accept request") return } if (existing is SASVerificationTransaction) { - existing.acceptToDeviceEvent(otherUserId, acceptReq) + existing.acceptVerificationEvent(otherUserId, acceptReq) } else { // not other types now } } - private suspend fun onKeyReceived(event: Event) { + private fun onRoomKeyRequestReceived(event: Event) { + val keyReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + if (keyReq == null || keyReq.isValid().not()) { + // ignore + Timber.e("## SAS Received invalid key request") + // TODO should we cancel? + return + } + handleKeyReceived(event, keyReq) + } + + private fun onKeyReceived(event: Event) { val keyReq = event.getClearContent().toModel()!! if (!keyReq.isValid()) { // ignore - Timber.e("## Received invalid key request") + Timber.e("## SAS Received invalid key request") return } + handleKeyReceived(event, keyReq) + } + + private fun handleKeyReceived(event: Event, keyReq: VerifInfoKey) { + Timber.d("## SAS Received Key from ${event.senderId} with info $keyReq") val otherUserId = event.senderId!! val existing = getExistingTransaction(otherUserId, keyReq.transactionID!!) if (existing == null) { - Timber.e("## Received invalid accept request") + Timber.e("## SAS Received invalid accept request") return } if (existing is SASVerificationTransaction) { - existing.acceptToDeviceEvent(otherUserId, keyReq) + existing.acceptVerificationEvent(otherUserId, keyReq) } else { // not other types now } } - private suspend fun onMacReceived(event: Event) { - val macReq = event.getClearContent().toModel()!! - - if (!macReq.isValid()) { + private fun onRoomMacReceived(event: Event) { + val macReq = event.getClearContent().toModel() + ?.copy( + // relates_to is in clear in encrypted payload + relatesTo = event.content.toModel()?.relatesTo + ) + if (macReq == null || macReq.isValid().not() || event.senderId == null) { // ignore - Timber.e("## Received invalid key request") + Timber.e("## SAS Received invalid mac request") + // TODO should we cancel? return } - val otherUserId = event.senderId!! - val existing = getExistingTransaction(otherUserId, macReq.transactionID!!) + handleMacReceived(event.senderId, macReq) + } + + private fun onMacReceived(event: Event) { + val macReq = event.getClearContent().toModel()!! + + if (!macReq.isValid() || event.senderId == null) { + // ignore + Timber.e("## SAS Received invalid mac request") + return + } + handleMacReceived(event.senderId, macReq) + } + + private fun handleMacReceived(senderId: String, macReq: VerifInfoMac) { + Timber.v("## SAS Received $macReq") + val existing = getExistingTransaction(senderId, macReq.transactionID!!) if (existing == null) { - Timber.e("## Received invalid accept request") + Timber.e("## SAS Received invalid accept request") return } if (existing is SASVerificationTransaction) { - existing.acceptToDeviceEvent(otherUserId, macReq) + existing.acceptVerificationEvent(senderId, macReq) } else { // not other types known for now } @@ -346,13 +525,10 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre val txID = createUniqueIDForTransaction(userId, deviceID) // should check if already one (and cancel it) if (KeyVerificationStart.VERIF_METHOD_SAS == method) { - val tx = OutgoingSASVerificationRequest( - this, + val tx = DefaultOutgoingSASVerificationRequest( setDeviceVerificationAction, credentials, cryptoStore, - sendToDeviceTask, - taskExecutor, myDeviceInfoHolder.get().myDevice.fingerprint()!!, txID, userId, @@ -366,6 +542,30 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre } } + override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback?) { + requestVerificationDMTask.configureWith( + RequestVerificationDMTask.Params( + 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 onFailure(failure: Throwable) { + callback?.onFailure(failure) + } + } + constraints = TaskConstraints(true) + retryCount = 3 + }.executeBy(taskExecutor) + } + /** * This string must be unique for the pair of users performing verification for the duration that the transaction is valid */ @@ -390,24 +590,28 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre this.removeTransaction(tx.otherUserId, tx.transactionId) } } - - fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) { - val cancelMessage = KeyVerificationCancel.create(transactionId, code) - val contentMap = MXUsersDevicesMap() - contentMap.setObject(userId, userDevice, cancelMessage) - - 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) - } +// +// 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/SASVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt index 589103d38a..443443afad 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 @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.crypto.verification import android.os.Build -import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.crypto.sas.CancelCode import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation @@ -26,13 +25,8 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXKey -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.SendToDeviceTask import im.vector.matrix.android.internal.extensions.toUnsignedInt -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.task.configureWith import org.matrix.olm.OlmSAS import org.matrix.olm.OlmUtility import timber.log.Timber @@ -42,12 +36,9 @@ import kotlin.properties.Delegates * Represents an ongoing short code interactive key verification between two devices. */ internal abstract class SASVerificationTransaction( - private val sasVerificationService: DefaultSasVerificationService, private val setDeviceVerificationAction: SetDeviceVerificationAction, - private val credentials: Credentials, + open val credentials: Credentials, private val cryptoStore: IMXCryptoStore, - private val sendToDeviceTask: SendToDeviceTask, - private val taskExecutor: TaskExecutor, private val deviceFingerprint: String, transactionId: String, otherUserId: String, @@ -55,6 +46,8 @@ internal abstract class SASVerificationTransaction( isIncoming: Boolean) : VerificationTransaction(transactionId, otherUserId, otherDevice, isIncoming) { + lateinit var transport: SasTransport + companion object { const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256" const val SAS_MAC_SHA256 = "hkdf-hmac-sha256" @@ -95,13 +88,13 @@ internal abstract class SASVerificationTransaction( private var olmSas: OlmSAS? = null - var startReq: KeyVerificationStart? = null - var accepted: KeyVerificationAccept? = null + var startReq: VerifInfoStart? = null + var accepted: VerifInfoAccept? = null var otherKey: String? = null var shortCodeBytes: ByteArray? = null - var myMac: KeyVerificationMac? = null - var theirMac: KeyVerificationMac? = null + var myMac: VerifInfoMac? = null + var theirMac: VerifInfoMac? = null fun getSAS(): OlmSAS { if (olmSas == null) olmSas = OlmSAS() @@ -160,7 +153,7 @@ internal abstract class SASVerificationTransaction( return } - val macMsg = KeyVerificationMac.create(transactionId, mapOf(keyId to macString), keyStrings) + val macMsg = transport.createMac(transactionId, mapOf(keyId to macString), keyStrings) myMac = macMsg state = SasVerificationTxState.SendingMac sendToOther(EventType.KEY_VERIFICATION_MAC, macMsg, SasVerificationTxState.MacSent, CancelCode.User) { @@ -176,25 +169,25 @@ internal abstract class SASVerificationTransaction( } // if not wait for it } - override fun acceptToDeviceEvent(senderId: String, event: SendToDeviceObject) { - when (event) { - is KeyVerificationStart -> onVerificationStart(event) - is KeyVerificationAccept -> onVerificationAccept(event) - is KeyVerificationKey -> onKeyVerificationKey(senderId, event) - is KeyVerificationMac -> onKeyVerificationMac(event) - else -> { + override fun acceptVerificationEvent(senderId: String, info: VerificationInfo) { + when (info) { + is VerifInfoStart -> onVerificationStart(info) + is VerifInfoAccept -> onVerificationAccept(info) + is VerifInfoKey -> onKeyVerificationKey(senderId, info) + is VerifInfoMac -> onKeyVerificationMac(info) + else -> { // nop } } } - abstract fun onVerificationStart(startReq: KeyVerificationStart) + abstract fun onVerificationStart(startReq: VerifInfoStart) - abstract fun onVerificationAccept(accept: KeyVerificationAccept) + abstract fun onVerificationAccept(accept: VerifInfoAccept) - abstract fun onKeyVerificationKey(userId: String, vKey: KeyVerificationKey) + abstract fun onKeyVerificationKey(userId: String, vKey: VerifInfoKey) - abstract fun onKeyVerificationMac(vKey: KeyVerificationMac) + abstract fun onKeyVerificationMac(vKey: VerifInfoMac) protected fun verifyMacs() { Timber.v("## SAS verifying macs for id:$transactionId") @@ -245,7 +238,7 @@ internal abstract class SASVerificationTransaction( // if none of the keys could be verified, then error because the app // should be informed about that if (verifiedDevices.isEmpty()) { - Timber.e("Verification: No devices verified") + Timber.e("## SAS Verification: No devices verified") cancel(CancelCode.MismatchedKeys) return } @@ -254,6 +247,7 @@ internal abstract class SASVerificationTransaction( verifiedDevices.forEach { setDeviceVerified(it, otherUserId) } + transport.done(transactionId) state = SasVerificationTxState.Verified } @@ -270,41 +264,15 @@ internal abstract class SASVerificationTransaction( override fun cancel(code: CancelCode) { cancelledReason = code state = SasVerificationTxState.Cancelled - sasVerificationService.cancelTransaction( - transactionId, - otherUserId, - otherDeviceId ?: "", - code) + transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code) } protected fun sendToOther(type: String, - keyToDevice: Any, + keyToDevice: VerificationInfo, nextState: SasVerificationTxState, onErrorReason: CancelCode, onDone: (() -> Unit)?) { - val contentMap = MXUsersDevicesMap() - contentMap.setObject(otherUserId, otherDeviceId, keyToDevice) - - sendToDeviceTask - .configureWith(SendToDeviceTask.Params(type, contentMap, transactionId)) { - this.callback = object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.v("## SAS verification [$transactionId] toDevice type '$type' success.") - if (onDone != null) { - onDone() - } else { - state = nextState - } - } - - override fun onFailure(failure: Throwable) { - Timber.e("## SAS verification [$transactionId] failed to send toDevice in state : $state") - - cancel(onErrorReason) - } - } - } - .executeBy(taskExecutor) + transport.sendToOther(type, keyToDevice, nextState, onErrorReason, onDone) } fun getShortCodeRepresentation(shortAuthenticationStringMode: String): String? { 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 new file mode 100644 index 0000000000..23ebd89f7a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransport.kt @@ -0,0 +1,53 @@ +/* + * 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.crypto.sas.SasVerificationTxState + +/** + * SAS verification can be performed using toDevice events or via DM. + * This class abstracts the concept of transport for SAS + */ +internal interface SasTransport { + + /** + * Sends a message + */ + fun sendToOther(type: String, + verificationInfo: VerificationInfo, + nextState: SasVerificationTxState, + onErrorReason: CancelCode, + onDone: (() -> Unit)?) + + fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) + + fun done(transactionId: String) + /** + * Creates an accept message suitable for this transport + */ + fun createAccept(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List): VerifInfoAccept + + fun createKey(tid: String, + pubKey: String): VerifInfoKey + + fun createMac(tid: String, mac: Map, keys: String): VerifInfoMac +} 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 new file mode 100644 index 0000000000..1173914ba1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportRoomMessage.kt @@ -0,0 +1,129 @@ +/* + * 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.MatrixCallback +import im.vector.matrix.android.api.session.crypto.CryptoService +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.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.configureWith +import timber.log.Timber +import javax.inject.Inject + +internal class SasTransportRoomMessage constructor( + private val roomId: String, + private val cryptoService: CryptoService, +// private val tx: SASVerificationTransaction?, + private val sendVerificationMessageTask: SendVerificationMessageTask, + private val taskExecutor: TaskExecutor +) : SasTransport { + + override fun sendToOther(type: String, verificationInfo: VerificationInfo, nextState: SasVerificationTxState, onErrorReason: CancelCode, onDone: (() -> Unit)?) { + Timber.d("## SAS sending msg type $type") + Timber.v("## SAS sending msg info $verificationInfo") + sendVerificationMessageTask.configureWith( + SendVerificationMessageTask.Params( + type, + roomId, + verificationInfo.toEventContent()!!, + cryptoService + ) + ) { + constraints = TaskConstraints(true) + retryCount = 3 + } + .executeBy(taskExecutor) + } + + override fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) { + Timber.d("## SAS canceling transaction $transactionId for reason $code") + sendVerificationMessageTask.configureWith( + SendVerificationMessageTask.Params( + EventType.KEY_VERIFICATION_CANCEL, + roomId, + MessageVerificationCancelContent.create(transactionId, code).toContent(), + cryptoService + ) + ) { + 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 onFailure(failure: Throwable) { + Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.") + } + } + } + .executeBy(taskExecutor) + } + + override fun done(transactionId: String) { + sendVerificationMessageTask.configureWith( + SendVerificationMessageTask.Params( + EventType.KEY_VERIFICATION_DONE, + roomId, + MessageVerificationDoneContent( + relatesTo = RelationDefaultContent( + RelationType.REFERENCE, + transactionId + ) + ).toContent(), + cryptoService + ) + ) { + constraints = TaskConstraints(true) + retryCount = 3 + } + .executeBy(taskExecutor) + } + + override fun createAccept(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List) + : VerifInfoAccept = MessageVerificationAcceptContent.create(tid, keyAgreementProtocol, hash, commitment, messageAuthenticationCode, shortAuthenticationStrings) + + override fun createKey(tid: String, pubKey: String): VerifInfoKey = MessageVerificationKeyContent.create(tid, pubKey) + + override fun createMac(tid: String, mac: Map, keys: String) = MessageVerificationMacContent.create(tid, mac, keys) +} + +internal class SasTransportRoomMessageFactory @Inject constructor( + private val sendVerificationMessageTask: DefaultSendVerificationMessageTask, + private val taskExecutor: TaskExecutor) { + + fun createTransport(roomId: String, + cryptoService: CryptoService +// tx: SASVerificationTransaction? + ): SasTransportRoomMessage { + return SasTransportRoomMessage(roomId, cryptoService, /*tx,*/ sendVerificationMessageTask, taskExecutor) + } +} 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 new file mode 100644 index 0000000000..4f36a5218e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportToDevice.kt @@ -0,0 +1,115 @@ +/* + * 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.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.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac +import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import timber.log.Timber +import javax.inject.Inject + +internal class SasTransportToDevice( + private var tx: SASVerificationTransaction?, + private var sendToDeviceTask: SendToDeviceTask, + private var taskExecutor: TaskExecutor +) : SasTransport { + + override fun sendToOther(type: String, verificationInfo: VerificationInfo, nextState: SasVerificationTxState, onErrorReason: CancelCode, onDone: (() -> Unit)?) { + Timber.d("## SAS sending msg type $type") + Timber.v("## SAS sending msg info $verificationInfo") + val tx = tx ?: return + val contentMap = MXUsersDevicesMap() + val toSendToDeviceObject = verificationInfo.toSendToDeviceObject() + ?: return Unit.also { tx.cancel() } + + contentMap.setObject(tx.otherUserId, tx.otherDeviceId, toSendToDeviceObject) + + sendToDeviceTask + .configureWith(SendToDeviceTask.Params(type, contentMap, tx.transactionId)) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.v("## SAS verification [$tx.transactionId] toDevice type '$type' success.") + if (onDone != null) { + onDone() + } else { + tx.state = nextState + } + } + + override fun onFailure(failure: Throwable) { + Timber.e("## SAS verification [$tx.transactionId] failed to send toDevice in state : $tx.state") + + tx.cancel(onErrorReason) + } + } + } + .executeBy(taskExecutor) + } + + override fun done(transactionId: String) { + // To device do not do anything here + } + + override fun cancelTransaction(transactionId: String, userId: String, userDevice: 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) + 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) + } + + override fun createAccept(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List) + : VerifInfoAccept = KeyVerificationAccept.create(tid, keyAgreementProtocol, hash, commitment, messageAuthenticationCode, shortAuthenticationStrings) + + override fun createKey(tid: String, pubKey: String): VerifInfoKey = KeyVerificationKey.create(tid, pubKey) + + override fun createMac(tid: String, mac: Map, keys: String) = KeyVerificationMac.create(tid, mac, keys) +} + +internal class SasToDeviceTransportFactory @Inject constructor( + private val sendToDeviceTask: SendToDeviceTask, + private val taskExecutor: TaskExecutor) { + + fun createTransport(tx: SASVerificationTransaction?): SasTransportToDevice { + return SasTransportToDevice(tx, sendToDeviceTask, taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoAccept.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoAccept.kt new file mode 100644 index 0000000000..c0677439f4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoAccept.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.internal.crypto.verification + +internal interface VerifInfoAccept : VerificationInfo { + + val transactionID: String? + + /** + * The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + val keyAgreementProtocol: String? + + /** + * The hash algorithm that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + val hash: String? + + /** + * The message authentication code that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + val messageAuthenticationCode: String? + + /** + * An array of short authentication string methods that Bob’s client (and Bob) understands. Must be a subset of the list proposed by Alice’s device + */ + val shortAuthenticationStrings: List? + + /** + * The hash (encoded as unpadded base64) of the concatenation of the device’s ephemeral public key (QB, encoded as unpadded base64) + * and the canonical JSON representation of the m.key.verification.start message. + */ + var commitment: String? +} + +internal interface AcceptVerifInfoFactory { + + fun create(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List): VerifInfoAccept +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoCancel.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoCancel.kt new file mode 100644 index 0000000000..94c52f61ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoCancel.kt @@ -0,0 +1,30 @@ +/* + * 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 + +interface VerifInfoCancel : VerificationInfo { + + val transactionID: String? + /** + * machine-readable reason for cancelling, see #CancelCode + */ + val code: String? + + /** + * human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given. + */ + val reason: String? +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoKey.kt new file mode 100644 index 0000000000..69ae917938 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoKey.kt @@ -0,0 +1,32 @@ +/* + * 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 + +/** + * Sent by both devices to send their ephemeral Curve25519 public key to the other device. + */ +internal interface VerifInfoKey : VerificationInfo { + + val transactionID: String? + /** + * The device’s ephemeral public key, as an unpadded base64 string + */ + val key: String? +} + +internal interface KeyVerifInfoFactory { + fun create(tid: String, pubKey: String): VerifInfoKey +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoMac.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoMac.kt new file mode 100644 index 0000000000..14da21a398 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoMac.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.verification + +internal interface VerifInfoMac : VerificationInfo { + + val transactionID: String? + + /** + * A map of key ID to the MAC of the key, as an unpadded base64 string, calculated using the MAC key + */ + val mac: Map? + + /** + * The MAC of the comma-separated, sorted list of key IDs given in the mac property, + * as an unpadded base64 string, calculated using the MAC key. + * For example, if the mac property gives MACs for the keys ed25519:ABCDEFG and ed25519:HIJKLMN, then this property will + * give the MAC of the string “ed25519:ABCDEFG,ed25519:HIJKLMN”. + */ + val keys: String? +} + +internal interface VerifInfoMacFactory { + fun create(tid: String, mac: Map, keys: String) : VerifInfoMac +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoStart.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoStart.kt new file mode 100644 index 0000000000..380022def7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoStart.kt @@ -0,0 +1,49 @@ +/* + * 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 + +interface VerifInfoStart : VerificationInfo { + + val method: String? + val fromDevice: String? + + val transactionID: String? + + val keyAgreementProtocols: List? + + /** + * An array of hashes that Alice’s client understands. + * Must include “sha256”. Other methods may be defined in the future. + */ + val hashes: List? + + /** + * An array of message authentication codes that Alice’s client understands. + * Must include “hkdf-hmac-sha256”. + * Other methods may be defined in the future. + */ + val messageAuthenticationCodes: List? + + /** + * An array of short authentication string methods that Alice’s client (and Alice) understands. + * Must include “decimal”. + * This document also describes the “emoji” method. + * Other methods may be defined in the future + */ + val shortAuthenticationStrings: List? + + fun toCanonicalJson(): String? +} 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 new file mode 100644 index 0000000000..5fe5c62edd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfo.kt @@ -0,0 +1,25 @@ +/* + * 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.events.model.Content +import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceObject + +interface VerificationInfo { + fun toEventContent(): Content? = null + fun toSendToDeviceObject(): SendToDeviceObject? = null + fun isValid() : Boolean +} 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 new file mode 100644 index 0000000000..2886d78d8c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt @@ -0,0 +1,113 @@ +/* + * 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 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.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult +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.SessionDatabase +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.task.TaskExecutor +import io.realm.OrderedCollectionChangeSet +import io.realm.RealmConfiguration +import io.realm.RealmResults +import timber.log.Timber +import java.util.* +import javax.inject.Inject + +internal class VerificationMessageLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, + @UserId private val userId: String, + private val cryptoService: CryptoService, + private val sasVerificationService: DefaultSasVerificationService, + private val taskExecutor: TaskExecutor) : + RealmLiveEntityObserver(realmConfiguration) { + + override val query = Monarchy.Query { + EventEntity.types(it, listOf( + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_DONE, + EventType.MESSAGE, + EventType.ENCRYPTED) + ) + } + + override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) { + // TODO do that in a task + // TODO how to ignore when it's an initial sync? + val events = changeSet.insertions + .asSequence() + .mapNotNull { results[it]?.asDomain() } + .filterNot { + // ignore mines ^^ + it.senderId == userId + } + .toList() + + 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") + + // 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()}") + 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) { + // TODO 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. + sasVerificationService.onRoomRequestReceived(event) + } + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransaction.kt index be3f4c7885..d6cc5e3279 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransaction.kt @@ -17,7 +17,6 @@ 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.SasVerificationTransaction -import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceObject /** * Generic interactive key verification transaction @@ -42,7 +41,7 @@ internal abstract class VerificationTransaction( listeners.remove(listener) } - abstract fun acceptToDeviceEvent(senderId: String, event: SendToDeviceObject) + abstract fun acceptVerificationEvent(senderId: String, info: VerificationInfo) abstract fun cancel(code: CancelCode) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt index fa0b9a1f1c..a89e21b04a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt @@ -104,7 +104,7 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure { return Failure.ServerError(matrixError, httpCode) } - } catch (ex: JsonDataException) { + } catch (ex: Exception) { // This is not a MatrixError Timber.w("The error returned by the server is not a MatrixError") } catch (ex: JsonEncodingException) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 0e88894969..b7d121998c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.securestorage.SecureStorageService +import im.vector.matrix.android.internal.crypto.verification.VerificationMessageLiveObserver import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.di.* @@ -164,6 +165,10 @@ internal abstract class SessionModule { @IntoSet abstract fun bindRoomCreateEventLiveObserver(roomCreateEventLiveObserver: RoomCreateEventLiveObserver): LiveEntityObserver + @Binds + @IntoSet + abstract fun bindVerificationEventObserver(verificationMessageLiveObserver: VerificationMessageLiveObserver): LiveEntityObserver + @Binds abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 8fad03b588..acbe385ff6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -157,7 +157,8 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private override fun deleteFailedEcho(localEcho: TimelineEvent) { monarchy.writeAsync { realm -> - TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.let { + TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId + ?: "").findFirst()?.let { it.deleteFromRealm() } EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 0fed1ca6f5..a225146d83 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -286,6 +286,24 @@ internal class LocalEchoEventFactory @Inject constructor( ) } + fun createVerificationRequest(roomId: String, fromDevice: String, to: String, methods: List): Event { + val localID = LocalEcho.createLocalEchoId() + return Event( + roomId = roomId, + originServerTs = dummyOriginServerTs(), + senderId = userId, + eventId = localID, + type = EventType.MESSAGE, + content = MessageVerificationRequestContent( + body = stringProvider.getString(R.string.key_verification_request_fallback_message, userId), + fromDevice = fromDevice, + to = to, + methods = methods + ).toContent(), + unsignedData = UnsignedData(age = null, transactionId = localID) + ) + } + private fun dummyOriginServerTs(): Long { return System.currentTimeMillis() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt index 4c45ba0a4d..d6d924ab46 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt @@ -20,7 +20,6 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.util.awaitTransaction import timber.log.Timber import javax.inject.Inject @@ -28,7 +27,7 @@ internal class LocalEchoUpdater @Inject constructor(private val monarchy: Monarc suspend fun updateSendState(eventId: String, sendState: SendState) { Timber.v("Update local state of $eventId to ${sendState.name}") - monarchy.awaitTransaction { realm -> + monarchy.writeAsync { realm -> val sendingEventEntity = EventEntity.where(realm, eventId).findFirst() if (sendingEventEntity != null) { if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) { diff --git a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml index a22533c6d1..4fe6009268 100644 --- a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml +++ b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml @@ -17,4 +17,7 @@ %1$s withdrew %2$s\'s invitation. Reason: %3$s There is no network connection right now + + %s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys. + \ No newline at end of file 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 442c5f6f96..94d7a39379 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 @@ -207,6 +207,11 @@ interface FragmentModule { @FragmentKey(VectorSettingsNotificationPreferenceFragment::class) fun bindVectorSettingsNotificationPreferenceFragment(fragment: VectorSettingsNotificationPreferenceFragment): Fragment + @Binds + @IntoMap + @FragmentKey(VectorSettingsLabsFragment::class) + fun bindVectorSettingsLabsFragment(fragment: VectorSettingsLabsFragment): Fragment + @Binds @IntoMap @FragmentKey(VectorSettingsPreferencesFragment::class) diff --git a/vector/src/main/java/im/vector/riotx/features/command/Command.kt b/vector/src/main/java/im/vector/riotx/features/command/Command.kt index 8b72ffa4a6..fbc3ddebe5 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/Command.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/Command.kt @@ -38,7 +38,9 @@ enum class Command(val command: String, val parameters: String, @StringRes val d CHANGE_DISPLAY_NAME("/nick", "", R.string.command_description_nick), MARKDOWN("/markdown", "", R.string.command_description_markdown), CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token), - SPOILER("/spoiler", "", R.string.command_description_spoiler); + SHRUG("/shrug", "", R.string.command_description_shrug), + // TODO temporary command + VERIFY_USER("/verify", "", R.string.command_description_spoiler); val length get() = command.length + 1 diff --git a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt index 359f2c1f13..23e30e1d3c 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt @@ -244,6 +244,17 @@ object CommandParser { ParsedCommand.SendSpoiler(message) } + Command.SHRUG.command -> { + val message = textMessage.subSequence(Command.SHRUG.command.length, textMessage.length).trim() + + ParsedCommand.SendShrug(message) + } + + Command.VERIFY_USER.command -> { + val message = textMessage.substring(Command.VERIFY_USER.command.length).trim() + + ParsedCommand.VerifyUser(message) + } else -> { // Unknown command ParsedCommand.ErrorUnknownSlashCommand(slashCommand) diff --git a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt index dd7c0c7e86..b16f68c7b9 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt @@ -46,4 +46,6 @@ sealed class ParsedCommand { class SetMarkdown(val enable: Boolean) : ParsedCommand() object ClearScalarToken : ParsedCommand() class SendSpoiler(val message: String) : ParsedCommand() + class SendShrug(val message: CharSequence) : ParsedCommand() + class VerifyUser(val userId: String) : ParsedCommand() } 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 e7a18753cd..efdfd53234 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 @@ -50,7 +50,6 @@ import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.rx.rx import im.vector.matrix.rx.unwrap -import im.vector.riotx.BuildConfig import im.vector.riotx.R import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.platform.VectorViewModel @@ -377,6 +376,25 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) popDraft() } + is ParsedCommand.SendShrug -> { + val sequence: CharSequence = buildString { + append("¯\\_(ツ)_/¯") + .apply { + if (slashCommandResult.message.isNotEmpty()) { + append(" ") + append(slashCommandResult.message) + } + } + } + room.sendTextMessage(sequence) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) + popDraft() + } + is ParsedCommand.VerifyUser -> { + session.getSasVerificationService().requestKeyVerificationInDMs(slashCommandResult.userId, room.roomId, null) + _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) + popDraft() + } is ParsedCommand.ChangeTopic -> { handleChangeTopicSlashCommand(slashCommandResult) popDraft() 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 5b6dec9900..4c36b55fef 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 @@ -64,6 +64,16 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me encryptedItemFactory.create(event, nextEvent, highlight, callback) } } + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_KEY, + EventType.KEY_VERIFICATION_MAC -> { + // These events are filtered from timeline in normal case + // Only visible in developer mode + defaultItemFactory.create(event, highlight, readMarkerVisible, callback) + } // Unhandled event types (yet) EventType.STATE_ROOM_THIRD_PARTY_INVITE -> defaultItemFactory.create(event, highlight, callback) 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 1cd851f8c8..033ff68433 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 @@ -42,7 +42,13 @@ object TimelineDisplayableEvents { val DEBUG_DISPLAYABLE_TYPES = DISPLAYABLE_TYPES + listOf( EventType.REDACTION, - EventType.REACTION + EventType.REACTION, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_KEY ) } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index dd99488465..ee8c0530d9 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -23,6 +23,7 @@ import android.net.Uri import android.provider.MediaStore import androidx.core.content.edit import androidx.preference.PreferenceManager +import im.vector.riotx.BuildConfig import im.vector.riotx.R import im.vector.riotx.features.homeserver.ServerUrlsRepository import im.vector.riotx.features.themes.ThemeUtils @@ -256,7 +257,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { } fun labAllowedExtendedLogging(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, false) + return defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, BuildConfig.DEBUG) } /** diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt index 37dfd02c43..cd201900fb 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt @@ -17,14 +17,21 @@ package im.vector.riotx.features.settings import im.vector.riotx.R +import im.vector.riotx.core.preference.VectorSwitchPreference +import javax.inject.Inject -class VectorSettingsLabsFragment : VectorSettingsBaseFragment() { +class VectorSettingsLabsFragment @Inject constructor(val vectorPreferences: VectorPreferences) : VectorSettingsBaseFragment() { override var titleRes = R.string.room_settings_labs_pref_title override val preferenceXmlRes = R.xml.vector_settings_labs override fun bindPref() { // Lab + + findPreference(VectorPreferences.SETTINGS_LABS_ALLOW_EXTENDED_LOGS)?.let { + it.isChecked = vectorPreferences.labAllowedExtendedLogging() + } + // val useCryptoPref = findPreference(VectorPreferences.SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY) as SwitchPreference // val cryptoIsEnabledPref = findPreference(VectorPreferences.SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY) diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 2e4d04354b..10ca8e728e 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1695,6 +1695,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Name or ID (#example:matrix.org) Enable swipe to reply in timeline + Enable verification other DM Link copied to clipboard diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index f259a34e44..f8d65fab15 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -12,6 +12,8 @@ "Leave the room" "%1$s made no changes" Sends the given message as a spoiler + Request to verify the given userID + Prepends ¯\\_(ツ)_/¯ to a plain-text message Spoiler Type keywords to find a reaction. diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index e9e5e27198..1a64f75de5 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -45,7 +45,6 @@ android:key="SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY" android:title="@string/labs_swipe_to_reply_in_timeline" /> - Date: Tue, 3 Dec 2019 17:43:49 +0100 Subject: [PATCH 02/70] Code review --- .../MessageVerificationAcceptContent.kt | 10 ++--- .../MessageVerificationCancelContent.kt | 4 +- .../message/MessageVerificationKeyContent.kt | 10 ++--- .../message/MessageVerificationMacContent.kt | 10 ++--- .../MessageVerificationRequestContent.kt | 4 +- .../MessageVerificationStartContent.kt | 6 +-- .../model/rest/KeyVerificationAccept.kt | 10 ++--- .../model/rest/KeyVerificationCancel.kt | 4 +- .../crypto/model/rest/KeyVerificationKey.kt | 8 ++-- .../crypto/model/rest/KeyVerificationMac.kt | 10 ++--- .../model/rest/KeyVerificationRequest.kt | 2 +- .../crypto/model/rest/KeyVerificationStart.kt | 4 +- .../crypto/tasks/RequestVerificationDMTask.kt | 5 +-- ...faultIncomingSASVerificationTransaction.kt | 10 ++--- .../DefaultOutgoingSASVerificationRequest.kt | 8 ++-- .../DefaultSasVerificationService.kt | 45 +++++++++---------- .../SASVerificationTransaction.kt | 26 +++++------ .../crypto/verification/SasTransport.kt | 6 +-- .../verification/SasTransportRoomMessage.kt | 6 +-- .../verification/SasTransportToDevice.kt | 6 +-- .../crypto/verification/VerificationInfo.kt | 2 +- ...nfoAccept.kt => VerificationInfoAccept.kt} | 6 +-- ...nfoCancel.kt => VerificationInfoCancel.kt} | 4 +- ...VerifInfoKey.kt => VerificationInfoKey.kt} | 6 +-- ...VerifInfoMac.kt => VerificationInfoMac.kt} | 6 +-- ...fInfoStart.kt => VerificationInfoStart.kt} | 2 +- .../VerificationMessageLiveObserver.kt | 15 ++++--- .../android/internal/session/SessionModule.kt | 2 +- .../session/room/send/DefaultSendService.kt | 13 +++--- .../room/send/LocalEchoEventFactory.kt | 4 +- .../session/room/send/LocalEchoUpdater.kt | 2 +- .../vector/riotx/features/command/Command.kt | 2 +- .../riotx/features/command/CommandParser.kt | 6 +-- .../home/room/detail/RoomDetailViewModel.kt | 12 +++-- .../settings/VectorSettingsLabsFragment.kt | 4 +- .../src/main/res/xml/vector_settings_labs.xml | 2 +- 36 files changed, 140 insertions(+), 142 deletions(-) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/{VerifInfoAccept.kt => VerificationInfoAccept.kt} (90%) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/{VerifInfoCancel.kt => VerificationInfoCancel.kt} (87%) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/{VerifInfoKey.kt => VerificationInfoKey.kt} (83%) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/{VerifInfoMac.kt => VerificationInfoMac.kt} (90%) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/{VerifInfoStart.kt => VerificationInfoStart.kt} (96%) 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 bda4b9f0ac..66914374df 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 @@ -20,8 +20,8 @@ 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.AcceptVerifInfoFactory -import im.vector.matrix.android.internal.crypto.verification.VerifInfoAccept +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoAcceptFactory +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoAccept import timber.log.Timber @JsonClass(generateAdapter = true) @@ -32,7 +32,7 @@ internal data class MessageVerificationAcceptContent( @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List?, @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?, @Json(name = "commitment") override var commitment: String? = null -) : VerifInfoAccept { +) : VerificationInfoAccept { override val transactionID: String? get() = relatesTo?.eventId @@ -52,14 +52,14 @@ internal data class MessageVerificationAcceptContent( override fun toEventContent() = this.toContent() - companion object : AcceptVerifInfoFactory { + companion object : VerificationInfoAcceptFactory { override fun create(tid: String, keyAgreementProtocol: String, hash: String, commitment: String, messageAuthenticationCode: String, - shortAuthenticationStrings: List): VerifInfoAccept { + shortAuthenticationStrings: List): VerificationInfoAccept { return MessageVerificationAcceptContent( hash, keyAgreementProtocol, 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 08fc3cbdbb..2070845f46 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 @@ -21,7 +21,7 @@ import im.vector.matrix.android.api.session.crypto.sas.CancelCode 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.VerifInfoCancel +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoCancel @JsonClass(generateAdapter = true) internal data class MessageVerificationCancelContent( @@ -29,7 +29,7 @@ internal data class MessageVerificationCancelContent( @Json(name = "reason") override val reason: String? = null, @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? -) : VerifInfoCancel { +) : VerificationInfoCancel { override val transactionID: String? get() = relatesTo?.eventId 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 0b93e3299a..2dacb19871 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 @@ -20,8 +20,8 @@ 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.VerifInfoKey -import im.vector.matrix.android.internal.crypto.verification.KeyVerifInfoFactory +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoKey +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoKeyFactory import timber.log.Timber @JsonClass(generateAdapter = true) @@ -31,7 +31,7 @@ internal data class MessageVerificationKeyContent( */ @Json(name = "key") override val key: String? = null, @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? -) : VerifInfoKey { +) : VerificationInfoKey { override val transactionID: String? get() = relatesTo?.eventId @@ -46,9 +46,9 @@ internal data class MessageVerificationKeyContent( override fun toEventContent() = this.toContent() - companion object : KeyVerifInfoFactory { + companion object : VerificationInfoKeyFactory { - override fun create(tid: String, pubKey: String): VerifInfoKey { + override fun create(tid: String, pubKey: String): VerificationInfoKey { return MessageVerificationKeyContent( pubKey, RelationDefaultContent( 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 92ea4bca52..f08625791f 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 @@ -20,15 +20,15 @@ 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.VerifInfoMac -import im.vector.matrix.android.internal.crypto.verification.VerifInfoMacFactory +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoMac +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoMacFactory @JsonClass(generateAdapter = true) internal data class MessageVerificationMacContent( @Json(name = "mac") override val mac: Map? = null, @Json(name = "keys") override val keys: String? = null, @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? -) : VerifInfoMac { +) : VerificationInfoMac { override val transactionID: String? get() = relatesTo?.eventId @@ -42,8 +42,8 @@ internal data class MessageVerificationMacContent( return true } - companion object : VerifInfoMacFactory { - override fun create(tid: String, mac: Map, keys: String): VerifInfoMac { + companion object : VerificationInfoMacFactory { + override fun create(tid: String, mac: Map, keys: String): VerificationInfoMac { return MessageVerificationMacContent( mac, keys, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.kt index afefa39847..897eb9dbbf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.kt @@ -21,12 +21,12 @@ import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) -class MessageVerificationRequestContent( +internal data class MessageVerificationRequestContent( @Json(name = "msgtype") override val type: String = MessageType.MSGTYPE_VERIFICATION_REQUEST, @Json(name = "body") override val body: String, @Json(name = "from_device") val fromDevice: String, @Json(name = "methods") val methods: List, - @Json(name = "to") val to: String, + @Json(name = "to") val toUserId: String, // @Json(name = "timestamp") val timestamp: Int, @Json(name = "format") val format: String? = null, @Json(name = "formatted_body") val formattedBody: String? = null, 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 f6ec00ffb2..7d77a34e27 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 @@ -22,12 +22,12 @@ 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.model.rest.KeyVerificationStart import im.vector.matrix.android.internal.crypto.verification.SASVerificationTransaction -import im.vector.matrix.android.internal.crypto.verification.VerifInfoStart +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart import im.vector.matrix.android.internal.util.JsonCanonicalizer import timber.log.Timber @JsonClass(generateAdapter = true) -data class MessageVerificationStartContent( +internal data class MessageVerificationStartContent( @Json(name = "from_device") override val fromDevice: String?, @Json(name = "hashes") override val hashes: List?, @Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List?, @@ -35,7 +35,7 @@ data class MessageVerificationStartContent( @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List?, @Json(name = "method") override val method: String?, @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? -) : VerifInfoStart { +) : VerificationInfoStart { override fun toCanonicalJson(): String? { return JsonCanonicalizer.getCanonicalJson(MessageVerificationStartContent::class.java, this) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationAccept.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationAccept.kt index 20d7682cb9..ef30986124 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationAccept.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationAccept.kt @@ -17,8 +17,8 @@ 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.VerifInfoAccept -import im.vector.matrix.android.internal.crypto.verification.AcceptVerifInfoFactory +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoAccept +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoAcceptFactory import timber.log.Timber /** @@ -65,7 +65,7 @@ internal data class KeyVerificationAccept( */ @Json(name = "commitment") override var commitment: String? = null -) : SendToDeviceObject, VerifInfoAccept { +) : SendToDeviceObject, VerificationInfoAccept { override fun isValid(): Boolean { if (transactionID.isNullOrBlank() @@ -82,13 +82,13 @@ internal data class KeyVerificationAccept( override fun toSendToDeviceObject() = this - companion object : AcceptVerifInfoFactory { + companion object : VerificationInfoAcceptFactory { override fun create(tid: String, keyAgreementProtocol: String, hash: String, commitment: String, messageAuthenticationCode: String, - shortAuthenticationStrings: List): VerifInfoAccept { + shortAuthenticationStrings: List): VerificationInfoAccept { return KeyVerificationAccept().apply { this.transactionID = tid this.keyAgreementProtocol = keyAgreementProtocol diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt index 7ffffbbfa1..818bffc942 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.model.rest import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.crypto.sas.CancelCode -import im.vector.matrix.android.internal.crypto.verification.VerifInfoCancel +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoCancel /** * To device event sent by either party to cancel a key verification. @@ -40,7 +40,7 @@ internal data class KeyVerificationCancel( * human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given. */ override var reason: String? = null -) : SendToDeviceObject, VerifInfoCancel { +) : SendToDeviceObject, VerificationInfoCancel { companion object { fun create(tid: String, cancelCode: CancelCode): KeyVerificationCancel { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt index 458c12743f..bf1482ac9f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt @@ -17,8 +17,8 @@ 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.KeyVerifInfoFactory -import im.vector.matrix.android.internal.crypto.verification.VerifInfoKey +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoKeyFactory +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoKey /** * Sent by both devices to send their ephemeral Curve25519 public key to the other device. @@ -35,9 +35,9 @@ internal data class KeyVerificationKey( */ @Json(name = "key") override val key: String? = null -) : SendToDeviceObject, VerifInfoKey { +) : SendToDeviceObject, VerificationInfoKey { - companion object : KeyVerifInfoFactory { + companion object : VerificationInfoKeyFactory { override fun create(tid: String, pubKey: String): KeyVerificationKey { return KeyVerificationKey(tid, pubKey) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationMac.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationMac.kt index d2c147e145..6bb1ae6644 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationMac.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationMac.kt @@ -17,8 +17,8 @@ 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.VerifInfoMac -import im.vector.matrix.android.internal.crypto.verification.VerifInfoMacFactory +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoMac +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoMacFactory /** * Sent by both devices to send the MAC of their device key to the other device. @@ -29,7 +29,7 @@ internal data class KeyVerificationMac( @Json(name = "mac") override val mac: Map? = null, @Json(name = "key") override val keys: String? = null -) : SendToDeviceObject, VerifInfoMac { +) : SendToDeviceObject, VerificationInfoMac { override fun isValid(): Boolean { if (transactionID.isNullOrBlank() || keys.isNullOrBlank() || mac.isNullOrEmpty()) { @@ -40,8 +40,8 @@ internal data class KeyVerificationMac( override fun toSendToDeviceObject(): SendToDeviceObject? = this - companion object : VerifInfoMacFactory { - override fun create(tid: String, mac: Map, keys: String): VerifInfoMac { + companion object : VerificationInfoMacFactory { + override fun create(tid: String, mac: Map, keys: String): VerificationInfoMac { return KeyVerificationMac(tid, mac, keys) } } 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 14954a17cd..51eed84412 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 @@ -23,7 +23,7 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationInfo * Requests a key verification with another user's devices. */ @JsonClass(generateAdapter = true) -data class KeyVerificationRequest( +internal data class KeyVerificationRequest( @Json(name = "from_device") val fromDevice: String, 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 f7cc10a12b..47c2a62d92 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 @@ -19,7 +19,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.crypto.sas.SasMode import im.vector.matrix.android.internal.crypto.verification.SASVerificationTransaction -import im.vector.matrix.android.internal.crypto.verification.VerifInfoStart +import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart import im.vector.matrix.android.internal.util.JsonCanonicalizer import timber.log.Timber @@ -27,7 +27,7 @@ import timber.log.Timber * Sent by Alice to initiate an interactive key verification. */ @JsonClass(generateAdapter = true) -class KeyVerificationStart : SendToDeviceObject, VerifInfoStart { +class KeyVerificationStart : SendToDeviceObject, VerificationInfoStart { override fun toCanonicalJson(): String? { return JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, this) 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 index 57d225a193..ae8a40f296 100644 --- 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 @@ -68,9 +68,8 @@ internal class DefaultRequestVerificationDMTask @Inject constructor( } private suspend fun createRequestEvent(params: RequestVerificationDMTask.Params): Event { - val event = localEchoEventFactory.createVerificationRequest(params.roomId, params.from, params.to, params.methods).also { - localEchoEventFactory.saveLocalEcho(monarchy, it) - } + val event = localEchoEventFactory.createVerificationRequest(params.roomId, params.from, params.to, params.methods) + .also { localEchoEventFactory.saveLocalEcho(monarchy, it) } if (params.cryptoService.isRoomEncrypted(params.roomId)) { try { return encryptEventTask.execute(EncryptEventTask.Params( 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 aac2b49f57..5eff26a5bb 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 @@ -66,7 +66,7 @@ internal class DefaultIncomingSASVerificationTransaction( } } - override fun onVerificationStart(startReq: VerifInfoStart) { + override fun onVerificationStart(startReq: VerificationInfoStart) { Timber.v("## SAS I: received verification request from state $state") if (state != SasVerificationTxState.None) { Timber.e("## SAS I: received verification request from invalid state") @@ -126,7 +126,7 @@ internal class DefaultIncomingSASVerificationTransaction( } } - private fun doAccept(accept: VerifInfoAccept) { + private fun doAccept(accept: VerificationInfoAccept) { this.accepted = accept Timber.v("## SAS incoming accept request id:$transactionId") @@ -144,12 +144,12 @@ internal class DefaultIncomingSASVerificationTransaction( } } - override fun onVerificationAccept(accept: VerifInfoAccept) { + override fun onVerificationAccept(accept: VerificationInfoAccept) { Timber.v("## SAS invalid message for incoming request id:$transactionId") cancel(CancelCode.UnexpectedMessage) } - override fun onKeyVerificationKey(userId: String, vKey: VerifInfoKey) { + override fun onKeyVerificationKey(userId: String, vKey: VerificationInfoKey) { Timber.v("## SAS received key for request id:$transactionId") if (state != SasVerificationTxState.SendingAccept && state != SasVerificationTxState.Accepted) { Timber.e("## SAS received key from invalid state $state") @@ -202,7 +202,7 @@ internal class DefaultIncomingSASVerificationTransaction( state = SasVerificationTxState.ShortCodeReady } - override fun onKeyVerificationMac(vKey: VerifInfoMac) { + override fun onKeyVerificationMac(vKey: VerificationInfoMac) { Timber.v("## SAS I: received mac for request id:$transactionId") // Check for state? if (state != SasVerificationTxState.SendingKey diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASVerificationRequest.kt index 4362e897c8..5827ecf4b8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASVerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASVerificationRequest.kt @@ -66,7 +66,7 @@ internal class DefaultOutgoingSASVerificationRequest( } } - override fun onVerificationStart(startReq: VerifInfoStart) { + override fun onVerificationStart(startReq: VerificationInfoStart) { Timber.e("## SAS O: onVerificationStart - unexpected id:$transactionId") cancel(CancelCode.UnexpectedMessage) } @@ -122,7 +122,7 @@ internal class DefaultOutgoingSASVerificationRequest( // ) // } - override fun onVerificationAccept(accept: VerifInfoAccept) { + override fun onVerificationAccept(accept: VerificationInfoAccept) { Timber.v("## SAS O: onVerificationAccept id:$transactionId") if (state != SasVerificationTxState.Started) { Timber.e("## SAS O: received accept request from invalid state $state") @@ -159,7 +159,7 @@ internal class DefaultOutgoingSASVerificationRequest( } } - override fun onKeyVerificationKey(userId: String, vKey: VerifInfoKey) { + override fun onKeyVerificationKey(userId: String, vKey: VerificationInfoKey) { Timber.v("## SAS O: onKeyVerificationKey id:$transactionId") if (state != SasVerificationTxState.SendingKey && state != SasVerificationTxState.KeySent) { Timber.e("## received key from invalid state $state") @@ -201,7 +201,7 @@ internal class DefaultOutgoingSASVerificationRequest( } } - override fun onKeyVerificationMac(vKey: VerifInfoMac) { + override fun onKeyVerificationMac(vKey: VerificationInfoMac) { Timber.v("## SAS O: onKeyVerificationMac id:$transactionId") if (state != SasVerificationTxState.OnKeyReceived && state != SasVerificationTxState.ShortCodeReady 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 6552dca7ab..1d15a996dd 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 @@ -39,7 +39,6 @@ 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.crypto.tasks.RequestVerificationDMTask -import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask 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 @@ -56,18 +55,18 @@ import kotlin.collections.HashMap import kotlin.collections.set @SessionScope -internal class DefaultSasVerificationService @Inject constructor(private val credentials: Credentials, - private val cryptoStore: IMXCryptoStore, - private val myDeviceInfoHolder: Lazy, - private val deviceListManager: DeviceListManager, - private val setDeviceVerificationAction: SetDeviceVerificationAction, - private val sendToDeviceTask: SendToDeviceTask, - private val requestVerificationDMTask: DefaultRequestVerificationDMTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val sasTransportRoomMessageFactory: SasTransportRoomMessageFactory, - private val sasToDeviceTransportFactory: SasToDeviceTransportFactory, - private val taskExecutor: TaskExecutor) - : VerificationTransaction.Listener, SasVerificationService { +internal class DefaultSasVerificationService @Inject constructor( + private val credentials: Credentials, + private val cryptoStore: IMXCryptoStore, + 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 +) : VerificationTransaction.Listener, SasVerificationService { private val uiHandler = Handler(Looper.getMainLooper()) @@ -217,7 +216,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre return } - handleStart(otherUserId, startReq as VerifInfoStart) { + handleStart(otherUserId, startReq as VerificationInfoStart) { it.transport = sasTransportRoomMessageFactory.createTransport(event.roomId ?: "", cryptoService) }?.let { @@ -246,7 +245,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre // startReq.fromDevice ?: event.getSenderKey()!!, // CancelCode.UnknownMethod // ) - sasToDeviceTransportFactory.createTransport(null).cancelTransaction( + sasTransportToDeviceFactory.createTransport(null).cancelTransaction( startReq.transactionID ?: "", otherUserId!!, startReq.fromDevice ?: event.getSenderKey()!!, @@ -257,9 +256,9 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre } // Download device keys prior to everything handleStart(otherUserId, startReq) { - it.transport = sasToDeviceTransportFactory.createTransport(it) + it.transport = sasTransportToDeviceFactory.createTransport(it) }?.let { - sasToDeviceTransportFactory.createTransport(null).cancelTransaction( + sasTransportToDeviceFactory.createTransport(null).cancelTransaction( startReq.transactionID ?: "", otherUserId!!, startReq.fromDevice ?: event.getSenderKey()!!, @@ -268,7 +267,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre } } - private suspend fun handleStart(otherUserId: String?, startReq: VerifInfoStart, txConfigure: (SASVerificationTransaction) -> Unit): CancelCode? { + private suspend fun handleStart(otherUserId: String?, startReq: VerificationInfoStart, txConfigure: (SASVerificationTransaction) -> Unit): CancelCode? { Timber.d("## SAS onStartRequestReceived ${startReq.transactionID!!}") if (checkKeysAreDownloaded(otherUserId!!, startReq) != null) { Timber.v("## SAS onStartRequestReceived $startReq") @@ -318,7 +317,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre } private suspend fun checkKeysAreDownloaded(otherUserId: String, - startReq: VerifInfoStart): MXUsersDevicesMap? { + startReq: VerificationInfoStart): MXUsersDevicesMap? { return try { val keys = deviceListManager.downloadKeys(listOf(otherUserId), true) val deviceIds = keys.getUserDeviceIds(otherUserId) ?: return null @@ -357,7 +356,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre handleOnCancel(otherUserId, cancelReq) } - private fun handleOnCancel(otherUserId: String, cancelReq: VerifInfoCancel) { + 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) { @@ -387,7 +386,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre handleAccept(acceptReq, event.senderId!!) } - private fun handleAccept(acceptReq: VerifInfoAccept, senderId: String) { + private fun handleAccept(acceptReq: VerificationInfoAccept, senderId: String) { if (!acceptReq.isValid()) { // ignore Timber.e("## SAS Received invalid accept request") @@ -433,7 +432,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre handleKeyReceived(event, keyReq) } - private fun handleKeyReceived(event: Event, keyReq: VerifInfoKey) { + private fun handleKeyReceived(event: Event, keyReq: VerificationInfoKey) { Timber.d("## SAS Received Key from ${event.senderId} with info $keyReq") val otherUserId = event.senderId!! val existing = getExistingTransaction(otherUserId, keyReq.transactionID!!) @@ -474,7 +473,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre handleMacReceived(event.senderId, macReq) } - private fun handleMacReceived(senderId: String, macReq: VerifInfoMac) { + private fun handleMacReceived(senderId: String, macReq: VerificationInfoMac) { Timber.v("## SAS Received $macReq") val existing = getExistingTransaction(senderId, macReq.transactionID!!) if (existing == 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 443443afad..31d6fd4b5c 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 @@ -88,13 +88,13 @@ internal abstract class SASVerificationTransaction( private var olmSas: OlmSAS? = null - var startReq: VerifInfoStart? = null - var accepted: VerifInfoAccept? = null + var startReq: VerificationInfoStart? = null + var accepted: VerificationInfoAccept? = null var otherKey: String? = null var shortCodeBytes: ByteArray? = null - var myMac: VerifInfoMac? = null - var theirMac: VerifInfoMac? = null + var myMac: VerificationInfoMac? = null + var theirMac: VerificationInfoMac? = null fun getSAS(): OlmSAS { if (olmSas == null) olmSas = OlmSAS() @@ -171,23 +171,23 @@ internal abstract class SASVerificationTransaction( override fun acceptVerificationEvent(senderId: String, info: VerificationInfo) { when (info) { - is VerifInfoStart -> onVerificationStart(info) - is VerifInfoAccept -> onVerificationAccept(info) - is VerifInfoKey -> onKeyVerificationKey(senderId, info) - is VerifInfoMac -> onKeyVerificationMac(info) - else -> { + is VerificationInfoStart -> onVerificationStart(info) + is VerificationInfoAccept -> onVerificationAccept(info) + is VerificationInfoKey -> onKeyVerificationKey(senderId, info) + is VerificationInfoMac -> onKeyVerificationMac(info) + else -> { // nop } } } - abstract fun onVerificationStart(startReq: VerifInfoStart) + abstract fun onVerificationStart(startReq: VerificationInfoStart) - abstract fun onVerificationAccept(accept: VerifInfoAccept) + abstract fun onVerificationAccept(accept: VerificationInfoAccept) - abstract fun onKeyVerificationKey(userId: String, vKey: VerifInfoKey) + abstract fun onKeyVerificationKey(userId: String, vKey: VerificationInfoKey) - abstract fun onKeyVerificationMac(vKey: VerifInfoMac) + abstract fun onKeyVerificationMac(vKey: VerificationInfoMac) protected fun verifyMacs() { Timber.v("## SAS verifying macs for id:$transactionId") 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 23ebd89f7a..31a89335b7 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 @@ -44,10 +44,10 @@ internal interface SasTransport { hash: String, commitment: String, messageAuthenticationCode: String, - shortAuthenticationStrings: List): VerifInfoAccept + shortAuthenticationStrings: List): VerificationInfoAccept fun createKey(tid: String, - pubKey: String): VerifInfoKey + pubKey: String): VerificationInfoKey - fun createMac(tid: String, mac: Map, keys: String): VerifInfoMac + fun createMac(tid: String, mac: Map, keys: String): VerificationInfoMac } 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 1173914ba1..fda411af39 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 @@ -33,7 +33,7 @@ import im.vector.matrix.android.internal.task.configureWith import timber.log.Timber import javax.inject.Inject -internal class SasTransportRoomMessage constructor( +internal class SasTransportRoomMessage( private val roomId: String, private val cryptoService: CryptoService, // private val tx: SASVerificationTransaction?, @@ -109,9 +109,9 @@ internal class SasTransportRoomMessage constructor( commitment: String, messageAuthenticationCode: String, shortAuthenticationStrings: List) - : VerifInfoAccept = MessageVerificationAcceptContent.create(tid, keyAgreementProtocol, hash, commitment, messageAuthenticationCode, shortAuthenticationStrings) + : VerificationInfoAccept = MessageVerificationAcceptContent.create(tid, keyAgreementProtocol, hash, commitment, messageAuthenticationCode, shortAuthenticationStrings) - override fun createKey(tid: String, pubKey: String): VerifInfoKey = MessageVerificationKeyContent.create(tid, pubKey) + override fun createKey(tid: String, pubKey: String): VerificationInfoKey = MessageVerificationKeyContent.create(tid, pubKey) override fun createMac(tid: String, mac: Map, keys: String) = MessageVerificationMacContent.create(tid, mac, keys) } 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 4f36a5218e..0e1459a920 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 @@ -98,14 +98,14 @@ internal class SasTransportToDevice( commitment: String, messageAuthenticationCode: String, shortAuthenticationStrings: List) - : VerifInfoAccept = KeyVerificationAccept.create(tid, keyAgreementProtocol, hash, commitment, messageAuthenticationCode, shortAuthenticationStrings) + : VerificationInfoAccept = KeyVerificationAccept.create(tid, keyAgreementProtocol, hash, commitment, messageAuthenticationCode, shortAuthenticationStrings) - override fun createKey(tid: String, pubKey: String): VerifInfoKey = KeyVerificationKey.create(tid, pubKey) + override fun createKey(tid: String, pubKey: String): VerificationInfoKey = KeyVerificationKey.create(tid, pubKey) override fun createMac(tid: String, mac: Map, keys: String) = KeyVerificationMac.create(tid, mac, keys) } -internal class SasToDeviceTransportFactory @Inject constructor( +internal class SasTransportToDeviceFactory @Inject constructor( private val sendToDeviceTask: SendToDeviceTask, private val taskExecutor: TaskExecutor) { 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 5fe5c62edd..44a65aa926 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,7 +18,7 @@ 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 -interface VerificationInfo { +internal interface VerificationInfo { fun toEventContent(): Content? = null fun toSendToDeviceObject(): SendToDeviceObject? = null fun isValid() : Boolean diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoAccept.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoAccept.kt similarity index 90% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoAccept.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoAccept.kt index c0677439f4..7f639c3bc7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoAccept.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoAccept.kt @@ -15,7 +15,7 @@ */ package im.vector.matrix.android.internal.crypto.verification -internal interface VerifInfoAccept : VerificationInfo { +internal interface VerificationInfoAccept : VerificationInfo { val transactionID: String? @@ -46,12 +46,12 @@ internal interface VerifInfoAccept : VerificationInfo { var commitment: String? } -internal interface AcceptVerifInfoFactory { +internal interface VerificationInfoAcceptFactory { fun create(tid: String, keyAgreementProtocol: String, hash: String, commitment: String, messageAuthenticationCode: String, - shortAuthenticationStrings: List): VerifInfoAccept + shortAuthenticationStrings: List): VerificationInfoAccept } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoCancel.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoCancel.kt similarity index 87% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoCancel.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoCancel.kt index 94c52f61ea..2970358a15 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoCancel.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoCancel.kt @@ -15,11 +15,11 @@ */ package im.vector.matrix.android.internal.crypto.verification -interface VerifInfoCancel : VerificationInfo { +internal interface VerificationInfoCancel : VerificationInfo { val transactionID: String? /** - * machine-readable reason for cancelling, see #CancelCode + * machine-readable reason for cancelling, see [CancelCode] */ val code: String? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoKey.kt similarity index 83% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoKey.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoKey.kt index 69ae917938..e5deb63b56 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoKey.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoKey.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.verification /** * Sent by both devices to send their ephemeral Curve25519 public key to the other device. */ -internal interface VerifInfoKey : VerificationInfo { +internal interface VerificationInfoKey : VerificationInfo { val transactionID: String? /** @@ -27,6 +27,6 @@ internal interface VerifInfoKey : VerificationInfo { val key: String? } -internal interface KeyVerifInfoFactory { - fun create(tid: String, pubKey: String): VerifInfoKey +internal interface VerificationInfoKeyFactory { + fun create(tid: String, pubKey: String): VerificationInfoKey } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoMac.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoMac.kt similarity index 90% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoMac.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoMac.kt index 14da21a398..01fc865250 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoMac.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoMac.kt @@ -15,7 +15,7 @@ */ package im.vector.matrix.android.internal.crypto.verification -internal interface VerifInfoMac : VerificationInfo { +internal interface VerificationInfoMac : VerificationInfo { val transactionID: String? @@ -33,6 +33,6 @@ internal interface VerifInfoMac : VerificationInfo { val keys: String? } -internal interface VerifInfoMacFactory { - fun create(tid: String, mac: Map, keys: String) : VerifInfoMac +internal interface VerificationInfoMacFactory { + fun create(tid: String, mac: Map, keys: String) : VerificationInfoMac } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoStart.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt similarity index 96% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoStart.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt index 380022def7..f1cbd3f9f1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerifInfoStart.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoStart.kt @@ -15,7 +15,7 @@ */ package im.vector.matrix.android.internal.crypto.verification -interface VerifInfoStart : VerificationInfo { +internal interface VerificationInfoStart : VerificationInfo { val method: String? val fromDevice: String? 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 2886d78d8c..f75a11cdcb 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 @@ -37,12 +37,13 @@ import timber.log.Timber import java.util.* import javax.inject.Inject -internal class VerificationMessageLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, - @UserId private val userId: String, - private val cryptoService: CryptoService, - private val sasVerificationService: DefaultSasVerificationService, - private val taskExecutor: TaskExecutor) : - RealmLiveEntityObserver(realmConfiguration) { +internal class VerificationMessageLiveObserver @Inject constructor( + @SessionDatabase realmConfiguration: RealmConfiguration, + @UserId private val userId: String, + private val cryptoService: CryptoService, + private val sasVerificationService: DefaultSasVerificationService, + private val taskExecutor: TaskExecutor +) : RealmLiveEntityObserver(realmConfiguration) { override val query = Monarchy.Query { EventEntity.types(it, listOf( @@ -70,7 +71,7 @@ internal class VerificationMessageLiveObserver @Inject constructor(@SessionDatab .toList() events.forEach { event -> - Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") + 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") // decrypt if needed? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index b7d121998c..883fd37745 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -167,7 +167,7 @@ internal abstract class SessionModule { @Binds @IntoSet - abstract fun bindVerificationEventObserver(verificationMessageLiveObserver: VerificationMessageLiveObserver): LiveEntityObserver + abstract fun bindVerificationMessageLiveObserver(verificationMessageLiveObserver: VerificationMessageLiveObserver): LiveEntityObserver @Binds abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index acbe385ff6..0e6c93590b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -157,13 +157,12 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private override fun deleteFailedEcho(localEcho: TimelineEvent) { monarchy.writeAsync { realm -> - TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId - ?: "").findFirst()?.let { - it.deleteFromRealm() - } - EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let { - it.deleteFromRealm() - } + TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "") + .findFirst() + ?.let { it.deleteFromRealm() } + EventEntity.where(realm, eventId = localEcho.root.eventId ?: "") + .findFirst() + ?.let { it.deleteFromRealm() } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index a225146d83..45172fb213 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -286,7 +286,7 @@ internal class LocalEchoEventFactory @Inject constructor( ) } - fun createVerificationRequest(roomId: String, fromDevice: String, to: String, methods: List): Event { + fun createVerificationRequest(roomId: String, fromDevice: String, toUserId: String, methods: List): Event { val localID = LocalEcho.createLocalEchoId() return Event( roomId = roomId, @@ -297,7 +297,7 @@ internal class LocalEchoEventFactory @Inject constructor( content = MessageVerificationRequestContent( body = stringProvider.getString(R.string.key_verification_request_fallback_message, userId), fromDevice = fromDevice, - to = to, + toUserId = toUserId, methods = methods ).toContent(), unsignedData = UnsignedData(age = null, transactionId = localID) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt index d6d924ab46..60d1a217a7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt @@ -25,7 +25,7 @@ import javax.inject.Inject internal class LocalEchoUpdater @Inject constructor(private val monarchy: Monarchy) { - suspend fun updateSendState(eventId: String, sendState: SendState) { + fun updateSendState(eventId: String, sendState: SendState) { Timber.v("Update local state of $eventId to ${sendState.name}") monarchy.writeAsync { realm -> val sendingEventEntity = EventEntity.where(realm, eventId).findFirst() diff --git a/vector/src/main/java/im/vector/riotx/features/command/Command.kt b/vector/src/main/java/im/vector/riotx/features/command/Command.kt index fbc3ddebe5..3f636775c0 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/Command.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/Command.kt @@ -40,7 +40,7 @@ enum class Command(val command: String, val parameters: String, @StringRes val d CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token), SHRUG("/shrug", "", R.string.command_description_shrug), // TODO temporary command - VERIFY_USER("/verify", "", R.string.command_description_spoiler); + VERIFY_USER("/verify", "", R.string.command_description_verify); val length get() = command.length + 1 diff --git a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt index 23e30e1d3c..dcdb7ad8a2 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt @@ -244,13 +244,13 @@ object CommandParser { ParsedCommand.SendSpoiler(message) } - Command.SHRUG.command -> { - val message = textMessage.subSequence(Command.SHRUG.command.length, textMessage.length).trim() + Command.SHRUG.command -> { + val message = textMessage.substring(Command.SHRUG.command.length).trim() ParsedCommand.SendShrug(message) } - Command.VERIFY_USER.command -> { + Command.VERIFY_USER.command -> { val message = textMessage.substring(Command.VERIFY_USER.command.length).trim() ParsedCommand.VerifyUser(message) 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 efdfd53234..c1d3f4ce4a 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 @@ -377,14 +377,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro popDraft() } is ParsedCommand.SendShrug -> { - val sequence: CharSequence = buildString { + val sequence = buildString { append("¯\\_(ツ)_/¯") - .apply { - if (slashCommandResult.message.isNotEmpty()) { - append(" ") - append(slashCommandResult.message) - } - } + if (slashCommandResult.message.isNotEmpty()) { + append(" ") + append(slashCommandResult.message) + } } room.sendTextMessage(sequence) _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt index cd201900fb..8b1a2dba31 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsLabsFragment.kt @@ -20,7 +20,9 @@ import im.vector.riotx.R import im.vector.riotx.core.preference.VectorSwitchPreference import javax.inject.Inject -class VectorSettingsLabsFragment @Inject constructor(val vectorPreferences: VectorPreferences) : VectorSettingsBaseFragment() { +class VectorSettingsLabsFragment @Inject constructor( + private val vectorPreferences: VectorPreferences +) : VectorSettingsBaseFragment() { override var titleRes = R.string.room_settings_labs_pref_title override val preferenceXmlRes = R.xml.vector_settings_labs diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index 1a64f75de5..530c207749 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -46,7 +46,7 @@ android:title="@string/labs_swipe_to_reply_in_timeline" /> From 36c5566b070281113b0ecf7170540fbc6f4f0ab9 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 3 Dec 2019 18:22:44 +0100 Subject: [PATCH 03/70] cleaning --- .../message/MessageVerificationStartContent.kt | 13 +++++++++---- .../verification/SasTransportRoomMessage.kt | 15 +++++++++++++-- .../crypto/verification/SasTransportToDevice.kt | 14 ++++++++++++-- 3 files changed, 34 insertions(+), 8 deletions(-) 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 7d77a34e27..f928e21a82 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 @@ -38,7 +38,7 @@ internal data class MessageVerificationStartContent( ) : VerificationInfoStart { override fun toCanonicalJson(): String? { - return JsonCanonicalizer.getCanonicalJson(MessageVerificationStartContent::class.java, this) + return JsonCanonicalizer.getCanonicalJson(MessageVerificationStartContent::class.java, this) } override val transactionID: String? @@ -46,9 +46,14 @@ internal data class MessageVerificationStartContent( override fun isValid(): Boolean { if ( - (transactionID.isNullOrBlank() || fromDevice.isNullOrBlank() || method != KeyVerificationStart.VERIF_METHOD_SAS || keyAgreementProtocols.isNullOrEmpty() || hashes.isNullOrEmpty()) + (transactionID.isNullOrBlank() + || fromDevice.isNullOrBlank() + || method != KeyVerificationStart.VERIF_METHOD_SAS + || keyAgreementProtocols.isNullOrEmpty() + || hashes.isNullOrEmpty()) || !hashes.contains("sha256") || messageAuthenticationCodes.isNullOrEmpty() - || (!messageAuthenticationCodes.contains(SASVerificationTransaction.SAS_MAC_SHA256) && !messageAuthenticationCodes.contains(SASVerificationTransaction.SAS_MAC_SHA256_LONGKDF)) + || (!messageAuthenticationCodes.contains(SASVerificationTransaction.SAS_MAC_SHA256) + && !messageAuthenticationCodes.contains(SASVerificationTransaction.SAS_MAC_SHA256_LONGKDF)) || shortAuthenticationStrings.isNullOrEmpty() || !shortAuthenticationStrings.contains(SasMode.DECIMAL)) { Timber.e("## received invalid verification request") @@ -57,5 +62,5 @@ internal data class MessageVerificationStartContent( return true } - override fun toEventContent() = this.toContent() + override fun toEventContent() = this.toContent() } 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 fda411af39..87bb4bc805 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 @@ -41,7 +41,11 @@ internal class SasTransportRoomMessage( private val taskExecutor: TaskExecutor ) : SasTransport { - override fun sendToOther(type: String, verificationInfo: VerificationInfo, nextState: SasVerificationTxState, onErrorReason: CancelCode, onDone: (() -> Unit)?) { + override fun sendToOther(type: String, + verificationInfo: VerificationInfo, + nextState: SasVerificationTxState, + onErrorReason: CancelCode, + onDone: (() -> Unit)?) { Timber.d("## SAS sending msg type $type") Timber.v("## SAS sending msg info $verificationInfo") sendVerificationMessageTask.configureWith( @@ -109,7 +113,14 @@ internal class SasTransportRoomMessage( commitment: String, messageAuthenticationCode: String, shortAuthenticationStrings: List) - : VerificationInfoAccept = MessageVerificationAcceptContent.create(tid, keyAgreementProtocol, hash, commitment, messageAuthenticationCode, shortAuthenticationStrings) + : VerificationInfoAccept = MessageVerificationAcceptContent.create( + tid, + keyAgreementProtocol, + hash, + commitment, + messageAuthenticationCode, + shortAuthenticationStrings + ) override fun createKey(tid: String, pubKey: String): VerificationInfoKey = MessageVerificationKeyContent.create(tid, pubKey) 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 0e1459a920..96ae47895e 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 @@ -36,7 +36,11 @@ internal class SasTransportToDevice( private var taskExecutor: TaskExecutor ) : SasTransport { - override fun sendToOther(type: String, verificationInfo: VerificationInfo, nextState: SasVerificationTxState, onErrorReason: CancelCode, onDone: (() -> Unit)?) { + override fun sendToOther(type: String, + verificationInfo: VerificationInfo, + nextState: SasVerificationTxState, + onErrorReason: CancelCode, + onDone: (() -> Unit)?) { Timber.d("## SAS sending msg type $type") Timber.v("## SAS sending msg info $verificationInfo") val tx = tx ?: return @@ -98,7 +102,13 @@ internal class SasTransportToDevice( commitment: String, messageAuthenticationCode: String, shortAuthenticationStrings: List) - : VerificationInfoAccept = KeyVerificationAccept.create(tid, keyAgreementProtocol, hash, commitment, messageAuthenticationCode, shortAuthenticationStrings) + : VerificationInfoAccept = KeyVerificationAccept.create( + tid, + keyAgreementProtocol, + hash, + commitment, + messageAuthenticationCode, + shortAuthenticationStrings) override fun createKey(tid: String, pubKey: String): VerificationInfoKey = KeyVerificationKey.create(tid, pubKey) From bbd9738452cd6a049fab8566656ab4e008a9be2d Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 3 Dec 2019 18:24:18 +0100 Subject: [PATCH 04/70] Simple strategy to Ignore old verification messages --- .../VerificationMessageLiveObserver.kt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) 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 f75a11cdcb..9a9cf9a420 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 @@ -70,12 +70,23 @@ internal class VerificationMessageLiveObserver @Inject constructor( } .toList() + // TODO use age also, ignore initial sync or back pagination? + val now = System.currentTimeMillis() + val tooInThePast = now - (10 * 60 * 1000 * 1000) + val tooInTheFuture = System.currentTimeMillis() + (5 * 60 * 1000 * 1000) + 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 eventOrigin = event.originServerTs ?: -1 + if (eventOrigin < tooInThePast || eventOrigin > tooInTheFuture) { + Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is out of time ^^") + 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 @@ -103,8 +114,6 @@ internal class VerificationMessageLiveObserver @Inject constructor( } EventType.MESSAGE -> { if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel()?.type) { - // TODO 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. sasVerificationService.onRoomRequestReceived(event) } } From 2aa9c3ea22d65ff669e0778cd990f8ca62c1ea83 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 4 Dec 2019 18:22:01 +0100 Subject: [PATCH 05/70] Fix / Use transport to start verification --- .../crypto/sas/SasVerificationService.kt | 7 +++++ .../MessageVerificationRequestContent.kt | 2 +- .../DefaultOutgoingSASVerificationRequest.kt | 17 ++++++------ .../DefaultSasVerificationService.kt | 27 +++++++++++++++++-- .../crypto/verification/SasTransport.kt | 10 ++++++- .../verification/SasTransportRoomMessage.kt | 21 +++++++++++++++ .../verification/SasTransportToDevice.kt | 23 +++++++++++++--- .../android/internal/di/MoshiProvider.kt | 1 + 8 files changed, 92 insertions(+), 16 deletions(-) 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 902baae06f..3c3c43dbd4 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 @@ -52,6 +52,13 @@ interface SasVerificationService { fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback?) + fun beginKeyVerificationInDMs(method: String, + transactionId: String, + roomId: String, + otherUserId: String, + otherDeviceId: String, + callback: MatrixCallback?): String? + // fun transactionUpdated(tx: SasVerificationTransaction) interface SasVerificationListener { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.kt index 897eb9dbbf..1f6c8158fe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationRequestContent.kt @@ -21,7 +21,7 @@ import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) -internal data class MessageVerificationRequestContent( +data class MessageVerificationRequestContent( @Json(name = "msgtype") override val type: String = MessageType.MSGTYPE_VERIFICATION_REQUEST, @Json(name = "body") override val body: String, @Json(name = "from_device") val fromDevice: String, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASVerificationRequest.kt index 5827ecf4b8..5eead70c17 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASVerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASVerificationRequest.kt @@ -78,14 +78,15 @@ internal class DefaultOutgoingSASVerificationRequest( throw IllegalStateException("Interactive Key verification already started") } - val startMessage = KeyVerificationStart() - startMessage.fromDevice = credentials.deviceId - startMessage.method = KeyVerificationStart.VERIF_METHOD_SAS - startMessage.transactionID = transactionId - startMessage.keyAgreementProtocols = KNOWN_AGREEMENT_PROTOCOLS - startMessage.hashes = KNOWN_HASHES - startMessage.messageAuthenticationCodes = KNOWN_MACS - startMessage.shortAuthenticationStrings = KNOWN_SHORT_CODES + val startMessage = transport.createStart( + credentials.deviceId ?: "", + KeyVerificationStart.VERIF_METHOD_SAS, + transactionId, + KNOWN_AGREEMENT_PROTOCOLS, + KNOWN_HASHES, + KNOWN_MACS, + KNOWN_SHORT_CODES + ) startReq = startMessage state = SasVerificationTxState.SendingStart 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 1d15a996dd..2546f1f492 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 @@ -348,7 +348,7 @@ internal class DefaultSasVerificationService @Inject constructor( if (!cancelReq.isValid()) { // ignore - Timber.e("## SAS Received invalid accept request") + Timber.e("## SAS Received invalid cancel request") return } val otherUserId = event.senderId!! @@ -477,7 +477,7 @@ internal class DefaultSasVerificationService @Inject constructor( Timber.v("## SAS Received $macReq") val existing = getExistingTransaction(senderId, macReq.transactionID!!) if (existing == null) { - Timber.e("## SAS Received invalid accept request") + Timber.e("## SAS Received invalid Mac request") return } if (existing is SASVerificationTransaction) { @@ -532,6 +532,7 @@ internal class DefaultSasVerificationService @Inject constructor( txID, userId, deviceID) + tx.transport = sasTransportToDeviceFactory.createTransport(tx) addTransaction(tx) tx.start() @@ -565,6 +566,28 @@ internal class DefaultSasVerificationService @Inject constructor( }.executeBy(taskExecutor) } + override fun beginKeyVerificationInDMs(method: String, transactionId: String, roomId: String, + otherUserId: String, otherDeviceId: String, + callback: MatrixCallback?): String? { + if (KeyVerificationStart.VERIF_METHOD_SAS == method) { + val tx = DefaultOutgoingSASVerificationRequest( + setDeviceVerificationAction, + credentials, + cryptoStore, + myDeviceInfoHolder.get().myDevice.fingerprint()!!, + transactionId, + otherUserId, + otherDeviceId) + tx.transport = sasTransportRoomMessageFactory.createTransport(roomId, cryptoService) + addTransaction(tx) + + tx.start() + return transactionId + } else { + throw IllegalArgumentException("Unknown verification method") + } + } + /** * This string must be unique for the pair of users performing verification for the duration that the transaction is valid */ 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 31a89335b7..ae5f55b662 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 @@ -47,7 +47,15 @@ internal interface SasTransport { shortAuthenticationStrings: List): VerificationInfoAccept fun createKey(tid: String, - pubKey: String): VerificationInfoKey + pubKey: String): VerificationInfoKey + + fun createStart(fromDevice: String, + method: String, + transactionID: String, + keyAgreementProtocols: List, + hashes: List, + messageAuthenticationCodes: List, + shortAuthenticationStrings: List) : VerificationInfoStart fun createMac(tid: String, mac: Map, keys: String): VerificationInfoMac } 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 87bb4bc805..fc131764af 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 @@ -125,6 +125,27 @@ internal class SasTransportRoomMessage( override fun createKey(tid: String, pubKey: String): VerificationInfoKey = MessageVerificationKeyContent.create(tid, pubKey) override fun createMac(tid: String, mac: Map, keys: String) = MessageVerificationMacContent.create(tid, mac, keys) + + override fun createStart(fromDevice: String, + method: String, + transactionID: String, + keyAgreementProtocols: List, + hashes: List, + messageAuthenticationCodes: List, + shortAuthenticationStrings: List): VerificationInfoStart { + return MessageVerificationStartContent( + fromDevice, + hashes, + keyAgreementProtocols, + messageAuthenticationCodes, + shortAuthenticationStrings, + method, + RelationDefaultContent( + type = RelationType.REFERENCE, + eventId = transactionID + ) + ) + } } internal class SasTransportRoomMessageFactory @Inject constructor( 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 96ae47895e..f1b472ae75 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 @@ -20,10 +20,7 @@ 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.internal.crypto.model.MXUsersDevicesMap -import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept -import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel -import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey -import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac +import im.vector.matrix.android.internal.crypto.model.rest.* import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith @@ -113,6 +110,24 @@ internal class SasTransportToDevice( override fun createKey(tid: String, pubKey: String): VerificationInfoKey = KeyVerificationKey.create(tid, pubKey) override fun createMac(tid: String, mac: Map, keys: String) = KeyVerificationMac.create(tid, mac, keys) + + override fun createStart(fromDevice: String, + method: String, + transactionID: String, + keyAgreementProtocols: List, + hashes: List, + messageAuthenticationCodes: List, + shortAuthenticationStrings: List): VerificationInfoStart { + return KeyVerificationStart().apply { + this.fromDevice = fromDevice + this.method = method + this.transactionID = transactionID + this.keyAgreementProtocols = keyAgreementProtocols + this.hashes = hashes + this.messageAuthenticationCodes = messageAuthenticationCodes + this.shortAuthenticationStrings = shortAuthenticationStrings + } + } } internal class SasTransportToDeviceFactory @Inject constructor( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt index 793be10880..98cf9e234e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt @@ -46,6 +46,7 @@ object MoshiProvider { .registerSubtype(MessageVideoContent::class.java, MessageType.MSGTYPE_VIDEO) .registerSubtype(MessageLocationContent::class.java, MessageType.MSGTYPE_LOCATION) .registerSubtype(MessageFileContent::class.java, MessageType.MSGTYPE_FILE) + .registerSubtype(MessageVerificationRequestContent::class.java, MessageType.MSGTYPE_VERIFICATION_REQUEST) ) .add(SerializeNulls.JSON_ADAPTER_FACTORY) .build() From e14602d1dc011752827b775b992111136b55532e Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 5 Dec 2019 09:53:51 +0100 Subject: [PATCH 06/70] fix rebase --- .../home/room/detail/timeline/factory/TimelineItemFactory.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4c36b55fef..a705576234 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 @@ -72,7 +72,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.KEY_VERIFICATION_MAC -> { // These events are filtered from timeline in normal case // Only visible in developer mode - defaultItemFactory.create(event, highlight, readMarkerVisible, callback) + defaultItemFactory.create(event, highlight, callback) } // Unhandled event types (yet) From 3cdd373368a7a732e238a5aa73fe21b8f4a4872e Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 5 Dec 2019 10:08:27 +0100 Subject: [PATCH 07/70] Convert KeyVerificationStart to data class --- .../crypto/model/rest/KeyVerificationStart.kt | 75 +++++-------------- .../DefaultSasVerificationService.kt | 8 +- .../verification/SasTransportToDevice.kt | 17 ++--- .../verification/VerificationInfoStart.kt | 13 ++++ 4 files changed, 40 insertions(+), 73 deletions(-) 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 47c2a62d92..d1e0ff9049 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 @@ -27,75 +27,36 @@ import timber.log.Timber * Sent by Alice to initiate an interactive key verification. */ @JsonClass(generateAdapter = true) -class KeyVerificationStart : SendToDeviceObject, VerificationInfoStart { +data class KeyVerificationStart( + @Json(name = "from_device") override val fromDevice: String? = null, + override val method: String? = null, + @Json(name = "transaction_id") override val transactionID: String? = null, + @Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List? = null, + @Json(name = "hashes") override val hashes: List? = null, + @Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List? = null, + @Json(name = "short_authentication_string") override val shortAuthenticationStrings: List? = null +) : SendToDeviceObject, VerificationInfoStart { override fun toCanonicalJson(): String? { return JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, this) } - /** - * Alice’s device ID - */ - @Json(name = "from_device") - override var fromDevice: String? = null - - override var method: String? = null - - /** - * String to identify the transaction. - * 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. - */ - @Json(name = "transaction_id") - override var transactionID: String? = null - - /** - * An array of key agreement protocols that Alice’s client understands. - * Must include “curve25519”. - * Other methods may be defined in the future - */ - @Json(name = "key_agreement_protocols") - override var keyAgreementProtocols: List? = null - - /** - * An array of hashes that Alice’s client understands. - * Must include “sha256”. Other methods may be defined in the future. - */ - override var hashes: List? = null - - /** - * An array of message authentication codes that Alice’s client understands. - * Must include “hkdf-hmac-sha256”. - * Other methods may be defined in the future. - */ - @Json(name = "message_authentication_codes") - override var messageAuthenticationCodes: List? = null - - /** - * An array of short authentication string methods that Alice’s client (and Alice) understands. - * Must include “decimal”. - * This document also describes the “emoji” method. - * Other methods may be defined in the future - */ - @Json(name = "short_authentication_string") - override var shortAuthenticationStrings: List? = null companion object { const val VERIF_METHOD_SAS = "m.sas.v1" } override fun isValid(): Boolean { - if (transactionID.isNullOrBlank() - || fromDevice.isNullOrBlank() - || method != VERIF_METHOD_SAS - || keyAgreementProtocols.isNullOrEmpty() - || hashes.isNullOrEmpty() - || hashes?.contains("sha256") == false + if ((transactionID.isNullOrBlank() + || fromDevice.isNullOrBlank() + || method != VERIF_METHOD_SAS + || keyAgreementProtocols.isNullOrEmpty() + || hashes.isNullOrEmpty()) + || !hashes.contains("sha256") || messageAuthenticationCodes.isNullOrEmpty() - || (messageAuthenticationCodes?.contains(SASVerificationTransaction.SAS_MAC_SHA256) == false - && messageAuthenticationCodes?.contains(SASVerificationTransaction.SAS_MAC_SHA256_LONGKDF) == false) - || shortAuthenticationStrings.isNullOrEmpty() - || shortAuthenticationStrings?.contains(SasMode.DECIMAL) == false) { + || (!messageAuthenticationCodes.contains(SASVerificationTransaction.SAS_MAC_SHA256) + && !messageAuthenticationCodes.contains(SASVerificationTransaction.SAS_MAC_SHA256_LONGKDF)) + || shortAuthenticationStrings.isNullOrEmpty() || !shortAuthenticationStrings.contains(SasMode.DECIMAL)) { Timber.e("## received invalid verification request") return false } 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 2546f1f492..d7e01c2fde 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 @@ -239,14 +239,8 @@ internal class DefaultSasVerificationService @Inject constructor( if (!startReq.isValid()) { Timber.e("## SAS received invalid verification request") if (startReq.transactionID != null) { -// cancelTransaction( -// startReq.transactionID!!, -// otherUserId!!, -// startReq.fromDevice ?: event.getSenderKey()!!, -// CancelCode.UnknownMethod -// ) sasTransportToDeviceFactory.createTransport(null).cancelTransaction( - startReq.transactionID ?: "", + startReq.transactionID, otherUserId!!, startReq.fromDevice ?: event.getSenderKey()!!, CancelCode.UnknownMethod 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 f1b472ae75..bce23de1cd 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 @@ -118,15 +118,14 @@ internal class SasTransportToDevice( hashes: List, messageAuthenticationCodes: List, shortAuthenticationStrings: List): VerificationInfoStart { - return KeyVerificationStart().apply { - this.fromDevice = fromDevice - this.method = method - this.transactionID = transactionID - this.keyAgreementProtocols = keyAgreementProtocols - this.hashes = hashes - this.messageAuthenticationCodes = messageAuthenticationCodes - this.shortAuthenticationStrings = shortAuthenticationStrings - } + return KeyVerificationStart( + fromDevice, + method, + transactionID, + keyAgreementProtocols, + hashes, + messageAuthenticationCodes, + shortAuthenticationStrings) } } 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 f1cbd3f9f1..2248a239fb 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 @@ -18,10 +18,23 @@ package im.vector.matrix.android.internal.crypto.verification internal interface VerificationInfoStart : VerificationInfo { val method: String? + /** + * Alice’s device ID + */ val fromDevice: String? + /** + * String to identify the transaction. + * 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? + /** + * An array of key agreement protocols that Alice’s client understands. + * Must include “curve25519”. + * Other methods may be defined in the future + */ val keyAgreementProtocols: List? /** From c462d15bcf2c4c63737fce5c824037f82ff201f6 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 5 Dec 2019 11:20:53 +0100 Subject: [PATCH 08/70] rebase --- .../android/internal/crypto/model/rest/KeyVerificationStart.kt | 1 - .../vector/matrix/android/internal/network/RetrofitExtensions.kt | 1 - vector/src/main/java/im/vector/riotx/features/command/Command.kt | 1 + 3 files changed, 1 insertion(+), 2 deletions(-) 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 d1e0ff9049..e8c0334539 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 @@ -41,7 +41,6 @@ data class KeyVerificationStart( return JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, this) } - companion object { const val VERIF_METHOD_SAS = "m.sas.v1" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt index a89e21b04a..0bce924abc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.network -import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonEncodingException import im.vector.matrix.android.api.failure.ConsentNotGivenError import im.vector.matrix.android.api.failure.Failure diff --git a/vector/src/main/java/im/vector/riotx/features/command/Command.kt b/vector/src/main/java/im/vector/riotx/features/command/Command.kt index 3f636775c0..776e8385ad 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/Command.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/Command.kt @@ -38,6 +38,7 @@ enum class Command(val command: String, val parameters: String, @StringRes val d CHANGE_DISPLAY_NAME("/nick", "", R.string.command_description_nick), MARKDOWN("/markdown", "", R.string.command_description_markdown), CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token), + SPOILER("/spoiler", "", R.string.command_description_spoiler), SHRUG("/shrug", "", R.string.command_description_shrug), // TODO temporary command VERIFY_USER("/verify", "", R.string.command_description_verify); From 73f0132d5dd073ae7d1b307ba0859439b41cf0ad Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 10 Dec 2019 16:37:54 +0100 Subject: [PATCH 09/70] FIx / room transport was not updating state --- .../DefaultSasVerificationService.kt | 10 ++++----- .../verification/SasTransportRoomMessage.kt | 22 +++++++++++++++---- 2 files changed, 23 insertions(+), 9 deletions(-) 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 d7e01c2fde..d54ae3a22a 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 @@ -206,7 +206,7 @@ internal class DefaultSasVerificationService @Inject constructor( Timber.e("## received invalid verification request") if (startReq.transactionID != null) { sasTransportRoomMessageFactory.createTransport(event.roomId - ?: "", cryptoService).cancelTransaction( + ?: "", cryptoService, null).cancelTransaction( startReq.transactionID ?: "", otherUserId!!, startReq.fromDevice ?: event.getSenderKey()!!, @@ -218,10 +218,10 @@ internal class DefaultSasVerificationService @Inject constructor( handleStart(otherUserId, startReq as VerificationInfoStart) { it.transport = sasTransportRoomMessageFactory.createTransport(event.roomId - ?: "", cryptoService) + ?: "", cryptoService, it) }?.let { sasTransportRoomMessageFactory.createTransport(event.roomId - ?: "", cryptoService).cancelTransaction( + ?: "", cryptoService, null).cancelTransaction( startReq.transactionID ?: "", otherUserId!!, startReq.fromDevice ?: event.getSenderKey()!!, @@ -431,7 +431,7 @@ internal class DefaultSasVerificationService @Inject constructor( val otherUserId = event.senderId!! val existing = getExistingTransaction(otherUserId, keyReq.transactionID!!) if (existing == null) { - Timber.e("## SAS Received invalid accept request") + Timber.e("## SAS Received invalid key request") return } if (existing is SASVerificationTransaction) { @@ -572,7 +572,7 @@ internal class DefaultSasVerificationService @Inject constructor( transactionId, otherUserId, otherDeviceId) - tx.transport = sasTransportRoomMessageFactory.createTransport(roomId, cryptoService) + tx.transport = sasTransportRoomMessageFactory.createTransport(roomId, cryptoService, tx) addTransaction(tx) tx.start() 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 fc131764af..8e956fd60e 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 @@ -36,7 +36,7 @@ import javax.inject.Inject internal class SasTransportRoomMessage( private val roomId: String, private val cryptoService: CryptoService, -// private val tx: SASVerificationTransaction?, + private val tx: SASVerificationTransaction?, private val sendVerificationMessageTask: SendVerificationMessageTask, private val taskExecutor: TaskExecutor ) : SasTransport { @@ -57,6 +57,20 @@ internal class SasTransportRoomMessage( ) ) { constraints = TaskConstraints(true) + callback = object : MatrixCallback { + override fun onSuccess(data: SendResponse) { + if (onDone != null) { + onDone() + } else { + tx?.state = nextState + } + } + + override fun onFailure(failure: Throwable) { + Timber.e("## SAS verification [${tx?.transactionId}] failed to send toDevice in state : ${tx?.state}") + tx?.cancel(onErrorReason) + } + } retryCount = 3 } .executeBy(taskExecutor) @@ -153,9 +167,9 @@ internal class SasTransportRoomMessageFactory @Inject constructor( private val taskExecutor: TaskExecutor) { fun createTransport(roomId: String, - cryptoService: CryptoService -// tx: SASVerificationTransaction? + cryptoService: CryptoService, + tx: SASVerificationTransaction? ): SasTransportRoomMessage { - return SasTransportRoomMessage(roomId, cryptoService, /*tx,*/ sendVerificationMessageTask, taskExecutor) + return SasTransportRoomMessage(roomId, cryptoService, tx, sendVerificationMessageTask, taskExecutor) } } From 0b93f34fa0d17f9f9b3e5998a6cfe4da48a6e985 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 11 Dec 2019 10:44:51 +0100 Subject: [PATCH 10/70] Use diff_match_patch sources as dependency --- CHANGES.md | 2 +- build.gradle | 6 - diff-match-patch/.gitignore | 1 + diff-match-patch/build.gradle | 8 + .../neil/plaintext/diff_match_patch.java | 2471 +++++++++++++++++ settings.gradle | 2 +- vector/build.gradle | 3 +- 7 files changed, 2483 insertions(+), 10 deletions(-) create mode 100644 diff-match-patch/.gitignore create mode 100644 diff-match-patch/build.gradle create mode 100644 diff-match-patch/src/main/java/name/fraser/neil/plaintext/diff_match_patch.java diff --git a/CHANGES.md b/CHANGES.md index 21cf052d45..adb83ac55e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,7 +19,7 @@ Translations 🗣: - Build 🧱: - - + - Include diff-match-patch sources as dependency Changes in RiotX 0.9.1 (2019-12-05) =================================================== diff --git a/build.gradle b/build.gradle index 714152370e..20b0c94b82 100644 --- a/build.gradle +++ b/build.gradle @@ -45,12 +45,6 @@ allprojects { maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } google() jcenter() - maven { - url 'https://repo.adobe.com/nexus/content/repositories/public/' - content { - includeGroupByRegex "diff_match_patch" - } - } } tasks.withType(JavaCompile).all { diff --git a/diff-match-patch/.gitignore b/diff-match-patch/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/diff-match-patch/.gitignore @@ -0,0 +1 @@ +/build diff --git a/diff-match-patch/build.gradle b/diff-match-patch/build.gradle new file mode 100644 index 0000000000..82292e24db --- /dev/null +++ b/diff-match-patch/build.gradle @@ -0,0 +1,8 @@ +apply plugin: 'java-library' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) +} + +sourceCompatibility = "8" +targetCompatibility = "8" diff --git a/diff-match-patch/src/main/java/name/fraser/neil/plaintext/diff_match_patch.java b/diff-match-patch/src/main/java/name/fraser/neil/plaintext/diff_match_patch.java new file mode 100644 index 0000000000..9d07867de5 --- /dev/null +++ b/diff-match-patch/src/main/java/name/fraser/neil/plaintext/diff_match_patch.java @@ -0,0 +1,2471 @@ +/* + * Diff Match and Patch + * Copyright 2018 The diff-match-patch Authors. + * https://github.com/google/diff-match-patch + * + * 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 name.fraser.neil.plaintext; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/* + * Functions for diff, match and patch. + * Computes the difference between two texts to create a patch. + * Applies the patch onto another text, allowing for errors. + * + * @author fraser@google.com (Neil Fraser) + */ + +/** + * Class containing the diff, match and patch methods. + * Also contains the behaviour settings. + */ +public class diff_match_patch { + + // Defaults. + // Set these on your diff_match_patch instance to override the defaults. + + /** + * Number of seconds to map a diff before giving up (0 for infinity). + */ + public float Diff_Timeout = 1.0f; + /** + * Cost of an empty edit operation in terms of edit characters. + */ + public short Diff_EditCost = 4; + /** + * At what point is no match declared (0.0 = perfection, 1.0 = very loose). + */ + public float Match_Threshold = 0.5f; + /** + * How far to search for a match (0 = exact location, 1000+ = broad match). + * A match this many characters away from the expected location will add + * 1.0 to the score (0.0 is a perfect match). + */ + public int Match_Distance = 1000; + /** + * When deleting a large block of text (over ~64 characters), how close do + * the contents have to be to match the expected contents. (0.0 = perfection, + * 1.0 = very loose). Note that Match_Threshold controls how closely the + * end points of a delete need to match. + */ + public float Patch_DeleteThreshold = 0.5f; + /** + * Chunk size for context length. + */ + public short Patch_Margin = 4; + + /** + * The number of bits in an int. + */ + private short Match_MaxBits = 32; + + /** + * Internal class for returning results from diff_linesToChars(). + * Other less paranoid languages just use a three-element array. + */ + protected static class LinesToCharsResult { + protected String chars1; + protected String chars2; + protected List lineArray; + + protected LinesToCharsResult(String chars1, String chars2, + List lineArray) { + this.chars1 = chars1; + this.chars2 = chars2; + this.lineArray = lineArray; + } + } + + + // DIFF FUNCTIONS + + + /** + * The data structure representing a diff is a Linked list of Diff objects: + * {Diff(Operation.DELETE, "Hello"), Diff(Operation.INSERT, "Goodbye"), + * Diff(Operation.EQUAL, " world.")} + * which means: delete "Hello", add "Goodbye" and keep " world." + */ + public enum Operation { + DELETE, INSERT, EQUAL + } + + /** + * Find the differences between two texts. + * Run a faster, slightly less optimal diff. + * This method allows the 'checklines' of diff_main() to be optional. + * Most of the time checklines is wanted, so default to true. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @return Linked List of Diff objects. + */ + public LinkedList diff_main(String text1, String text2) { + return diff_main(text1, text2, true); + } + + /** + * Find the differences between two texts. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @return Linked List of Diff objects. + */ + public LinkedList diff_main(String text1, String text2, + boolean checklines) { + // Set a deadline by which time the diff must be complete. + long deadline; + if (Diff_Timeout <= 0) { + deadline = Long.MAX_VALUE; + } else { + deadline = System.currentTimeMillis() + (long) (Diff_Timeout * 1000); + } + return diff_main(text1, text2, checklines, deadline); + } + + /** + * Find the differences between two texts. Simplifies the problem by + * stripping any common prefix or suffix off the texts before diffing. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @param deadline Time when the diff should be complete by. Used + * internally for recursive calls. Users should set DiffTimeout instead. + * @return Linked List of Diff objects. + */ + private LinkedList diff_main(String text1, String text2, + boolean checklines, long deadline) { + // Check for null inputs. + if (text1 == null || text2 == null) { + throw new IllegalArgumentException("Null inputs. (diff_main)"); + } + + // Check for equality (speedup). + LinkedList diffs; + if (text1.equals(text2)) { + diffs = new LinkedList(); + if (text1.length() != 0) { + diffs.add(new Diff(Operation.EQUAL, text1)); + } + return diffs; + } + + // Trim off common prefix (speedup). + int commonlength = diff_commonPrefix(text1, text2); + String commonprefix = text1.substring(0, commonlength); + text1 = text1.substring(commonlength); + text2 = text2.substring(commonlength); + + // Trim off common suffix (speedup). + commonlength = diff_commonSuffix(text1, text2); + String commonsuffix = text1.substring(text1.length() - commonlength); + text1 = text1.substring(0, text1.length() - commonlength); + text2 = text2.substring(0, text2.length() - commonlength); + + // Compute the diff on the middle block. + diffs = diff_compute(text1, text2, checklines, deadline); + + // Restore the prefix and suffix. + if (commonprefix.length() != 0) { + diffs.addFirst(new Diff(Operation.EQUAL, commonprefix)); + } + if (commonsuffix.length() != 0) { + diffs.addLast(new Diff(Operation.EQUAL, commonsuffix)); + } + + diff_cleanupMerge(diffs); + return diffs; + } + + /** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff. + * @param deadline Time when the diff should be complete by. + * @return Linked List of Diff objects. + */ + private LinkedList diff_compute(String text1, String text2, + boolean checklines, long deadline) { + LinkedList diffs = new LinkedList(); + + if (text1.length() == 0) { + // Just add some text (speedup). + diffs.add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + if (text2.length() == 0) { + // Just delete some text (speedup). + diffs.add(new Diff(Operation.DELETE, text1)); + return diffs; + } + + String longtext = text1.length() > text2.length() ? text1 : text2; + String shorttext = text1.length() > text2.length() ? text2 : text1; + int i = longtext.indexOf(shorttext); + if (i != -1) { + // Shorter text is inside the longer text (speedup). + Operation op = (text1.length() > text2.length()) ? + Operation.DELETE : Operation.INSERT; + diffs.add(new Diff(op, longtext.substring(0, i))); + diffs.add(new Diff(Operation.EQUAL, shorttext)); + diffs.add(new Diff(op, longtext.substring(i + shorttext.length()))); + return diffs; + } + + if (shorttext.length() == 1) { + // Single character string. + // After the previous speedup, the character can't be an equality. + diffs.add(new Diff(Operation.DELETE, text1)); + diffs.add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + // Check to see if the problem can be split in two. + String[] hm = diff_halfMatch(text1, text2); + if (hm != null) { + // A half-match was found, sort out the return data. + String text1_a = hm[0]; + String text1_b = hm[1]; + String text2_a = hm[2]; + String text2_b = hm[3]; + String mid_common = hm[4]; + // Send both pairs off for separate processing. + LinkedList diffs_a = diff_main(text1_a, text2_a, + checklines, deadline); + LinkedList diffs_b = diff_main(text1_b, text2_b, + checklines, deadline); + // Merge the results. + diffs = diffs_a; + diffs.add(new Diff(Operation.EQUAL, mid_common)); + diffs.addAll(diffs_b); + return diffs; + } + + if (checklines && text1.length() > 100 && text2.length() > 100) { + return diff_lineMode(text1, text2, deadline); + } + + return diff_bisect(text1, text2, deadline); + } + + /** + * Do a quick line-level diff on both strings, then rediff the parts for + * greater accuracy. + * This speedup can produce non-minimal diffs. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param deadline Time when the diff should be complete by. + * @return Linked List of Diff objects. + */ + private LinkedList diff_lineMode(String text1, String text2, + long deadline) { + // Scan the text on a line-by-line basis first. + LinesToCharsResult a = diff_linesToChars(text1, text2); + text1 = a.chars1; + text2 = a.chars2; + List linearray = a.lineArray; + + LinkedList diffs = diff_main(text1, text2, false, deadline); + + // Convert the diff back to original text. + diff_charsToLines(diffs, linearray); + // Eliminate freak matches (e.g. blank lines) + diff_cleanupSemantic(diffs); + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + diffs.add(new Diff(Operation.EQUAL, "")); + int count_delete = 0; + int count_insert = 0; + String text_delete = ""; + String text_insert = ""; + ListIterator pointer = diffs.listIterator(); + Diff thisDiff = pointer.next(); + while (thisDiff != null) { + switch (thisDiff.operation) { + case INSERT: + count_insert++; + text_insert += thisDiff.text; + break; + case DELETE: + count_delete++; + text_delete += thisDiff.text; + break; + case EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete >= 1 && count_insert >= 1) { + // Delete the offending records and add the merged ones. + pointer.previous(); + for (int j = 0; j < count_delete + count_insert; j++) { + pointer.previous(); + pointer.remove(); + } + for (Diff subDiff : diff_main(text_delete, text_insert, false, + deadline)) { + pointer.add(subDiff); + } + } + count_insert = 0; + count_delete = 0; + text_delete = ""; + text_insert = ""; + break; + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + diffs.removeLast(); // Remove the dummy entry at the end. + + return diffs; + } + + /** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param deadline Time at which to bail if not yet complete. + * @return LinkedList of Diff objects. + */ + protected LinkedList diff_bisect(String text1, String text2, + long deadline) { + // Cache the text lengths to prevent multiple calls. + int text1_length = text1.length(); + int text2_length = text2.length(); + int max_d = (text1_length + text2_length + 1) / 2; + int v_offset = max_d; + int v_length = 2 * max_d; + int[] v1 = new int[v_length]; + int[] v2 = new int[v_length]; + for (int x = 0; x < v_length; x++) { + v1[x] = -1; + v2[x] = -1; + } + v1[v_offset + 1] = 0; + v2[v_offset + 1] = 0; + int delta = text1_length - text2_length; + // If the total number of characters is odd, then the front path will + // collide with the reverse path. + boolean front = (delta % 2 != 0); + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + int k1start = 0; + int k1end = 0; + int k2start = 0; + int k2end = 0; + for (int d = 0; d < max_d; d++) { + // Bail out if deadline is reached. + if (System.currentTimeMillis() > deadline) { + break; + } + + // Walk the front path one step. + for (int k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { + int k1_offset = v_offset + k1; + int x1; + if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) { + x1 = v1[k1_offset + 1]; + } else { + x1 = v1[k1_offset - 1] + 1; + } + int y1 = x1 - k1; + while (x1 < text1_length && y1 < text2_length + && text1.charAt(x1) == text2.charAt(y1)) { + x1++; + y1++; + } + v1[k1_offset] = x1; + if (x1 > text1_length) { + // Ran off the right of the graph. + k1end += 2; + } else if (y1 > text2_length) { + // Ran off the bottom of the graph. + k1start += 2; + } else if (front) { + int k2_offset = v_offset + delta - k1; + if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { + // Mirror x2 onto top-left coordinate system. + int x2 = text1_length - v2[k2_offset]; + if (x1 >= x2) { + // Overlap detected. + return diff_bisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + + // Walk the reverse path one step. + for (int k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { + int k2_offset = v_offset + k2; + int x2; + if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) { + x2 = v2[k2_offset + 1]; + } else { + x2 = v2[k2_offset - 1] + 1; + } + int y2 = x2 - k2; + while (x2 < text1_length && y2 < text2_length + && text1.charAt(text1_length - x2 - 1) + == text2.charAt(text2_length - y2 - 1)) { + x2++; + y2++; + } + v2[k2_offset] = x2; + if (x2 > text1_length) { + // Ran off the left of the graph. + k2end += 2; + } else if (y2 > text2_length) { + // Ran off the top of the graph. + k2start += 2; + } else if (!front) { + int k1_offset = v_offset + delta - k2; + if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { + int x1 = v1[k1_offset]; + int y1 = v_offset + x1 - k1_offset; + // Mirror x2 onto top-left coordinate system. + x2 = text1_length - x2; + if (x1 >= x2) { + // Overlap detected. + return diff_bisectSplit(text1, text2, x1, y1, deadline); + } + } + } + } + } + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + LinkedList diffs = new LinkedList(); + diffs.add(new Diff(Operation.DELETE, text1)); + diffs.add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + /** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param x Index of split point in text1. + * @param y Index of split point in text2. + * @param deadline Time at which to bail if not yet complete. + * @return LinkedList of Diff objects. + */ + private LinkedList diff_bisectSplit(String text1, String text2, + int x, int y, long deadline) { + String text1a = text1.substring(0, x); + String text2a = text2.substring(0, y); + String text1b = text1.substring(x); + String text2b = text2.substring(y); + + // Compute both diffs serially. + LinkedList diffs = diff_main(text1a, text2a, false, deadline); + LinkedList diffsb = diff_main(text1b, text2b, false, deadline); + + diffs.addAll(diffsb); + return diffs; + } + + /** + * Split two texts into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param text1 First string. + * @param text2 Second string. + * @return An object containing the encoded text1, the encoded text2 and + * the List of unique strings. The zeroth element of the List of + * unique strings is intentionally blank. + */ + protected LinesToCharsResult diff_linesToChars(String text1, String text2) { + List lineArray = new ArrayList(); + Map lineHash = new HashMap(); + // e.g. linearray[4] == "Hello\n" + // e.g. linehash.get("Hello\n") == 4 + + // "\x00" is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + lineArray.add(""); + + // Allocate 2/3rds of the space for text1, the rest for text2. + String chars1 = diff_linesToCharsMunge(text1, lineArray, lineHash, 40000); + String chars2 = diff_linesToCharsMunge(text2, lineArray, lineHash, 65535); + return new LinesToCharsResult(chars1, chars2, lineArray); + } + + /** + * Split a text into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param text String to encode. + * @param lineArray List of unique strings. + * @param lineHash Map of strings to indices. + * @param maxLines Maximum length of lineArray. + * @return Encoded string. + */ + private String diff_linesToCharsMunge(String text, List lineArray, + Map lineHash, int maxLines) { + int lineStart = 0; + int lineEnd = -1; + String line; + StringBuilder chars = new StringBuilder(); + // Walk the text, pulling out a substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + while (lineEnd < text.length() - 1) { + lineEnd = text.indexOf('\n', lineStart); + if (lineEnd == -1) { + lineEnd = text.length() - 1; + } + line = text.substring(lineStart, lineEnd + 1); + + if (lineHash.containsKey(line)) { + chars.append(String.valueOf((char) (int) lineHash.get(line))); + } else { + if (lineArray.size() == maxLines) { + // Bail out at 65535 because + // String.valueOf((char) 65536).equals(String.valueOf(((char) 0))) + line = text.substring(lineStart); + lineEnd = text.length(); + } + lineArray.add(line); + lineHash.put(line, lineArray.size() - 1); + chars.append(String.valueOf((char) (lineArray.size() - 1))); + } + lineStart = lineEnd + 1; + } + return chars.toString(); + } + + /** + * Rehydrate the text in a diff from a string of line hashes to real lines of + * text. + * @param diffs List of Diff objects. + * @param lineArray List of unique strings. + */ + protected void diff_charsToLines(List diffs, + List lineArray) { + StringBuilder text; + for (Diff diff : diffs) { + text = new StringBuilder(); + for (int j = 0; j < diff.text.length(); j++) { + text.append(lineArray.get(diff.text.charAt(j))); + } + diff.text = text.toString(); + } + } + + /** + * Determine the common prefix of two strings + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the start of each string. + */ + public int diff_commonPrefix(String text1, String text2) { + // Performance analysis: https://neil.fraser.name/news/2007/10/09/ + int n = Math.min(text1.length(), text2.length()); + for (int i = 0; i < n; i++) { + if (text1.charAt(i) != text2.charAt(i)) { + return i; + } + } + return n; + } + + /** + * Determine the common suffix of two strings + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of each string. + */ + public int diff_commonSuffix(String text1, String text2) { + // Performance analysis: https://neil.fraser.name/news/2007/10/09/ + int text1_length = text1.length(); + int text2_length = text2.length(); + int n = Math.min(text1_length, text2_length); + for (int i = 1; i <= n; i++) { + if (text1.charAt(text1_length - i) != text2.charAt(text2_length - i)) { + return i - 1; + } + } + return n; + } + + /** + * Determine if the suffix of one string is the prefix of another. + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of the first + * string and the start of the second string. + */ + protected int diff_commonOverlap(String text1, String text2) { + // Cache the text lengths to prevent multiple calls. + int text1_length = text1.length(); + int text2_length = text2.length(); + // Eliminate the null case. + if (text1_length == 0 || text2_length == 0) { + return 0; + } + // Truncate the longer string. + if (text1_length > text2_length) { + text1 = text1.substring(text1_length - text2_length); + } else if (text1_length < text2_length) { + text2 = text2.substring(0, text1_length); + } + int text_length = Math.min(text1_length, text2_length); + // Quick check for the worst case. + if (text1.equals(text2)) { + return text_length; + } + + // Start by looking for a single character match + // and increase length until no match is found. + // Performance analysis: https://neil.fraser.name/news/2010/11/04/ + int best = 0; + int length = 1; + while (true) { + String pattern = text1.substring(text_length - length); + int found = text2.indexOf(pattern); + if (found == -1) { + return best; + } + length += found; + if (found == 0 || text1.substring(text_length - length).equals( + text2.substring(0, length))) { + best = length; + length++; + } + } + } + + /** + * Do the two texts share a substring which is at least half the length of + * the longer text? + * This speedup can produce non-minimal diffs. + * @param text1 First string. + * @param text2 Second string. + * @return Five element String array, containing the prefix of text1, the + * suffix of text1, the prefix of text2, the suffix of text2 and the + * common middle. Or null if there was no match. + */ + protected String[] diff_halfMatch(String text1, String text2) { + if (Diff_Timeout <= 0) { + // Don't risk returning a non-optimal diff if we have unlimited time. + return null; + } + String longtext = text1.length() > text2.length() ? text1 : text2; + String shorttext = text1.length() > text2.length() ? text2 : text1; + if (longtext.length() < 4 || shorttext.length() * 2 < longtext.length()) { + return null; // Pointless. + } + + // First check if the second quarter is the seed for a half-match. + String[] hm1 = diff_halfMatchI(longtext, shorttext, + (longtext.length() + 3) / 4); + // Check again based on the third quarter. + String[] hm2 = diff_halfMatchI(longtext, shorttext, + (longtext.length() + 1) / 2); + String[] hm; + if (hm1 == null && hm2 == null) { + return null; + } else if (hm2 == null) { + hm = hm1; + } else if (hm1 == null) { + hm = hm2; + } else { + // Both matched. Select the longest. + hm = hm1[4].length() > hm2[4].length() ? hm1 : hm2; + } + + // A half-match was found, sort out the return data. + if (text1.length() > text2.length()) { + return hm; + //return new String[]{hm[0], hm[1], hm[2], hm[3], hm[4]}; + } else { + return new String[]{hm[2], hm[3], hm[0], hm[1], hm[4]}; + } + } + + /** + * Does a substring of shorttext exist within longtext such that the + * substring is at least half the length of longtext? + * @param longtext Longer string. + * @param shorttext Shorter string. + * @param i Start index of quarter length substring within longtext. + * @return Five element String array, containing the prefix of longtext, the + * suffix of longtext, the prefix of shorttext, the suffix of shorttext + * and the common middle. Or null if there was no match. + */ + private String[] diff_halfMatchI(String longtext, String shorttext, int i) { + // Start with a 1/4 length substring at position i as a seed. + String seed = longtext.substring(i, i + longtext.length() / 4); + int j = -1; + String best_common = ""; + String best_longtext_a = "", best_longtext_b = ""; + String best_shorttext_a = "", best_shorttext_b = ""; + while ((j = shorttext.indexOf(seed, j + 1)) != -1) { + int prefixLength = diff_commonPrefix(longtext.substring(i), + shorttext.substring(j)); + int suffixLength = diff_commonSuffix(longtext.substring(0, i), + shorttext.substring(0, j)); + if (best_common.length() < suffixLength + prefixLength) { + best_common = shorttext.substring(j - suffixLength, j) + + shorttext.substring(j, j + prefixLength); + best_longtext_a = longtext.substring(0, i - suffixLength); + best_longtext_b = longtext.substring(i + prefixLength); + best_shorttext_a = shorttext.substring(0, j - suffixLength); + best_shorttext_b = shorttext.substring(j + prefixLength); + } + } + if (best_common.length() * 2 >= longtext.length()) { + return new String[]{best_longtext_a, best_longtext_b, + best_shorttext_a, best_shorttext_b, best_common}; + } else { + return null; + } + } + + /** + * Reduce the number of edits by eliminating semantically trivial equalities. + * @param diffs LinkedList of Diff objects. + */ + public void diff_cleanupSemantic(LinkedList diffs) { + if (diffs.isEmpty()) { + return; + } + boolean changes = false; + Deque equalities = new ArrayDeque(); // Double-ended queue of qualities. + String lastEquality = null; // Always equal to equalities.peek().text + ListIterator pointer = diffs.listIterator(); + // Number of characters that changed prior to the equality. + int length_insertions1 = 0; + int length_deletions1 = 0; + // Number of characters that changed after the equality. + int length_insertions2 = 0; + int length_deletions2 = 0; + Diff thisDiff = pointer.next(); + while (thisDiff != null) { + if (thisDiff.operation == Operation.EQUAL) { + // Equality found. + equalities.push(thisDiff); + length_insertions1 = length_insertions2; + length_deletions1 = length_deletions2; + length_insertions2 = 0; + length_deletions2 = 0; + lastEquality = thisDiff.text; + } else { + // An insertion or deletion. + if (thisDiff.operation == Operation.INSERT) { + length_insertions2 += thisDiff.text.length(); + } else { + length_deletions2 += thisDiff.text.length(); + } + // Eliminate an equality that is smaller or equal to the edits on both + // sides of it. + if (lastEquality != null && (lastEquality.length() + <= Math.max(length_insertions1, length_deletions1)) + && (lastEquality.length() + <= Math.max(length_insertions2, length_deletions2))) { + //System.out.println("Splitting: '" + lastEquality + "'"); + // Walk back to offending equality. + while (thisDiff != equalities.peek()) { + thisDiff = pointer.previous(); + } + pointer.next(); + + // Replace equality with a delete. + pointer.set(new Diff(Operation.DELETE, lastEquality)); + // Insert a corresponding an insert. + pointer.add(new Diff(Operation.INSERT, lastEquality)); + + equalities.pop(); // Throw away the equality we just deleted. + if (!equalities.isEmpty()) { + // Throw away the previous equality (it needs to be reevaluated). + equalities.pop(); + } + if (equalities.isEmpty()) { + // There are no previous equalities, walk back to the start. + while (pointer.hasPrevious()) { + pointer.previous(); + } + } else { + // There is a safe equality we can fall back to. + thisDiff = equalities.peek(); + while (thisDiff != pointer.previous()) { + // Intentionally empty loop. + } + } + + length_insertions1 = 0; // Reset the counters. + length_insertions2 = 0; + length_deletions1 = 0; + length_deletions2 = 0; + lastEquality = null; + changes = true; + } + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + + // Normalize the diff. + if (changes) { + diff_cleanupMerge(diffs); + } + diff_cleanupSemanticLossless(diffs); + + // Find any overlaps between deletions and insertions. + // e.g: abcxxxxxxdef + // -> abcxxxdef + // e.g: xxxabcdefxxx + // -> defxxxabc + // Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = diffs.listIterator(); + Diff prevDiff = null; + thisDiff = null; + if (pointer.hasNext()) { + prevDiff = pointer.next(); + if (pointer.hasNext()) { + thisDiff = pointer.next(); + } + } + while (thisDiff != null) { + if (prevDiff.operation == Operation.DELETE && + thisDiff.operation == Operation.INSERT) { + String deletion = prevDiff.text; + String insertion = thisDiff.text; + int overlap_length1 = this.diff_commonOverlap(deletion, insertion); + int overlap_length2 = this.diff_commonOverlap(insertion, deletion); + if (overlap_length1 >= overlap_length2) { + if (overlap_length1 >= deletion.length() / 2.0 || + overlap_length1 >= insertion.length() / 2.0) { + // Overlap found. Insert an equality and trim the surrounding edits. + pointer.previous(); + pointer.add(new Diff(Operation.EQUAL, + insertion.substring(0, overlap_length1))); + prevDiff.text = + deletion.substring(0, deletion.length() - overlap_length1); + thisDiff.text = insertion.substring(overlap_length1); + // pointer.add inserts the element before the cursor, so there is + // no need to step past the new element. + } + } else { + if (overlap_length2 >= deletion.length() / 2.0 || + overlap_length2 >= insertion.length() / 2.0) { + // Reverse overlap found. + // Insert an equality and swap and trim the surrounding edits. + pointer.previous(); + pointer.add(new Diff(Operation.EQUAL, + deletion.substring(0, overlap_length2))); + prevDiff.operation = Operation.INSERT; + prevDiff.text = + insertion.substring(0, insertion.length() - overlap_length2); + thisDiff.operation = Operation.DELETE; + thisDiff.text = deletion.substring(overlap_length2); + // pointer.add inserts the element before the cursor, so there is + // no need to step past the new element. + } + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + prevDiff = thisDiff; + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + } + + /** + * Look for single edits surrounded on both sides by equalities + * which can be shifted sideways to align the edit to a word boundary. + * e.g: The cat came. -> The cat came. + * @param diffs LinkedList of Diff objects. + */ + public void diff_cleanupSemanticLossless(LinkedList diffs) { + String equality1, edit, equality2; + String commonString; + int commonOffset; + int score, bestScore; + String bestEquality1, bestEdit, bestEquality2; + // Create a new iterator at the start. + ListIterator pointer = diffs.listIterator(); + Diff prevDiff = pointer.hasNext() ? pointer.next() : null; + Diff thisDiff = pointer.hasNext() ? pointer.next() : null; + Diff nextDiff = pointer.hasNext() ? pointer.next() : null; + // Intentionally ignore the first and last element (don't need checking). + while (nextDiff != null) { + if (prevDiff.operation == Operation.EQUAL && + nextDiff.operation == Operation.EQUAL) { + // This is a single edit surrounded by equalities. + equality1 = prevDiff.text; + edit = thisDiff.text; + equality2 = nextDiff.text; + + // First, shift the edit as far left as possible. + commonOffset = diff_commonSuffix(equality1, edit); + if (commonOffset != 0) { + commonString = edit.substring(edit.length() - commonOffset); + equality1 = equality1.substring(0, equality1.length() - commonOffset); + edit = commonString + edit.substring(0, edit.length() - commonOffset); + equality2 = commonString + equality2; + } + + // Second, step character by character right, looking for the best fit. + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + bestScore = diff_cleanupSemanticScore(equality1, edit) + + diff_cleanupSemanticScore(edit, equality2); + while (edit.length() != 0 && equality2.length() != 0 + && edit.charAt(0) == equality2.charAt(0)) { + equality1 += edit.charAt(0); + edit = edit.substring(1) + equality2.charAt(0); + equality2 = equality2.substring(1); + score = diff_cleanupSemanticScore(equality1, edit) + + diff_cleanupSemanticScore(edit, equality2); + // The >= encourages trailing rather than leading whitespace on edits. + if (score >= bestScore) { + bestScore = score; + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + } + } + + if (!prevDiff.text.equals(bestEquality1)) { + // We have an improvement, save it back to the diff. + if (bestEquality1.length() != 0) { + prevDiff.text = bestEquality1; + } else { + pointer.previous(); // Walk past nextDiff. + pointer.previous(); // Walk past thisDiff. + pointer.previous(); // Walk past prevDiff. + pointer.remove(); // Delete prevDiff. + pointer.next(); // Walk past thisDiff. + pointer.next(); // Walk past nextDiff. + } + thisDiff.text = bestEdit; + if (bestEquality2.length() != 0) { + nextDiff.text = bestEquality2; + } else { + pointer.remove(); // Delete nextDiff. + nextDiff = thisDiff; + thisDiff = prevDiff; + } + } + } + prevDiff = thisDiff; + thisDiff = nextDiff; + nextDiff = pointer.hasNext() ? pointer.next() : null; + } + } + + /** + * Given two strings, compute a score representing whether the internal + * boundary falls on logical boundaries. + * Scores range from 6 (best) to 0 (worst). + * @param one First string. + * @param two Second string. + * @return The score. + */ + private int diff_cleanupSemanticScore(String one, String two) { + if (one.length() == 0 || two.length() == 0) { + // Edges are the best. + return 6; + } + + // Each port of this function behaves slightly differently due to + // subtle differences in each language's definition of things like + // 'whitespace'. Since this function's purpose is largely cosmetic, + // the choice has been made to use each language's native features + // rather than force total conformity. + char char1 = one.charAt(one.length() - 1); + char char2 = two.charAt(0); + boolean nonAlphaNumeric1 = !Character.isLetterOrDigit(char1); + boolean nonAlphaNumeric2 = !Character.isLetterOrDigit(char2); + boolean whitespace1 = nonAlphaNumeric1 && Character.isWhitespace(char1); + boolean whitespace2 = nonAlphaNumeric2 && Character.isWhitespace(char2); + boolean lineBreak1 = whitespace1 + && Character.getType(char1) == Character.CONTROL; + boolean lineBreak2 = whitespace2 + && Character.getType(char2) == Character.CONTROL; + boolean blankLine1 = lineBreak1 && BLANKLINEEND.matcher(one).find(); + boolean blankLine2 = lineBreak2 && BLANKLINESTART.matcher(two).find(); + + if (blankLine1 || blankLine2) { + // Five points for blank lines. + return 5; + } else if (lineBreak1 || lineBreak2) { + // Four points for line breaks. + return 4; + } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { + // Three points for end of sentences. + return 3; + } else if (whitespace1 || whitespace2) { + // Two points for whitespace. + return 2; + } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { + // One point for non-alphanumeric. + return 1; + } + return 0; + } + + // Define some regex patterns for matching boundaries. + private Pattern BLANKLINEEND + = Pattern.compile("\\n\\r?\\n\\Z", Pattern.DOTALL); + private Pattern BLANKLINESTART + = Pattern.compile("\\A\\r?\\n\\r?\\n", Pattern.DOTALL); + + /** + * Reduce the number of edits by eliminating operationally trivial equalities. + * @param diffs LinkedList of Diff objects. + */ + public void diff_cleanupEfficiency(LinkedList diffs) { + if (diffs.isEmpty()) { + return; + } + boolean changes = false; + Deque equalities = new ArrayDeque(); // Double-ended queue of equalities. + String lastEquality = null; // Always equal to equalities.peek().text + ListIterator pointer = diffs.listIterator(); + // Is there an insertion operation before the last equality. + boolean pre_ins = false; + // Is there a deletion operation before the last equality. + boolean pre_del = false; + // Is there an insertion operation after the last equality. + boolean post_ins = false; + // Is there a deletion operation after the last equality. + boolean post_del = false; + Diff thisDiff = pointer.next(); + Diff safeDiff = thisDiff; // The last Diff that is known to be unsplittable. + while (thisDiff != null) { + if (thisDiff.operation == Operation.EQUAL) { + // Equality found. + if (thisDiff.text.length() < Diff_EditCost && (post_ins || post_del)) { + // Candidate found. + equalities.push(thisDiff); + pre_ins = post_ins; + pre_del = post_del; + lastEquality = thisDiff.text; + } else { + // Not a candidate, and can never become one. + equalities.clear(); + lastEquality = null; + safeDiff = thisDiff; + } + post_ins = post_del = false; + } else { + // An insertion or deletion. + if (thisDiff.operation == Operation.DELETE) { + post_del = true; + } else { + post_ins = true; + } + /* + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + */ + if (lastEquality != null + && ((pre_ins && pre_del && post_ins && post_del) + || ((lastEquality.length() < Diff_EditCost / 2) + && ((pre_ins ? 1 : 0) + (pre_del ? 1 : 0) + + (post_ins ? 1 : 0) + (post_del ? 1 : 0)) == 3))) { + //System.out.println("Splitting: '" + lastEquality + "'"); + // Walk back to offending equality. + while (thisDiff != equalities.peek()) { + thisDiff = pointer.previous(); + } + pointer.next(); + + // Replace equality with a delete. + pointer.set(new Diff(Operation.DELETE, lastEquality)); + // Insert a corresponding an insert. + pointer.add(thisDiff = new Diff(Operation.INSERT, lastEquality)); + + equalities.pop(); // Throw away the equality we just deleted. + lastEquality = null; + if (pre_ins && pre_del) { + // No changes made which could affect previous entry, keep going. + post_ins = post_del = true; + equalities.clear(); + safeDiff = thisDiff; + } else { + if (!equalities.isEmpty()) { + // Throw away the previous equality (it needs to be reevaluated). + equalities.pop(); + } + if (equalities.isEmpty()) { + // There are no previous questionable equalities, + // walk back to the last known safe diff. + thisDiff = safeDiff; + } else { + // There is an equality we can fall back to. + thisDiff = equalities.peek(); + } + while (thisDiff != pointer.previous()) { + // Intentionally empty loop. + } + post_ins = post_del = false; + } + + changes = true; + } + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + + if (changes) { + diff_cleanupMerge(diffs); + } + } + + /** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param diffs LinkedList of Diff objects. + */ + public void diff_cleanupMerge(LinkedList diffs) { + diffs.add(new Diff(Operation.EQUAL, "")); // Add a dummy entry at the end. + ListIterator pointer = diffs.listIterator(); + int count_delete = 0; + int count_insert = 0; + String text_delete = ""; + String text_insert = ""; + Diff thisDiff = pointer.next(); + Diff prevEqual = null; + int commonlength; + while (thisDiff != null) { + switch (thisDiff.operation) { + case INSERT: + count_insert++; + text_insert += thisDiff.text; + prevEqual = null; + break; + case DELETE: + count_delete++; + text_delete += thisDiff.text; + prevEqual = null; + break; + case EQUAL: + if (count_delete + count_insert > 1) { + boolean both_types = count_delete != 0 && count_insert != 0; + // Delete the offending records. + pointer.previous(); // Reverse direction. + while (count_delete-- > 0) { + pointer.previous(); + pointer.remove(); + } + while (count_insert-- > 0) { + pointer.previous(); + pointer.remove(); + } + if (both_types) { + // Factor out any common prefixies. + commonlength = diff_commonPrefix(text_insert, text_delete); + if (commonlength != 0) { + if (pointer.hasPrevious()) { + thisDiff = pointer.previous(); + assert thisDiff.operation == Operation.EQUAL + : "Previous diff should have been an equality."; + thisDiff.text += text_insert.substring(0, commonlength); + pointer.next(); + } else { + pointer.add(new Diff(Operation.EQUAL, + text_insert.substring(0, commonlength))); + } + text_insert = text_insert.substring(commonlength); + text_delete = text_delete.substring(commonlength); + } + // Factor out any common suffixies. + commonlength = diff_commonSuffix(text_insert, text_delete); + if (commonlength != 0) { + thisDiff = pointer.next(); + thisDiff.text = text_insert.substring(text_insert.length() + - commonlength) + thisDiff.text; + text_insert = text_insert.substring(0, text_insert.length() + - commonlength); + text_delete = text_delete.substring(0, text_delete.length() + - commonlength); + pointer.previous(); + } + } + // Insert the merged records. + if (text_delete.length() != 0) { + pointer.add(new Diff(Operation.DELETE, text_delete)); + } + if (text_insert.length() != 0) { + pointer.add(new Diff(Operation.INSERT, text_insert)); + } + // Step forward to the equality. + thisDiff = pointer.hasNext() ? pointer.next() : null; + } else if (prevEqual != null) { + // Merge this equality with the previous one. + prevEqual.text += thisDiff.text; + pointer.remove(); + thisDiff = pointer.previous(); + pointer.next(); // Forward direction + } + count_insert = 0; + count_delete = 0; + text_delete = ""; + text_insert = ""; + prevEqual = thisDiff; + break; + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + if (diffs.getLast().text.length() == 0) { + diffs.removeLast(); // Remove the dummy entry at the end. + } + + /* + * Second pass: look for single edits surrounded on both sides by equalities + * which can be shifted sideways to eliminate an equality. + * e.g: ABAC -> ABAC + */ + boolean changes = false; + // Create a new iterator at the start. + // (As opposed to walking the current one back.) + pointer = diffs.listIterator(); + Diff prevDiff = pointer.hasNext() ? pointer.next() : null; + thisDiff = pointer.hasNext() ? pointer.next() : null; + Diff nextDiff = pointer.hasNext() ? pointer.next() : null; + // Intentionally ignore the first and last element (don't need checking). + while (nextDiff != null) { + if (prevDiff.operation == Operation.EQUAL && + nextDiff.operation == Operation.EQUAL) { + // This is a single edit surrounded by equalities. + if (thisDiff.text.endsWith(prevDiff.text)) { + // Shift the edit over the previous equality. + thisDiff.text = prevDiff.text + + thisDiff.text.substring(0, thisDiff.text.length() + - prevDiff.text.length()); + nextDiff.text = prevDiff.text + nextDiff.text; + pointer.previous(); // Walk past nextDiff. + pointer.previous(); // Walk past thisDiff. + pointer.previous(); // Walk past prevDiff. + pointer.remove(); // Delete prevDiff. + pointer.next(); // Walk past thisDiff. + thisDiff = pointer.next(); // Walk past nextDiff. + nextDiff = pointer.hasNext() ? pointer.next() : null; + changes = true; + } else if (thisDiff.text.startsWith(nextDiff.text)) { + // Shift the edit over the next equality. + prevDiff.text += nextDiff.text; + thisDiff.text = thisDiff.text.substring(nextDiff.text.length()) + + nextDiff.text; + pointer.remove(); // Delete nextDiff. + nextDiff = pointer.hasNext() ? pointer.next() : null; + changes = true; + } + } + prevDiff = thisDiff; + thisDiff = nextDiff; + nextDiff = pointer.hasNext() ? pointer.next() : null; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if (changes) { + diff_cleanupMerge(diffs); + } + } + + /** + * loc is a location in text1, compute and return the equivalent location in + * text2. + * e.g. "The cat" vs "The big cat", 1->1, 5->8 + * @param diffs List of Diff objects. + * @param loc Location within text1. + * @return Location within text2. + */ + public int diff_xIndex(List diffs, int loc) { + int chars1 = 0; + int chars2 = 0; + int last_chars1 = 0; + int last_chars2 = 0; + Diff lastDiff = null; + for (Diff aDiff : diffs) { + if (aDiff.operation != Operation.INSERT) { + // Equality or deletion. + chars1 += aDiff.text.length(); + } + if (aDiff.operation != Operation.DELETE) { + // Equality or insertion. + chars2 += aDiff.text.length(); + } + if (chars1 > loc) { + // Overshot the location. + lastDiff = aDiff; + break; + } + last_chars1 = chars1; + last_chars2 = chars2; + } + if (lastDiff != null && lastDiff.operation == Operation.DELETE) { + // The location was deleted. + return last_chars2; + } + // Add the remaining character length. + return last_chars2 + (loc - last_chars1); + } + + /** + * Convert a Diff list into a pretty HTML report. + * @param diffs List of Diff objects. + * @return HTML representation. + */ + public String diff_prettyHtml(List diffs) { + StringBuilder html = new StringBuilder(); + for (Diff aDiff : diffs) { + String text = aDiff.text.replace("&", "&").replace("<", "<") + .replace(">", ">").replace("\n", "¶
"); + switch (aDiff.operation) { + case INSERT: + html.append("").append(text) + .append(""); + break; + case DELETE: + html.append("").append(text) + .append(""); + break; + case EQUAL: + html.append("").append(text).append(""); + break; + } + } + return html.toString(); + } + + /** + * Compute and return the source text (all equalities and deletions). + * @param diffs List of Diff objects. + * @return Source text. + */ + public String diff_text1(List diffs) { + StringBuilder text = new StringBuilder(); + for (Diff aDiff : diffs) { + if (aDiff.operation != Operation.INSERT) { + text.append(aDiff.text); + } + } + return text.toString(); + } + + /** + * Compute and return the destination text (all equalities and insertions). + * @param diffs List of Diff objects. + * @return Destination text. + */ + public String diff_text2(List diffs) { + StringBuilder text = new StringBuilder(); + for (Diff aDiff : diffs) { + if (aDiff.operation != Operation.DELETE) { + text.append(aDiff.text); + } + } + return text.toString(); + } + + /** + * Compute the Levenshtein distance; the number of inserted, deleted or + * substituted characters. + * @param diffs List of Diff objects. + * @return Number of changes. + */ + public int diff_levenshtein(List diffs) { + int levenshtein = 0; + int insertions = 0; + int deletions = 0; + for (Diff aDiff : diffs) { + switch (aDiff.operation) { + case INSERT: + insertions += aDiff.text.length(); + break; + case DELETE: + deletions += aDiff.text.length(); + break; + case EQUAL: + // A deletion and an insertion is one substitution. + levenshtein += Math.max(insertions, deletions); + insertions = 0; + deletions = 0; + break; + } + } + levenshtein += Math.max(insertions, deletions); + return levenshtein; + } + + /** + * Crush the diff into an encoded string which describes the operations + * required to transform text1 into text2. + * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. + * Operations are tab-separated. Inserted text is escaped using %xx notation. + * @param diffs List of Diff objects. + * @return Delta text. + */ + public String diff_toDelta(List diffs) { + StringBuilder text = new StringBuilder(); + for (Diff aDiff : diffs) { + switch (aDiff.operation) { + case INSERT: + try { + text.append("+").append(URLEncoder.encode(aDiff.text, "UTF-8") + .replace('+', ' ')).append("\t"); + } catch (UnsupportedEncodingException e) { + // Not likely on modern system. + throw new Error("This system does not support UTF-8.", e); + } + break; + case DELETE: + text.append("-").append(aDiff.text.length()).append("\t"); + break; + case EQUAL: + text.append("=").append(aDiff.text.length()).append("\t"); + break; + } + } + String delta = text.toString(); + if (delta.length() != 0) { + // Strip off trailing tab character. + delta = delta.substring(0, delta.length() - 1); + delta = unescapeForEncodeUriCompatability(delta); + } + return delta; + } + + /** + * Given the original text1, and an encoded string which describes the + * operations required to transform text1 into text2, compute the full diff. + * @param text1 Source string for the diff. + * @param delta Delta text. + * @return Array of Diff objects or null if invalid. + * @throws IllegalArgumentException If invalid input. + */ + public LinkedList diff_fromDelta(String text1, String delta) + throws IllegalArgumentException { + LinkedList diffs = new LinkedList(); + int pointer = 0; // Cursor in text1 + String[] tokens = delta.split("\t"); + for (String token : tokens) { + if (token.length() == 0) { + // Blank tokens are ok (from a trailing \t). + continue; + } + // Each token begins with a one character parameter which specifies the + // operation of this token (delete, insert, equality). + String param = token.substring(1); + switch (token.charAt(0)) { + case '+': + // decode would change all "+" to " " + param = param.replace("+", "%2B"); + try { + param = URLDecoder.decode(param, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // Not likely on modern system. + throw new Error("This system does not support UTF-8.", e); + } catch (IllegalArgumentException e) { + // Malformed URI sequence. + throw new IllegalArgumentException( + "Illegal escape in diff_fromDelta: " + param, e); + } + diffs.add(new Diff(Operation.INSERT, param)); + break; + case '-': + // Fall through. + case '=': + int n; + try { + n = Integer.parseInt(param); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Invalid number in diff_fromDelta: " + param, e); + } + if (n < 0) { + throw new IllegalArgumentException( + "Negative number in diff_fromDelta: " + param); + } + String text; + try { + text = text1.substring(pointer, pointer += n); + } catch (StringIndexOutOfBoundsException e) { + throw new IllegalArgumentException("Delta length (" + pointer + + ") larger than source text length (" + text1.length() + + ").", e); + } + if (token.charAt(0) == '=') { + diffs.add(new Diff(Operation.EQUAL, text)); + } else { + diffs.add(new Diff(Operation.DELETE, text)); + } + break; + default: + // Anything else is an error. + throw new IllegalArgumentException( + "Invalid diff operation in diff_fromDelta: " + token.charAt(0)); + } + } + if (pointer != text1.length()) { + throw new IllegalArgumentException("Delta length (" + pointer + + ") smaller than source text length (" + text1.length() + ")."); + } + return diffs; + } + + + // MATCH FUNCTIONS + + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc'. + * Returns -1 if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + public int match_main(String text, String pattern, int loc) { + // Check for null inputs. + if (text == null || pattern == null) { + throw new IllegalArgumentException("Null inputs. (match_main)"); + } + + loc = Math.max(0, Math.min(loc, text.length())); + if (text.equals(pattern)) { + // Shortcut (potentially not guaranteed by the algorithm) + return 0; + } else if (text.length() == 0) { + // Nothing to match. + return -1; + } else if (loc + pattern.length() <= text.length() + && text.substring(loc, loc + pattern.length()).equals(pattern)) { + // Perfect match at the perfect spot! (Includes case of null pattern) + return loc; + } else { + // Do a fuzzy compare. + return match_bitap(text, pattern, loc); + } + } + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the + * Bitap algorithm. Returns -1 if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + protected int match_bitap(String text, String pattern, int loc) { + assert (Match_MaxBits == 0 || pattern.length() <= Match_MaxBits) + : "Pattern too long for this application."; + + // Initialise the alphabet. + Map s = match_alphabet(pattern); + + // Highest score beyond which we give up. + double score_threshold = Match_Threshold; + // Is there a nearby exact match? (speedup) + int best_loc = text.indexOf(pattern, loc); + if (best_loc != -1) { + score_threshold = Math.min(match_bitapScore(0, best_loc, loc, pattern), + score_threshold); + // What about in the other direction? (speedup) + best_loc = text.lastIndexOf(pattern, loc + pattern.length()); + if (best_loc != -1) { + score_threshold = Math.min(match_bitapScore(0, best_loc, loc, pattern), + score_threshold); + } + } + + // Initialise the bit arrays. + int matchmask = 1 << (pattern.length() - 1); + best_loc = -1; + + int bin_min, bin_mid; + int bin_max = pattern.length() + text.length(); + // Empty initialization added to appease Java compiler. + int[] last_rd = new int[0]; + for (int d = 0; d < pattern.length(); d++) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from 'loc' we can stray at + // this error level. + bin_min = 0; + bin_mid = bin_max; + while (bin_min < bin_mid) { + if (match_bitapScore(d, loc + bin_mid, loc, pattern) + <= score_threshold) { + bin_min = bin_mid; + } else { + bin_max = bin_mid; + } + bin_mid = (bin_max - bin_min) / 2 + bin_min; + } + // Use the result from this iteration as the maximum for the next. + bin_max = bin_mid; + int start = Math.max(1, loc - bin_mid + 1); + int finish = Math.min(loc + bin_mid, text.length()) + pattern.length(); + + int[] rd = new int[finish + 2]; + rd[finish + 1] = (1 << d) - 1; + for (int j = finish; j >= start; j--) { + int charMatch; + if (text.length() <= j - 1 || !s.containsKey(text.charAt(j - 1))) { + // Out of range. + charMatch = 0; + } else { + charMatch = s.get(text.charAt(j - 1)); + } + if (d == 0) { + // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; + } else { + // Subsequent passes: fuzzy match. + rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) + | (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1]; + } + if ((rd[j] & matchmask) != 0) { + double score = match_bitapScore(d, j - 1, loc, pattern); + // This match will almost certainly be better than any existing + // match. But check anyway. + if (score <= score_threshold) { + // Told you so. + score_threshold = score; + best_loc = j - 1; + if (best_loc > loc) { + // When passing loc, don't exceed our current distance from loc. + start = Math.max(1, 2 * loc - best_loc); + } else { + // Already passed loc, downhill from here on in. + break; + } + } + } + } + if (match_bitapScore(d + 1, loc, loc, pattern) > score_threshold) { + // No hope for a (better) match at greater error levels. + break; + } + last_rd = rd; + } + return best_loc; + } + + /** + * Compute and return the score for a match with e errors and x location. + * @param e Number of errors in match. + * @param x Location of match. + * @param loc Expected location of match. + * @param pattern Pattern being sought. + * @return Overall score for match (0.0 = good, 1.0 = bad). + */ + private double match_bitapScore(int e, int x, int loc, String pattern) { + float accuracy = (float) e / pattern.length(); + int proximity = Math.abs(loc - x); + if (Match_Distance == 0) { + // Dodge divide by zero error. + return proximity == 0 ? accuracy : 1.0; + } + return accuracy + (proximity / (float) Match_Distance); + } + + /** + * Initialise the alphabet for the Bitap algorithm. + * @param pattern The text to encode. + * @return Hash of character locations. + */ + protected Map match_alphabet(String pattern) { + Map s = new HashMap(); + char[] char_pattern = pattern.toCharArray(); + for (char c : char_pattern) { + s.put(c, 0); + } + int i = 0; + for (char c : char_pattern) { + s.put(c, s.get(c) | (1 << (pattern.length() - i - 1))); + i++; + } + return s; + } + + + // PATCH FUNCTIONS + + + /** + * Increase the context until it is unique, + * but don't let the pattern expand beyond Match_MaxBits. + * @param patch The patch to grow. + * @param text Source text. + */ + protected void patch_addContext(Patch patch, String text) { + if (text.length() == 0) { + return; + } + String pattern = text.substring(patch.start2, patch.start2 + patch.length1); + int padding = 0; + + // Look for the first and last matches of pattern in text. If two different + // matches are found, increase the pattern length. + while (text.indexOf(pattern) != text.lastIndexOf(pattern) + && pattern.length() < Match_MaxBits - Patch_Margin - Patch_Margin) { + padding += Patch_Margin; + pattern = text.substring(Math.max(0, patch.start2 - padding), + Math.min(text.length(), patch.start2 + patch.length1 + padding)); + } + // Add one chunk for good luck. + padding += Patch_Margin; + + // Add the prefix. + String prefix = text.substring(Math.max(0, patch.start2 - padding), + patch.start2); + if (prefix.length() != 0) { + patch.diffs.addFirst(new Diff(Operation.EQUAL, prefix)); + } + // Add the suffix. + String suffix = text.substring(patch.start2 + patch.length1, + Math.min(text.length(), patch.start2 + patch.length1 + padding)); + if (suffix.length() != 0) { + patch.diffs.addLast(new Diff(Operation.EQUAL, suffix)); + } + + // Roll back the start points. + patch.start1 -= prefix.length(); + patch.start2 -= prefix.length(); + // Extend the lengths. + patch.length1 += prefix.length() + suffix.length(); + patch.length2 += prefix.length() + suffix.length(); + } + + /** + * Compute a list of patches to turn text1 into text2. + * A set of diffs will be computed. + * @param text1 Old text. + * @param text2 New text. + * @return LinkedList of Patch objects. + */ + public LinkedList patch_make(String text1, String text2) { + if (text1 == null || text2 == null) { + throw new IllegalArgumentException("Null inputs. (patch_make)"); + } + // No diffs provided, compute our own. + LinkedList diffs = diff_main(text1, text2, true); + if (diffs.size() > 2) { + diff_cleanupSemantic(diffs); + diff_cleanupEfficiency(diffs); + } + return patch_make(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. + * text1 will be derived from the provided diffs. + * @param diffs Array of Diff objects for text1 to text2. + * @return LinkedList of Patch objects. + */ + public LinkedList patch_make(LinkedList diffs) { + if (diffs == null) { + throw new IllegalArgumentException("Null inputs. (patch_make)"); + } + // No origin string provided, compute our own. + String text1 = diff_text1(diffs); + return patch_make(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. + * text2 is ignored, diffs are the delta between text1 and text2. + * @param text1 Old text + * @param text2 Ignored. + * @param diffs Array of Diff objects for text1 to text2. + * @return LinkedList of Patch objects. + * @deprecated Prefer patch_make(String text1, LinkedList diffs). + */ + @Deprecated public LinkedList patch_make(String text1, String text2, + LinkedList diffs) { + return patch_make(text1, diffs); + } + + /** + * Compute a list of patches to turn text1 into text2. + * text2 is not provided, diffs are the delta between text1 and text2. + * @param text1 Old text. + * @param diffs Array of Diff objects for text1 to text2. + * @return LinkedList of Patch objects. + */ + public LinkedList patch_make(String text1, LinkedList diffs) { + if (text1 == null || diffs == null) { + throw new IllegalArgumentException("Null inputs. (patch_make)"); + } + + LinkedList patches = new LinkedList(); + if (diffs.isEmpty()) { + return patches; // Get rid of the null case. + } + Patch patch = new Patch(); + int char_count1 = 0; // Number of characters into the text1 string. + int char_count2 = 0; // Number of characters into the text2 string. + // Start with text1 (prepatch_text) and apply the diffs until we arrive at + // text2 (postpatch_text). We recreate the patches one by one to determine + // context info. + String prepatch_text = text1; + String postpatch_text = text1; + for (Diff aDiff : diffs) { + if (patch.diffs.isEmpty() && aDiff.operation != Operation.EQUAL) { + // A new patch starts here. + patch.start1 = char_count1; + patch.start2 = char_count2; + } + + switch (aDiff.operation) { + case INSERT: + patch.diffs.add(aDiff); + patch.length2 += aDiff.text.length(); + postpatch_text = postpatch_text.substring(0, char_count2) + + aDiff.text + postpatch_text.substring(char_count2); + break; + case DELETE: + patch.length1 += aDiff.text.length(); + patch.diffs.add(aDiff); + postpatch_text = postpatch_text.substring(0, char_count2) + + postpatch_text.substring(char_count2 + aDiff.text.length()); + break; + case EQUAL: + if (aDiff.text.length() <= 2 * Patch_Margin + && !patch.diffs.isEmpty() && aDiff != diffs.getLast()) { + // Small equality inside a patch. + patch.diffs.add(aDiff); + patch.length1 += aDiff.text.length(); + patch.length2 += aDiff.text.length(); + } + + if (aDiff.text.length() >= 2 * Patch_Margin && !patch.diffs.isEmpty()) { + // Time for a new patch. + if (!patch.diffs.isEmpty()) { + patch_addContext(patch, prepatch_text); + patches.add(patch); + patch = new Patch(); + // Unlike Unidiff, our patch lists have a rolling context. + // https://github.com/google/diff-match-patch/wiki/Unidiff + // Update prepatch text & pos to reflect the application of the + // just completed patch. + prepatch_text = postpatch_text; + char_count1 = char_count2; + } + } + break; + } + + // Update the current character count. + if (aDiff.operation != Operation.INSERT) { + char_count1 += aDiff.text.length(); + } + if (aDiff.operation != Operation.DELETE) { + char_count2 += aDiff.text.length(); + } + } + // Pick up the leftover patch if not empty. + if (!patch.diffs.isEmpty()) { + patch_addContext(patch, prepatch_text); + patches.add(patch); + } + + return patches; + } + + /** + * Given an array of patches, return another array that is identical. + * @param patches Array of Patch objects. + * @return Array of Patch objects. + */ + public LinkedList patch_deepCopy(LinkedList patches) { + LinkedList patchesCopy = new LinkedList(); + for (Patch aPatch : patches) { + Patch patchCopy = new Patch(); + for (Diff aDiff : aPatch.diffs) { + Diff diffCopy = new Diff(aDiff.operation, aDiff.text); + patchCopy.diffs.add(diffCopy); + } + patchCopy.start1 = aPatch.start1; + patchCopy.start2 = aPatch.start2; + patchCopy.length1 = aPatch.length1; + patchCopy.length2 = aPatch.length2; + patchesCopy.add(patchCopy); + } + return patchesCopy; + } + + /** + * Merge a set of patches onto the text. Return a patched text, as well + * as an array of true/false values indicating which patches were applied. + * @param patches Array of Patch objects + * @param text Old text. + * @return Two element Object array, containing the new text and an array of + * boolean values. + */ + public Object[] patch_apply(LinkedList patches, String text) { + if (patches.isEmpty()) { + return new Object[]{text, new boolean[0]}; + } + + // Deep copy the patches so that no changes are made to originals. + patches = patch_deepCopy(patches); + + String nullPadding = patch_addPadding(patches); + text = nullPadding + text + nullPadding; + patch_splitMax(patches); + + int x = 0; + // delta keeps track of the offset between the expected and actual location + // of the previous patch. If there are patches expected at positions 10 and + // 20, but the first patch was found at 12, delta is 2 and the second patch + // has an effective expected position of 22. + int delta = 0; + boolean[] results = new boolean[patches.size()]; + for (Patch aPatch : patches) { + int expected_loc = aPatch.start2 + delta; + String text1 = diff_text1(aPatch.diffs); + int start_loc; + int end_loc = -1; + if (text1.length() > this.Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern in the case of + // a monster delete. + start_loc = match_main(text, + text1.substring(0, this.Match_MaxBits), expected_loc); + if (start_loc != -1) { + end_loc = match_main(text, + text1.substring(text1.length() - this.Match_MaxBits), + expected_loc + text1.length() - this.Match_MaxBits); + if (end_loc == -1 || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = -1; + } + } + } else { + start_loc = match_main(text, text1, expected_loc); + } + if (start_loc == -1) { + // No match found. :( + results[x] = false; + // Subtract the delta for this failed patch from subsequent patches. + delta -= aPatch.length2 - aPatch.length1; + } else { + // Found a match. :) + results[x] = true; + delta = start_loc - expected_loc; + String text2; + if (end_loc == -1) { + text2 = text.substring(start_loc, + Math.min(start_loc + text1.length(), text.length())); + } else { + text2 = text.substring(start_loc, + Math.min(end_loc + this.Match_MaxBits, text.length())); + } + if (text1.equals(text2)) { + // Perfect match, just shove the replacement text in. + text = text.substring(0, start_loc) + diff_text2(aPatch.diffs) + + text.substring(start_loc + text1.length()); + } else { + // Imperfect match. Run a diff to get a framework of equivalent + // indices. + LinkedList diffs = diff_main(text1, text2, false); + if (text1.length() > this.Match_MaxBits + && diff_levenshtein(diffs) / (float) text1.length() + > this.Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + results[x] = false; + } else { + diff_cleanupSemanticLossless(diffs); + int index1 = 0; + for (Diff aDiff : aPatch.diffs) { + if (aDiff.operation != Operation.EQUAL) { + int index2 = diff_xIndex(diffs, index1); + if (aDiff.operation == Operation.INSERT) { + // Insertion + text = text.substring(0, start_loc + index2) + aDiff.text + + text.substring(start_loc + index2); + } else if (aDiff.operation == Operation.DELETE) { + // Deletion + text = text.substring(0, start_loc + index2) + + text.substring(start_loc + diff_xIndex(diffs, + index1 + aDiff.text.length())); + } + } + if (aDiff.operation != Operation.DELETE) { + index1 += aDiff.text.length(); + } + } + } + } + } + x++; + } + // Strip the padding off. + text = text.substring(nullPadding.length(), text.length() + - nullPadding.length()); + return new Object[]{text, results}; + } + + /** + * Add some padding on text start and end so that edges can match something. + * Intended to be called only from within patch_apply. + * @param patches Array of Patch objects. + * @return The padding string added to each side. + */ + public String patch_addPadding(LinkedList patches) { + short paddingLength = this.Patch_Margin; + String nullPadding = ""; + for (short x = 1; x <= paddingLength; x++) { + nullPadding += String.valueOf((char) x); + } + + // Bump all the patches forward. + for (Patch aPatch : patches) { + aPatch.start1 += paddingLength; + aPatch.start2 += paddingLength; + } + + // Add some padding on start of first diff. + Patch patch = patches.getFirst(); + LinkedList diffs = patch.diffs; + if (diffs.isEmpty() || diffs.getFirst().operation != Operation.EQUAL) { + // Add nullPadding equality. + diffs.addFirst(new Diff(Operation.EQUAL, nullPadding)); + patch.start1 -= paddingLength; // Should be 0. + patch.start2 -= paddingLength; // Should be 0. + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs.getFirst().text.length()) { + // Grow first equality. + Diff firstDiff = diffs.getFirst(); + int extraLength = paddingLength - firstDiff.text.length(); + firstDiff.text = nullPadding.substring(firstDiff.text.length()) + + firstDiff.text; + patch.start1 -= extraLength; + patch.start2 -= extraLength; + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + // Add some padding on end of last diff. + patch = patches.getLast(); + diffs = patch.diffs; + if (diffs.isEmpty() || diffs.getLast().operation != Operation.EQUAL) { + // Add nullPadding equality. + diffs.addLast(new Diff(Operation.EQUAL, nullPadding)); + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs.getLast().text.length()) { + // Grow last equality. + Diff lastDiff = diffs.getLast(); + int extraLength = paddingLength - lastDiff.text.length(); + lastDiff.text += nullPadding.substring(0, extraLength); + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + return nullPadding; + } + + /** + * Look through the patches and break up any which are longer than the + * maximum limit of the match algorithm. + * Intended to be called only from within patch_apply. + * @param patches LinkedList of Patch objects. + */ + public void patch_splitMax(LinkedList patches) { + short patch_size = Match_MaxBits; + String precontext, postcontext; + Patch patch; + int start1, start2; + boolean empty; + Operation diff_type; + String diff_text; + ListIterator pointer = patches.listIterator(); + Patch bigpatch = pointer.hasNext() ? pointer.next() : null; + while (bigpatch != null) { + if (bigpatch.length1 <= Match_MaxBits) { + bigpatch = pointer.hasNext() ? pointer.next() : null; + continue; + } + // Remove the big old patch. + pointer.remove(); + start1 = bigpatch.start1; + start2 = bigpatch.start2; + precontext = ""; + while (!bigpatch.diffs.isEmpty()) { + // Create one of several smaller patches. + patch = new Patch(); + empty = true; + patch.start1 = start1 - precontext.length(); + patch.start2 = start2 - precontext.length(); + if (precontext.length() != 0) { + patch.length1 = patch.length2 = precontext.length(); + patch.diffs.add(new Diff(Operation.EQUAL, precontext)); + } + while (!bigpatch.diffs.isEmpty() + && patch.length1 < patch_size - Patch_Margin) { + diff_type = bigpatch.diffs.getFirst().operation; + diff_text = bigpatch.diffs.getFirst().text; + if (diff_type == Operation.INSERT) { + // Insertions are harmless. + patch.length2 += diff_text.length(); + start2 += diff_text.length(); + patch.diffs.addLast(bigpatch.diffs.removeFirst()); + empty = false; + } else if (diff_type == Operation.DELETE && patch.diffs.size() == 1 + && patch.diffs.getFirst().operation == Operation.EQUAL + && diff_text.length() > 2 * patch_size) { + // This is a large deletion. Let it pass in one chunk. + patch.length1 += diff_text.length(); + start1 += diff_text.length(); + empty = false; + patch.diffs.add(new Diff(diff_type, diff_text)); + bigpatch.diffs.removeFirst(); + } else { + // Deletion or equality. Only take as much as we can stomach. + diff_text = diff_text.substring(0, Math.min(diff_text.length(), + patch_size - patch.length1 - Patch_Margin)); + patch.length1 += diff_text.length(); + start1 += diff_text.length(); + if (diff_type == Operation.EQUAL) { + patch.length2 += diff_text.length(); + start2 += diff_text.length(); + } else { + empty = false; + } + patch.diffs.add(new Diff(diff_type, diff_text)); + if (diff_text.equals(bigpatch.diffs.getFirst().text)) { + bigpatch.diffs.removeFirst(); + } else { + bigpatch.diffs.getFirst().text = bigpatch.diffs.getFirst().text + .substring(diff_text.length()); + } + } + } + // Compute the head context for the next patch. + precontext = diff_text2(patch.diffs); + precontext = precontext.substring(Math.max(0, precontext.length() + - Patch_Margin)); + // Append the end context for this patch. + if (diff_text1(bigpatch.diffs).length() > Patch_Margin) { + postcontext = diff_text1(bigpatch.diffs).substring(0, Patch_Margin); + } else { + postcontext = diff_text1(bigpatch.diffs); + } + if (postcontext.length() != 0) { + patch.length1 += postcontext.length(); + patch.length2 += postcontext.length(); + if (!patch.diffs.isEmpty() + && patch.diffs.getLast().operation == Operation.EQUAL) { + patch.diffs.getLast().text += postcontext; + } else { + patch.diffs.add(new Diff(Operation.EQUAL, postcontext)); + } + } + if (!empty) { + pointer.add(patch); + } + } + bigpatch = pointer.hasNext() ? pointer.next() : null; + } + } + + /** + * Take a list of patches and return a textual representation. + * @param patches List of Patch objects. + * @return Text representation of patches. + */ + public String patch_toText(List patches) { + StringBuilder text = new StringBuilder(); + for (Patch aPatch : patches) { + text.append(aPatch); + } + return text.toString(); + } + + /** + * Parse a textual representation of patches and return a List of Patch + * objects. + * @param textline Text representation of patches. + * @return List of Patch objects. + * @throws IllegalArgumentException If invalid input. + */ + public List patch_fromText(String textline) + throws IllegalArgumentException { + List patches = new LinkedList(); + if (textline.length() == 0) { + return patches; + } + List textList = Arrays.asList(textline.split("\n")); + LinkedList text = new LinkedList(textList); + Patch patch; + Pattern patchHeader + = Pattern.compile("^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@$"); + Matcher m; + char sign; + String line; + while (!text.isEmpty()) { + m = patchHeader.matcher(text.getFirst()); + if (!m.matches()) { + throw new IllegalArgumentException( + "Invalid patch string: " + text.getFirst()); + } + patch = new Patch(); + patches.add(patch); + patch.start1 = Integer.parseInt(m.group(1)); + if (m.group(2).length() == 0) { + patch.start1--; + patch.length1 = 1; + } else if (m.group(2).equals("0")) { + patch.length1 = 0; + } else { + patch.start1--; + patch.length1 = Integer.parseInt(m.group(2)); + } + + patch.start2 = Integer.parseInt(m.group(3)); + if (m.group(4).length() == 0) { + patch.start2--; + patch.length2 = 1; + } else if (m.group(4).equals("0")) { + patch.length2 = 0; + } else { + patch.start2--; + patch.length2 = Integer.parseInt(m.group(4)); + } + text.removeFirst(); + + while (!text.isEmpty()) { + try { + sign = text.getFirst().charAt(0); + } catch (IndexOutOfBoundsException e) { + // Blank line? Whatever. + text.removeFirst(); + continue; + } + line = text.getFirst().substring(1); + line = line.replace("+", "%2B"); // decode would change all "+" to " " + try { + line = URLDecoder.decode(line, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // Not likely on modern system. + throw new Error("This system does not support UTF-8.", e); + } catch (IllegalArgumentException e) { + // Malformed URI sequence. + throw new IllegalArgumentException( + "Illegal escape in patch_fromText: " + line, e); + } + if (sign == '-') { + // Deletion. + patch.diffs.add(new Diff(Operation.DELETE, line)); + } else if (sign == '+') { + // Insertion. + patch.diffs.add(new Diff(Operation.INSERT, line)); + } else if (sign == ' ') { + // Minor equality. + patch.diffs.add(new Diff(Operation.EQUAL, line)); + } else if (sign == '@') { + // Start of next patch. + break; + } else { + // WTF? + throw new IllegalArgumentException( + "Invalid patch mode '" + sign + "' in: " + line); + } + text.removeFirst(); + } + } + return patches; + } + + + /** + * Class representing one diff operation. + */ + public static class Diff { + /** + * One of: INSERT, DELETE or EQUAL. + */ + public Operation operation; + /** + * The text associated with this diff operation. + */ + public String text; + + /** + * Constructor. Initializes the diff with the provided values. + * @param operation One of INSERT, DELETE or EQUAL. + * @param text The text being applied. + */ + public Diff(Operation operation, String text) { + // Construct a diff with the specified operation and text. + this.operation = operation; + this.text = text; + } + + /** + * Display a human-readable version of this Diff. + * @return text version. + */ + public String toString() { + String prettyText = this.text.replace('\n', '\u00b6'); + return "Diff(" + this.operation + ",\"" + prettyText + "\")"; + } + + /** + * Create a numeric hash value for a Diff. + * This function is not used by DMP. + * @return Hash value. + */ + @Override + public int hashCode() { + final int prime = 31; + int result = (operation == null) ? 0 : operation.hashCode(); + result += prime * ((text == null) ? 0 : text.hashCode()); + return result; + } + + /** + * Is this Diff equivalent to another Diff? + * @param obj Another Diff to compare against. + * @return true or false. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Diff other = (Diff) obj; + if (operation != other.operation) { + return false; + } + if (text == null) { + if (other.text != null) { + return false; + } + } else if (!text.equals(other.text)) { + return false; + } + return true; + } + } + + + /** + * Class representing one patch operation. + */ + public static class Patch { + public LinkedList diffs; + public int start1; + public int start2; + public int length1; + public int length2; + + /** + * Constructor. Initializes with an empty list of diffs. + */ + public Patch() { + this.diffs = new LinkedList(); + } + + /** + * Emulate GNU diff's format. + * Header: @@ -382,8 +481,9 @@ + * Indices are printed as 1-based, not 0-based. + * @return The GNU diff string. + */ + public String toString() { + String coords1, coords2; + if (this.length1 == 0) { + coords1 = this.start1 + ",0"; + } else if (this.length1 == 1) { + coords1 = Integer.toString(this.start1 + 1); + } else { + coords1 = (this.start1 + 1) + "," + this.length1; + } + if (this.length2 == 0) { + coords2 = this.start2 + ",0"; + } else if (this.length2 == 1) { + coords2 = Integer.toString(this.start2 + 1); + } else { + coords2 = (this.start2 + 1) + "," + this.length2; + } + StringBuilder text = new StringBuilder(); + text.append("@@ -").append(coords1).append(" +").append(coords2) + .append(" @@\n"); + // Escape the body of the patch with %xx notation. + for (Diff aDiff : this.diffs) { + switch (aDiff.operation) { + case INSERT: + text.append('+'); + break; + case DELETE: + text.append('-'); + break; + case EQUAL: + text.append(' '); + break; + } + try { + text.append(URLEncoder.encode(aDiff.text, "UTF-8").replace('+', ' ')) + .append("\n"); + } catch (UnsupportedEncodingException e) { + // Not likely on modern system. + throw new Error("This system does not support UTF-8.", e); + } + } + return unescapeForEncodeUriCompatability(text.toString()); + } + } + + /** + * Unescape selected chars for compatability with JavaScript's encodeURI. + * In speed critical applications this could be dropped since the + * receiving application will certainly decode these fine. + * Note that this function is case-sensitive. Thus "%3f" would not be + * unescaped. But this is ok because it is only called with the output of + * URLEncoder.encode which returns uppercase hex. + * + * Example: "%3F" -> "?", "%24" -> "$", etc. + * + * @param str The string to escape. + * @return The escaped string. + */ + private static String unescapeForEncodeUriCompatability(String str) { + return str.replace("%21", "!").replace("%7E", "~") + .replace("%27", "'").replace("%28", "(").replace("%29", ")") + .replace("%3B", ";").replace("%2F", "/").replace("%3F", "?") + .replace("%3A", ":").replace("%40", "@").replace("%26", "&") + .replace("%3D", "=").replace("%2B", "+").replace("%24", "$") + .replace("%2C", ",").replace("%23", "#"); + } +} diff --git a/settings.gradle b/settings.gradle index 793f7a3426..d020abade4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx' +include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch' diff --git a/vector/build.gradle b/vector/build.gradle index d7f4a8b453..442876643e 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -229,6 +229,7 @@ dependencies { implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android-rx") + implementation project(":diff-match-patch") implementation 'com.android.support:multidex:1.0.3' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" @@ -341,8 +342,6 @@ dependencies { exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' } - implementation 'diff_match_patch:diff_match_patch:current' - implementation "androidx.emoji:emoji-appcompat:1.0.0" // TESTS From 8305ce67dd1136c382e914e261cbc512376a408a Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 11 Dec 2019 14:44:31 +0100 Subject: [PATCH 11/70] Aggregate Event References for DM verifications --- .../android/api/session/events/model/Event.kt | 8 + .../api/session/events/model/UnsignedData.kt | 14 ++ .../room/model/EventAnnotationsSummary.kt | 3 +- .../room/model/ReferencesAggregatedContent.kt | 31 ++++ .../room/model/ReferencesAggregatedSummary.kt | 30 ++++ .../model/message/MessageRelationContent.kt | 2 +- .../verification/SasTransportRoomMessage.kt | 7 + .../verification/SasTransportToDevice.kt | 1 - .../VerificationMessageLiveObserver.kt | 21 ++- .../mapper/EventAnnotationsSummaryMapper.kt | 18 +++ .../internal/database/mapper/EventMapper.kt | 2 + .../model/EventAnnotationsSummaryEntity.kt | 3 +- .../internal/database/model/EventEntity.kt | 2 + .../ReferencesAggregatedSummaryEntity.kt | 31 ++++ .../database/model/SessionRealmModule.kt | 1 + ...eferencesAggregatedSummaryEntityQueries.kt | 22 +++ .../android/internal/di/MatrixModule.kt | 9 +- .../room/EventRelationsAggregationTask.kt | 141 +++++++++++++++--- .../room/EventRelationsAggregationUpdater.kt | 6 + .../internal/session/sync/RoomSyncHandler.kt | 20 ++- .../android/internal/task/TaskExecutor.kt | 1 + .../android/internal/task/TaskThread.kt | 3 +- .../util/MatrixCoroutineDispatchers.kt | 3 +- 23 files changed, 336 insertions(+), 43 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedContent.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedSummary.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReferencesAggregatedSummaryEntity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index bc6885eddc..ab39dea178 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -85,6 +85,14 @@ data class Event( @Transient var sendState: SendState = SendState.UNKNOWN + /** + The `age` value transcoded in a timestamp based on the device clock when the SDK received + the event from the home server. + Unlike `age`, this value is static. + */ + @Transient + var ageLocalTs: Long? = null + /** * Check if event is a state event. * @return true if event is state event. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt index 57eaa7dc76..b179cb7a31 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt @@ -21,9 +21,23 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class UnsignedData( + /** + * The time in milliseconds that has elapsed since the event was sent. + * This field is generated by the local homeserver, and may be incorrect if the local time on at least one of the two servers + * is out of sync, which can cause the age to either be negative or greater than it actually is. + */ @Json(name = "age") val age: Long?, + /** + * Optional. The event that redacted this event, if any. + */ @Json(name = "redacted_because") val redactedEvent: Event? = null, + /** + * The client-supplied transaction ID, if the client being given the event is the same one which sent it. + */ @Json(name = "transaction_id") val transactionId: String? = null, + /** + * Optional. The previous content for this event. If there is no previous content, this key will be missing. + */ @Json(name = "prev_content") val prevContent: Map? = null, @Json(name = "m.relations") val relations: AggregatedRelations? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EventAnnotationsSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EventAnnotationsSummary.kt index 0d403be2f4..28edfcfe04 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EventAnnotationsSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EventAnnotationsSummary.kt @@ -18,5 +18,6 @@ package im.vector.matrix.android.api.session.room.model data class EventAnnotationsSummary( var eventId: String, var reactionsSummary: List, - var editSummary: EditAggregatedSummary? + var editSummary: EditAggregatedSummary?, + var referencesAggregatedSummary: ReferencesAggregatedSummary? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedContent.kt new file mode 100644 index 0000000000..ae6e52a091 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedContent.kt @@ -0,0 +1,31 @@ +/* + * 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 + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Contains an aggregated summary info of the references. + * Put pre-computed info that you want to access quickly without having + * to go through all references events + */ +@JsonClass(generateAdapter = true) +data class ReferencesAggregatedContent( + // Verification status info for m.key.verification.request msgType events + @Json(name = "verif_sum") val verificationSummary: String + // Add more fields for future summary info. +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedSummary.kt new file mode 100644 index 0000000000..fce166c37a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedSummary.kt @@ -0,0 +1,30 @@ +/* + * 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 + +import im.vector.matrix.android.api.session.events.model.Content + +/** + * Events can relates to other events, this object keeps a summary + * of all events that are referencing the 'eventId' event via the RelationType.REFERENCE + */ +class ReferencesAggregatedSummary( + val eventId: String, + val content: Content?, + val sourceEvents: List, + val localEchos: List +) + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageRelationContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageRelationContent.kt index ec773916fd..f65215e2bf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageRelationContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageRelationContent.kt @@ -21,6 +21,6 @@ import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) -internal data class MessageRelationContent( +data class MessageRelationContent( @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? ) 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 8e956fd60e..e40e8be31f 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 @@ -29,6 +29,7 @@ import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTas 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 timber.log.Timber import javax.inject.Inject @@ -56,6 +57,8 @@ internal class SasTransportRoomMessage( cryptoService ) ) { + callbackThread = TaskThread.DM_VERIF + executionThread = TaskThread.DM_VERIF constraints = TaskConstraints(true) callback = object : MatrixCallback { override fun onSuccess(data: SendResponse) { @@ -86,6 +89,8 @@ internal class SasTransportRoomMessage( cryptoService ) ) { + callbackThread = TaskThread.DM_VERIF + executionThread = TaskThread.DM_VERIF constraints = TaskConstraints(true) retryCount = 3 callback = object : MatrixCallback { @@ -115,6 +120,8 @@ internal class SasTransportRoomMessage( cryptoService ) ) { + callbackThread = TaskThread.DM_VERIF + executionThread = TaskThread.DM_VERIF constraints = TaskConstraints(true) retryCount = 3 } 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 bce23de1cd..85e9099972 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 @@ -61,7 +61,6 @@ internal class SasTransportToDevice( override fun onFailure(failure: Throwable) { Timber.e("## SAS verification [$tx.transactionId] failed to send toDevice in state : $tx.state") - tx.cancel(onErrorReason) } } 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 9a9cf9a420..e7a38e8908 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 @@ -70,10 +70,12 @@ internal class VerificationMessageLiveObserver @Inject constructor( } .toList() - // TODO use age also, ignore initial sync or back pagination? + // TODO ignore initial sync or back pagination? + val now = System.currentTimeMillis() - val tooInThePast = now - (10 * 60 * 1000 * 1000) - val tooInTheFuture = System.currentTimeMillis() + (5 * 60 * 1000 * 1000) + 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}") @@ -81,11 +83,18 @@ internal class VerificationMessageLiveObserver @Inject constructor( // 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 eventOrigin = event.originServerTs ?: -1 - if (eventOrigin < tooInThePast || eventOrigin > tooInTheFuture) { - Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is out of time ^^") + 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? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt index 701d35926a..ccdb8fb91f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt @@ -19,9 +19,11 @@ package im.vector.matrix.android.internal.database.mapper import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary +import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedSummary import im.vector.matrix.android.internal.database.model.EditAggregatedSummaryEntity import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntity +import im.vector.matrix.android.internal.database.model.ReferencesAggregatedSummaryEntity import io.realm.RealmList internal object EventAnnotationsSummaryMapper { @@ -45,6 +47,14 @@ internal object EventAnnotationsSummaryMapper { it.sourceLocalEchoEvents.toList(), it.lastEditTs ) + }, + referencesAggregatedSummary = annotationsSummary.referencesSummaryEntity?.let { + ReferencesAggregatedSummary( + it.eventId, + ContentMapper.map(it.content), + it.sourceEvents.toList(), + it.sourceLocalEcho.toList() + ) } ) } @@ -75,6 +85,14 @@ internal object EventAnnotationsSummaryMapper { }) } } + eventAnnotationsSummaryEntity.referencesSummaryEntity = annotationsSummary.referencesAggregatedSummary?.let { + ReferencesAggregatedSummaryEntity( + it.eventId, + ContentMapper.map(it.content), + RealmList().apply { addAll(it.sourceEvents) }, + RealmList().apply { addAll(it.localEchos) } + ) + } return eventAnnotationsSummaryEntity } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt index ed5f04ef75..faac57e486 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt @@ -43,6 +43,7 @@ internal object EventMapper { eventEntity.redacts = event.redacts eventEntity.age = event.unsignedData?.age ?: event.originServerTs eventEntity.unsignedData = uds + eventEntity.ageLocalTs = event.ageLocalTs return eventEntity } @@ -70,6 +71,7 @@ internal object EventMapper { unsignedData = ud, redacts = eventEntity.redacts ).also { + it.ageLocalTs = eventEntity.ageLocalTs it.sendState = eventEntity.sendState eventEntity.decryptionResultJson?.let { json -> try { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventAnnotationsSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventAnnotationsSummaryEntity.kt index 523d94b770..1a4f72f0b1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventAnnotationsSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventAnnotationsSummaryEntity.kt @@ -24,7 +24,8 @@ internal open class EventAnnotationsSummaryEntity( var eventId: String = "", var roomId: String? = null, var reactionsSummary: RealmList = RealmList(), - var editSummary: EditAggregatedSummaryEntity? = null + var editSummary: EditAggregatedSummaryEntity? = null, + var referencesSummaryEntity: ReferencesAggregatedSummaryEntity? = null ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt index 4def7aec5d..9e3ed6a93b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt @@ -59,6 +59,8 @@ internal open class EventEntity(@Index var eventId: String = "", sendStateStr = value.name } + var ageLocalTs: Long? = null + companion object @LinkingObjects("untimelinedStateEvents") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReferencesAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReferencesAggregatedSummaryEntity.kt new file mode 100644 index 0000000000..1c3ea70e52 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReferencesAggregatedSummaryEntity.kt @@ -0,0 +1,31 @@ +/* + * 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.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +internal open class ReferencesAggregatedSummaryEntity( + var eventId: String = "", + var content: String? = null, + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + var sourceEvents: RealmList = RealmList(), + // List of transaction ids for local echos + var sourceLocalEcho: RealmList = RealmList() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index 6059d3faf7..4a93819027 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -38,6 +38,7 @@ import io.realm.annotations.RealmModule IgnoredUserEntity::class, BreadcrumbsEntity::class, EventAnnotationsSummaryEntity::class, + ReferencesAggregatedSummaryEntity::class, ReactionAggregatedSummaryEntity::class, EditAggregatedSummaryEntity::class, PushRulesEntity::class, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt new file mode 100644 index 0000000000..9c7547b5e1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt @@ -0,0 +1,22 @@ +package im.vector.matrix.android.internal.database.query + +import im.vector.matrix.android.internal.database.model.ReferencesAggregatedSummaryEntity +import im.vector.matrix.android.internal.database.model.ReferencesAggregatedSummaryEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun ReferencesAggregatedSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { + val query = realm.where() + query.equalTo(ReferencesAggregatedSummaryEntityFields.EVENT_ID, eventId) + return query +} + +internal fun ReferencesAggregatedSummaryEntity.Companion.create(realm: Realm, txID: String): ReferencesAggregatedSummaryEntity { + return realm.createObject(ReferencesAggregatedSummaryEntity::class.java).apply { + this.eventId = txID + } +} + + + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt index c17864b82b..1cf0964e50 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt @@ -36,10 +36,11 @@ internal object MatrixModule { @MatrixScope fun providesMatrixCoroutineDispatchers(): MatrixCoroutineDispatchers { return MatrixCoroutineDispatchers(io = Dispatchers.IO, - computation = Dispatchers.Default, - main = Dispatchers.Main, - crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher(), - sync = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + computation = Dispatchers.Default, + main = Dispatchers.Main, + crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher(), + sync = Executors.newSingleThreadExecutor().asCoroutineDispatcher(), + dmVerif = Executors.newSingleThreadExecutor().asCoroutineDispatcher() ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt index 3d7c5df5fc..5c38fcf797 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -19,7 +19,9 @@ 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.* +import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedContent import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent import im.vector.matrix.android.api.session.room.model.relation.ReactionContent import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent @@ -42,6 +44,14 @@ internal interface EventRelationsAggregationTask : Task { + EventType.REACTION -> { // we got a reaction!! Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") handleReaction(event, roomId, realm, userId, isLocalEcho) } - EventType.MESSAGE -> { + EventType.MESSAGE -> { if (event.unsignedData?.relations?.annotations != null) { Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) @@ -99,33 +109,49 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( } } - EventType.ENCRYPTED -> { + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_KEY -> { + Timber.v("## SAS REF in room $roomId for event ${event.eventId}") + event.content.toModel()?.relatesTo?.let { + if (it.type == RelationType.REFERENCE && it.eventId != null) { + handleVerification(realm, event, roomId, isLocalEcho, it.eventId, userId) + } + } + } + + EventType.ENCRYPTED -> { // Relation type is in clear val encryptedEventContent = event.content.toModel() if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE) { // we need to decrypt if needed - if (event.mxDecryptionResult == null) { - try { - val result = cryptoService.decryptEvent(event, event.roomId) - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - } catch (e: MXCryptoError) { - Timber.w("Failed to decrypt e2e replace") - // TODO -> we should keep track of this and retry, or aggregation will be broken - } - } + decryptIfNeeded(event) event.getClearContent().toModel()?.let { Timber.v("###REPLACE in room $roomId for event ${event.eventId}") // A replace! handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) } + } else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) { + decryptIfNeeded(event) + when (event.getClearType()) { + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_KEY -> { + Timber.v("## SAS REF in room $roomId for event ${event.eventId}") + encryptedEventContent.relatesTo.eventId?.let { + handleVerification(realm, event, roomId, isLocalEcho, it, userId) + } + } + } } } - EventType.REDACTION -> { + EventType.REDACTION -> { val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } ?: return@forEach when (eventToPrune.type) { @@ -145,7 +171,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( } } } - else -> Timber.v("UnHandled event ${event.eventId}") + else -> Timber.v("UnHandled event ${event.eventId}") } } catch (t: Throwable) { Timber.e(t, "## Should not happen ") @@ -153,6 +179,23 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( } } + private fun decryptIfNeeded(event: Event) { + if (event.mxDecryptionResult == null) { + try { + val result = cryptoService.decryptEvent(event, event.roomId ?: "") + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } catch (e: MXCryptoError) { + Timber.w("Failed to decrypt e2e replace") + // TODO -> we should keep track of this and retry, or aggregation will be broken + } + } + } + private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean, relatedEventId: String? = null) { val eventId = event.eventId ?: return val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return @@ -228,7 +271,8 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( val eventSummary = EventAnnotationsSummaryEntity.create(realm, roomId, eventId) val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) sum.key = it.key - sum.firstTimestamp = event.originServerTs ?: 0 // TODO how to maintain order? + sum.firstTimestamp = event.originServerTs + ?: 0 // TODO how to maintain order? sum.count = it.count eventSummary.reactionsSummary.add(sum) } else { @@ -374,4 +418,61 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( Timber.e("## Cannot find summary for key $reactionKey") } } + + private fun handleVerification(realm: Realm, event: Event, roomId: String, isLocalEcho: Boolean, relatedEventId: String, userId: String) { + val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventId).findFirst() + ?: EventAnnotationsSummaryEntity.create(realm, roomId, relatedEventId).apply { this.roomId = roomId } + + val verifSummary = eventSummary.referencesSummaryEntity + ?: ReferencesAggregatedSummaryEntity.create(realm, relatedEventId).also { + eventSummary.referencesSummaryEntity = it + } + + val txId = event.unsignedData?.transactionId + + if (!isLocalEcho && verifSummary.sourceLocalEcho.contains(txId)) { + // ok it has already been handled + } else { + ContentMapper.map(verifSummary.content)?.toModel() + var data = ContentMapper.map(verifSummary.content)?.toModel() + ?: ReferencesAggregatedContent(VerificationState.REQUEST.name) + // TODO ignore invalid messages? e.g a START after a CANCEL? + // i.e. never change state if already canceled/done + val newState = when (event.getClearType()) { + EventType.KEY_VERIFICATION_START -> { + VerificationState.WAITING + } + EventType.KEY_VERIFICATION_ACCEPT -> { + VerificationState.WAITING + } + EventType.KEY_VERIFICATION_KEY -> { + VerificationState.WAITING + } + EventType.KEY_VERIFICATION_MAC -> { + VerificationState.WAITING + } + EventType.KEY_VERIFICATION_CANCEL -> { + if (event.senderId == userId) { + VerificationState.CANCELED_BY_ME + } else VerificationState.CANCELED_BY_OTHER + } + EventType.KEY_VERIFICATION_DONE -> { + VerificationState.DONE + } + else -> VerificationState.REQUEST + } + + data = data.copy(verificationSummary = newState.name) + verifSummary.content = ContentMapper.map(data.toContent()) + } + + if (isLocalEcho) { + verifSummary.sourceLocalEcho.add(event.eventId) + } else { + if (verifSummary.sourceLocalEcho.contains(txId)) { + verifSummary.sourceLocalEcho.remove(txId) + } + verifSummary.sourceEvents.add(event.eventId) + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt index aadf1bfccf..916430877e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt @@ -48,6 +48,12 @@ internal class EventRelationsAggregationUpdater @Inject constructor(@SessionData EventType.MESSAGE, EventType.REDACTION, EventType.REACTION, + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_KEY, EventType.ENCRYPTED) ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 4a003eb7d9..649de30339 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -93,14 +93,15 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch // PRIVATE METHODS ***************************************************************************** private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy, isInitialSync: Boolean, reporter: DefaultInitialSyncProgressService?) { + val syncLocalTimeStampMillis = System.currentTimeMillis() val rooms = when (handlingStrategy) { is HandlingStrategy.JOINED -> handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_joined_rooms, 0.6f) { - handleJoinedRoom(realm, it.key, it.value, isInitialSync) + handleJoinedRoom(realm, it.key, it.value, isInitialSync, syncLocalTimeStampMillis) } is HandlingStrategy.INVITED -> handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_invited_rooms, 0.1f) { - handleInvitedRoom(realm, it.key, it.value) + handleInvitedRoom(realm, it.key, it.value, syncLocalTimeStampMillis) } is HandlingStrategy.LEFT -> { @@ -115,7 +116,8 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch private fun handleJoinedRoom(realm: Realm, roomId: String, roomSync: RoomSync, - isInitialSync: Boolean): RoomEntity { + isInitialSync: Boolean, + syncLocalTimestampMillis: Long): RoomEntity { Timber.v("Handle join sync for room $roomId") if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) { @@ -154,7 +156,8 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch roomEntity, roomSync.timeline.events, roomSync.timeline.prevToken, - roomSync.timeline.limited + roomSync.timeline.limited, + syncLocalTimestampMillis ) roomEntity.addOrUpdate(chunkEntity) } @@ -170,12 +173,13 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch private fun handleInvitedRoom(realm: Realm, roomId: String, - roomSync: InvitedRoomSync): RoomEntity { + roomSync: InvitedRoomSync, + syncLocalTimestampMillis: Long): RoomEntity { Timber.v("Handle invited sync for room $roomId") val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) roomEntity.membership = Membership.INVITE if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) { - val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events) + val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events, syncLocalTimestampMillis = syncLocalTimestampMillis) roomEntity.addOrUpdate(chunkEntity) } val hasRoomMember = roomSync.inviteState?.events?.firstOrNull { @@ -200,7 +204,8 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch roomEntity: RoomEntity, eventList: List, prevToken: String? = null, - isLimited: Boolean = true): ChunkEntity { + isLimited: Boolean = true, + syncLocalTimestampMillis: Long): ChunkEntity { val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId) var stateIndexOffset = 0 val chunkEntity = if (!isLimited && lastChunk != null) { @@ -216,6 +221,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch val eventIds = ArrayList(eventList.size) for (event in eventList) { + event.ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } event.eventId?.also { eventIds.add(it) } chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset) // Give info to crypto module diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt index d5392779d1..fefd21b2cf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt @@ -86,5 +86,6 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers TaskThread.CALLER -> EmptyCoroutineContext TaskThread.CRYPTO -> coroutineDispatchers.crypto TaskThread.SYNC -> coroutineDispatchers.sync + TaskThread.DM_VERIF -> coroutineDispatchers.dmVerif } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskThread.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskThread.kt index 16ed93662c..4c24f30506 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskThread.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskThread.kt @@ -22,5 +22,6 @@ internal enum class TaskThread { IO, CALLER, CRYPTO, - SYNC + SYNC, + DM_VERIF } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/MatrixCoroutineDispatchers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/MatrixCoroutineDispatchers.kt index 23201c084e..ce43c5ea77 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/MatrixCoroutineDispatchers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/MatrixCoroutineDispatchers.kt @@ -23,5 +23,6 @@ internal data class MatrixCoroutineDispatchers( val computation: CoroutineDispatcher, val main: CoroutineDispatcher, val crypto: CoroutineDispatcher, - val sync: CoroutineDispatcher + val sync: CoroutineDispatcher, + val dmVerif: CoroutineDispatcher ) From 02f03e6b2330b12ef1e9f9e173de57fad20e206c Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 11 Dec 2019 16:00:53 +0100 Subject: [PATCH 12/70] Fix test compilation --- .../vector/matrix/android/SingleThreadCoroutineDispatcher.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/SingleThreadCoroutineDispatcher.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/SingleThreadCoroutineDispatcher.kt index e63123f3b3..fb1faa92d4 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/SingleThreadCoroutineDispatcher.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/SingleThreadCoroutineDispatcher.kt @@ -18,5 +18,8 @@ package im.vector.matrix.android import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.asCoroutineDispatcher +import java.util.concurrent.Executors -internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main, Main) +internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main, Main, + Executors.newSingleThreadExecutor().asCoroutineDispatcher()) From 0776a301ea5f1f80e9736038ebbc04960d95565a Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 11 Dec 2019 16:48:33 +0100 Subject: [PATCH 13/70] Incoming DM verification handling in timeline --- .../room/model/ReferencesAggregatedSummary.kt | 1 - ...eferencesAggregatedSummaryEntityQueries.kt | 3 - .../home/room/detail/RoomDetailAction.kt | 3 + .../home/room/detail/RoomDetailFragment.kt | 4 + .../home/room/detail/RoomDetailViewModel.kt | 18 ++ .../timeline/TimelineEventController.kt | 4 + .../action/MessageActionsViewModel.kt | 7 +- .../timeline/factory/EncryptionItemFactory.kt | 7 +- .../timeline/factory/MessageItemFactory.kt | 69 ++++++- .../timeline/factory/TimelineItemFactory.kt | 9 +- .../factory/VerificationItemFactory.kt | 116 ++++++++++++ .../timeline/format/NoticeEventFormatter.kt | 6 + .../helper/MessageInformationDataFactory.kt | 16 +- .../timeline/item/AbsBaseMessageItem.kt | 142 ++++++++++++++ .../detail/timeline/item/AbsMessageItem.kt | 98 ++-------- .../timeline/item/MessageInformationData.kt | 10 +- .../item/VerificationRequestConclusionItem.kt | 77 ++++++++ .../timeline/item/VerificationRequestItem.kt | 177 ++++++++++++++++++ ...button_destructive_background_selector.xml | 5 + ...button_destructive_text_color_selector.xml | 5 + .../button_positive_background_selector.xml | 5 + .../button_positive_text_color_selector.xml | 5 + .../src/main/res/drawable/ic_shield_black.xml | 14 ++ .../main/res/drawable/ic_shield_trusted.xml | 18 ++ .../res/drawable/rounded_rect_shape_8.xml | 11 ++ .../layout/item_timeline_event_base_state.xml | 95 ++++++++++ ..._timeline_event_verification_done_stub.xml | 36 ++++ .../item_timeline_event_verification_stub.xml | 68 +++++++ vector/src/main/res/values/colors_riot.xml | 3 + vector/src/main/res/values/colors_riotx.xml | 11 ++ vector/src/main/res/values/strings_riotX.xml | 10 + vector/src/main/res/values/styles_riot.xml | 17 ++ 32 files changed, 963 insertions(+), 107 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt create mode 100644 vector/src/main/res/color/button_destructive_background_selector.xml create mode 100644 vector/src/main/res/color/button_destructive_text_color_selector.xml create mode 100644 vector/src/main/res/color/button_positive_background_selector.xml create mode 100644 vector/src/main/res/color/button_positive_text_color_selector.xml create mode 100644 vector/src/main/res/drawable/ic_shield_black.xml create mode 100644 vector/src/main/res/drawable/ic_shield_trusted.xml create mode 100644 vector/src/main/res/drawable/rounded_rect_shape_8.xml create mode 100644 vector/src/main/res/layout/item_timeline_event_base_state.xml create mode 100644 vector/src/main/res/layout/item_timeline_event_verification_done_stub.xml create mode 100644 vector/src/main/res/layout/item_timeline_event_verification_stub.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedSummary.kt index fce166c37a..ca9d81cba1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedSummary.kt @@ -27,4 +27,3 @@ class ReferencesAggregatedSummary( val sourceEvents: List, val localEchos: List ) - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt index 9c7547b5e1..88f127066d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt @@ -17,6 +17,3 @@ internal fun ReferencesAggregatedSummaryEntity.Companion.create(realm: Realm, tx this.eventId = txID } } - - - 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 c1743ae3fc..5d00b09204 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 @@ -63,4 +63,7 @@ sealed class RoomDetailAction : VectorViewModelAction { object ClearSendQueue : RoomDetailAction() object ResendAll : RoomDetailAction() + + data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String, val otherdDeviceId: String) : RoomDetailAction() + data class DeclineVerificationRequest(val transactionId: 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 80f54a9c1f..9cc8eabe58 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 @@ -1024,6 +1024,10 @@ class RoomDetailFragment @Inject constructor( .show(requireActivity().supportFragmentManager, "DISPLAY_EDITS") } + override fun onTimelineItemAction(itemAction: RoomDetailAction) { + roomDetailViewModel.handle(itemAction) + } + override fun onRoomCreateLinkClicked(url: String) { permalinkHandler.launch(requireContext(), url, object : NavigateToRoomInterceptor { override fun navToRoom(roomId: String, eventId: String?): Boolean { 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 c1d3f4ce4a..3ce27be63a 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 @@ -48,6 +48,7 @@ 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 @@ -177,6 +178,8 @@ 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) } } @@ -786,6 +789,21 @@ 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 + ) + } + + private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) { + Timber.e("TODO implement $action") + } + private fun observeSyncState() { session.rx() .liveSyncState() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 576b9fa0ba..fe1a681480 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.epoxy.LoadingItem_ import im.vector.riotx.core.extensions.localDateTime +import im.vector.riotx.features.home.room.detail.RoomDetailAction import im.vector.riotx.features.home.room.detail.RoomDetailViewState import im.vector.riotx.features.home.room.detail.UnreadState import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory @@ -62,6 +63,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) fun onEditedDecorationClicked(informationData: MessageInformationData) + + // TODO move all callbacks to this? + fun onTimelineItemAction(itemAction: RoomDetailAction) } interface ReactionPillCallback { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 102412948b..e8047c2b06 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -23,10 +23,7 @@ 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.isTextMessage import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.message.MessageContent -import im.vector.matrix.android.api.session.room.model.message.MessageImageContent -import im.vector.matrix.android.api.session.room.model.message.MessageTextContent -import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent @@ -172,6 +169,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { eventHtmlRenderer.get().render(messageContent.formattedBody ?: messageContent.body) + } else if (messageContent is MessageVerificationRequestContent) { + stringProvider.getString(R.string.verification_request) } else { messageContent?.body } 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 3f234fcd3e..29b01120d1 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 @@ -17,6 +17,7 @@ package im.vector.riotx.features.home.room.detail.timeline.factory import android.view.View +import im.vector.matrix.android.api.session.Session 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 @@ -34,7 +35,8 @@ import javax.inject.Inject class EncryptionItemFactory @Inject constructor(private val stringProvider: StringProvider, private val avatarRenderer: AvatarRenderer, - private val avatarSizeProvider: AvatarSizeProvider) { + private val avatarSizeProvider: AvatarSizeProvider, + private val session: Session) { fun create(event: TimelineEvent, highlight: Boolean, @@ -46,7 +48,8 @@ class EncryptionItemFactory @Inject constructor(private val stringProvider: Stri sendState = event.root.sendState, avatarUrl = event.senderAvatar, memberName = event.getDisambiguatedDisplayName(), - showInformation = false + showInformation = false, + sentByMe = event.root.senderId == session.myUserId ) val attributes = NoticeItem.Attributes( avatarRenderer = avatarRenderer, 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 9c96f17022..93d5ab3789 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 @@ -24,6 +24,7 @@ import android.text.style.ClickableSpan import android.text.style.ForegroundColorSpan import android.view.View import dagger.Lazy +import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.* @@ -64,7 +65,8 @@ class MessageItemFactory @Inject constructor( private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, private val defaultItemFactory: DefaultItemFactory, private val noticeItemFactory: NoticeItemFactory, - private val avatarSizeProvider: AvatarSizeProvider) { + private val avatarSizeProvider: AvatarSizeProvider, + private val session: Session) { fun create(event: TimelineEvent, nextEvent: TimelineEvent?, @@ -97,14 +99,15 @@ class MessageItemFactory @Inject constructor( // val all = event.root.toContent() // val ev = all.toModel() return when (messageContent) { - is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) - is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes) - else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback) + is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) + is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) + else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback) } } @@ -128,6 +131,51 @@ class MessageItemFactory @Inject constructor( })) } + private fun buildVerificationRequestMessageItem(messageContent: MessageVerificationRequestContent, + @Suppress("UNUSED_PARAMETER") + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): VerificationRequestItem? { + // If this request is not sent by me or sent to me, we should ignore it in timeline + val myUserId = session.myUserId + if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) { + return null + } + + val otherUserId = if (informationData.sentByMe) messageContent.toUserId else informationData.senderId + val otherUserName = if (informationData.sentByMe) session.getUser(messageContent.toUserId)?.displayName + else informationData.memberName + 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 + ) + ) + .callback(callback) +// .izLocalFile(messageContent.getFileUrl().isLocalFile()) +// .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) +// .filename(messageContent.body) +// .iconRes(R.drawable.filetype_audio) +// .clickListener( +// DebouncedClickListener(View.OnClickListener { +// callback?.onAudioMessageClicked(messageContent) +// })) + } + private fun buildFileMessageItem(messageContent: MessageFileContent, informationData: MessageInformationData, highlight: Boolean, @@ -193,7 +241,8 @@ class MessageItemFactory @Inject constructor( val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val thumbnailData = ImageContentRenderer.Data( filename = messageContent.body, - url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl, + url = messageContent.videoInfo?.thumbnailFile?.url + ?: messageContent.videoInfo?.thumbnailUrl, elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), height = messageContent.videoInfo?.height, maxHeight = maxHeight, 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 a705576234..3b0c2c2bb7 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 @@ -28,7 +28,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me private val encryptedItemFactory: EncryptedItemFactory, private val noticeItemFactory: NoticeItemFactory, private val defaultItemFactory: DefaultItemFactory, - private val roomCreateItemFactory: RoomCreateItemFactory) { + private val roomCreateItemFactory: RoomCreateItemFactory, + private val verificationConclusionItemFactory: VerificationItemFactory) { fun create(event: TimelineEvent, nextEvent: TimelineEvent?, @@ -66,13 +67,15 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me } EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_START, - EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_MAC -> { // These events are filtered from timeline in normal case // Only visible in developer mode - defaultItemFactory.create(event, highlight, callback) + noticeItemFactory.create(event, highlight, callback) + } + EventType.KEY_VERIFICATION_DONE -> { + verificationConclusionItemFactory.create(event, highlight, callback) } // Unhandled event types (yet) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt new file mode 100644 index 0000000000..56284b6777 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt @@ -0,0 +1,116 @@ +/* + * 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.home.room.detail.timeline.factory + +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.RelationType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent +import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.session.room.VerificationState +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.resources.UserPreferencesProvider +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider +import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory +import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory +import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestConclusionItem +import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestConclusionItem_ +import javax.inject.Inject + +/** + * Can creates verification conclusion items + * Notice that not all KEY_VERIFICATION_DONE will be displayed in timeline, + * several checks are made to see if this conclusion is attached to a known request + */ +class VerificationItemFactory @Inject constructor( + private val colorProvider: ColorProvider, + private val messageInformationDataFactory: MessageInformationDataFactory, + private val messageItemAttributesFactory: MessageItemAttributesFactory, + private val avatarSizeProvider: AvatarSizeProvider, + private val noticeItemFactory: NoticeItemFactory, + private val userPreferencesProvider: UserPreferencesProvider, + private val session: Session +) { + + fun create(event: TimelineEvent, + highlight: Boolean, + callback: TimelineEventController.Callback? + ): VectorEpoxyModel<*>? { + if (event.root.eventId == null) return null + + val relContent: MessageRelationContent = event.root.content.toModel() + ?: event.root.getClearContent().toModel() + ?: return ignoredConclusion(event, highlight, callback) + + if (relContent.relatesTo?.type != RelationType.REFERENCE) return ignoredConclusion(event, highlight, callback) + val refEventId = relContent.relatesTo?.eventId + ?: return ignoredConclusion(event, highlight, callback) + + // If we cannot find the referenced request we do not display the done event + val refEvent = session.getRoom(event.root.roomId ?: "")?.getTimeLineEvent(refEventId) + ?: return ignoredConclusion(event, highlight, callback) + + // If it's not a request ignore this event + if (refEvent.root.getClearContent().toModel() == null) return ignoredConclusion(event, highlight, callback) + + // Is the request referenced is actually really completed? + val referenceInformationData = messageInformationDataFactory.create(refEvent, null) + if (referenceInformationData.referencesInfoData?.verificationStatus != VerificationState.DONE) return ignoredConclusion(event, highlight, callback) + + val informationData = messageInformationDataFactory.create(event, null) + val attributes = messageItemAttributesFactory.create(null, informationData, callback) + + when (event.root.getClearType()) { + EventType.KEY_VERIFICATION_DONE -> { + // We only tale the one sent by me + if (informationData.sentByMe) { + // We only display the done sent by the other user, the done send by me is ignored + return ignoredConclusion(event, highlight, callback) + } + return VerificationRequestConclusionItem_() + .attributes( + VerificationRequestConclusionItem.Attributes( + toUserId = informationData.senderId, + toUserName = informationData.memberName.toString(), + informationData = informationData, + avatarRenderer = attributes.avatarRenderer, + colorProvider = colorProvider, + emojiTypeFace = attributes.emojiTypeFace, + itemClickListener = attributes.itemClickListener, + itemLongClickListener = attributes.itemLongClickListener, + reactionPillCallback = attributes.reactionPillCallback, + readReceiptsCallback = attributes.readReceiptsCallback + ) + ) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) + } + } + return null + } + + private fun ignoredConclusion(event: TimelineEvent, + highlight: Boolean, + callback: TimelineEventController.Callback? + ): VectorEpoxyModel<*>? { + if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, callback) + return null + } +} 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 75100e6c03..f5253a9a28 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 @@ -44,6 +44,12 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.MESSAGE, EventType.REACTION, + EventType.KEY_VERIFICATION_START, + EventType.KEY_VERIFICATION_CANCEL, + EventType.KEY_VERIFICATION_ACCEPT, + EventType.KEY_VERIFICATION_MAC, + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_KEY, 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 784a180d00..5e29c8db67 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 @@ -20,15 +20,19 @@ package im.vector.riotx.features.home.room.detail.timeline.helper 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.toModel +import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited +import im.vector.matrix.android.internal.session.room.VerificationState +import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.features.home.getColorFromUserId -import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData +import im.vector.riotx.features.home.room.detail.timeline.item.ReferencesInfoData import me.gujun.android.span.span import javax.inject.Inject @@ -86,7 +90,15 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses .map { ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs) } - .toList() + .toList(), + referencesInfoData = event.annotations?.referencesAggregatedSummary?.let { referencesAggregatedSummary -> + val stateStr = referencesAggregatedSummary.content.toModel()?.verificationSummary + ReferencesInfoData( + VerificationState.values().firstOrNull { stateStr == it.name } + ?: VerificationState.REQUEST + ) + }, + sentByMe = event.root.senderId == session.myUserId ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt new file mode 100644 index 0000000000..6d99bb2650 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt @@ -0,0 +1,142 @@ +/* + * 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.home.room.detail.timeline.item + +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.IdRes +import androidx.core.view.isVisible +import im.vector.matrix.android.api.session.room.send.SendState +import im.vector.riotx.R +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.utils.DebouncedClickListener +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import im.vector.riotx.features.reactions.widget.ReactionButton +import im.vector.riotx.features.ui.getMessageTextColor + +/** + * Base timeline item with reactions and read receipts. + * Manages associated click listeners and send status. + * Should not be used as this, use a subclass. + */ +abstract class AbsBaseMessageItem : BaseEventItem() { + + abstract val baseAttributes: Attributes + + private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { + baseAttributes.readReceiptsCallback?.onReadReceiptsClicked(baseAttributes.informationData.readReceipts) + }) + + private var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { + override fun onReacted(reactionButton: ReactionButton) { + baseAttributes.reactionPillCallback?.onClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString, true) + } + + override fun onUnReacted(reactionButton: ReactionButton) { + baseAttributes.reactionPillCallback?.onClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString, false) + } + + override fun onLongClick(reactionButton: ReactionButton) { + baseAttributes.reactionPillCallback?.onLongClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString) + } + } + + open fun shouldShowReactionAtBottom(): Boolean { + return true + } + + override fun getEventIds(): List { + return listOf(baseAttributes.informationData.eventId) + } + + override fun bind(holder: H) { + super.bind(holder) + holder.readReceiptsView.render( + baseAttributes.informationData.readReceipts, + baseAttributes.avatarRenderer, + _readReceiptsClickListener + ) + + val reactions = baseAttributes.informationData.orderedReactionList + if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) { + holder.reactionsContainer.isVisible = false + } else { + holder.reactionsContainer.isVisible = true + holder.reactionsContainer.removeAllViews() + reactions.take(8).forEach { reaction -> + val reactionButton = ReactionButton(holder.view.context) + reactionButton.reactedListener = reactionClickListener + reactionButton.setTag(R.id.reactionsContainer, reaction.key) + reactionButton.reactionString = reaction.key + reactionButton.reactionCount = reaction.count + reactionButton.setChecked(reaction.addedByMe) + reactionButton.isEnabled = reaction.synced + holder.reactionsContainer.addView(reactionButton) + } + holder.reactionsContainer.setOnLongClickListener(baseAttributes.itemLongClickListener) + } + + holder.view.setOnClickListener(baseAttributes.itemClickListener) + holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener) + } + + override fun unbind(holder: H) { + holder.readReceiptsView.unbind() + super.unbind(holder) + } + + protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) { + root.isClickable = baseAttributes.informationData.sendState.isSent() + val state = if (baseAttributes.informationData.hasPendingEdits) SendState.UNSENT else baseAttributes.informationData.sendState + textView?.setTextColor(baseAttributes.colorProvider.getMessageTextColor(state)) + failureIndicator?.isVisible = baseAttributes.informationData.sendState.hasFailed() + } + + abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) { + val reactionsContainer by bind(R.id.reactionsContainer) + } + + /** + * This class holds all the common attributes for timeline items. + */ + interface Attributes { + // val avatarSize: Int, + val informationData: MessageInformationData + val avatarRenderer: AvatarRenderer + val colorProvider: ColorProvider + val itemLongClickListener: View.OnLongClickListener? + val itemClickListener: View.OnClickListener? + // val memberClickListener: View.OnClickListener? + val reactionPillCallback: TimelineEventController.ReactionPillCallback? + // val avatarCallback: TimelineEventController.AvatarCallback? + val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? +// val emojiTypeFace: Typeface? + } + +// data class AbsAttributes( +// override val informationData: MessageInformationData, +// override val avatarRenderer: AvatarRenderer, +// override val colorProvider: ColorProvider, +// override val itemLongClickListener: View.OnLongClickListener? = null, +// override val itemClickListener: View.OnClickListener? = null, +// override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null, +// override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null +// ) : Attributes +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 713b60d4d8..7d8fe3a10e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -18,22 +18,24 @@ package im.vector.riotx.features.home.room.detail.timeline.item import android.graphics.Typeface import android.view.View -import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.annotation.IdRes -import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute -import im.vector.matrix.android.api.session.room.send.SendState import im.vector.riotx.R import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotx.features.reactions.widget.ReactionButton -import im.vector.riotx.features.ui.getMessageTextColor -abstract class AbsMessageItem : BaseEventItem() { +/** + * Base timeline item that adds an optional information bar with the sender avatar, name and time + * Adds associated click listeners (on avatar, displayname) + */ +abstract class AbsMessageItem : AbsBaseMessageItem() { + + override val baseAttributes: AbsBaseMessageItem.Attributes + get() = attributes @EpoxyAttribute lateinit var attributes: Attributes @@ -45,24 +47,6 @@ abstract class AbsMessageItem : BaseEventItem() { attributes.avatarCallback?.onMemberNameClicked(attributes.informationData) }) - private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { - attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts) - }) - - var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { - override fun onReacted(reactionButton: ReactionButton) { - attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, true) - } - - override fun onUnReacted(reactionButton: ReactionButton) { - attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, false) - } - - override fun onLongClick(reactionButton: ReactionButton) { - attributes.reactionPillCallback?.onLongClickOnReactionPill(attributes.informationData, reactionButton.reactionString) - } - } - override fun bind(holder: H) { super.bind(holder) if (attributes.informationData.showInformation) { @@ -94,60 +78,12 @@ abstract class AbsMessageItem : BaseEventItem() { holder.avatarImageView.setOnLongClickListener(null) holder.memberNameView.setOnLongClickListener(null) } - holder.view.setOnClickListener(attributes.itemClickListener) - holder.view.setOnLongClickListener(attributes.itemLongClickListener) - - holder.readReceiptsView.render( - attributes.informationData.readReceipts, - attributes.avatarRenderer, - _readReceiptsClickListener - ) - - val reactions = attributes.informationData.orderedReactionList - if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) { - holder.reactionsContainer.isVisible = false - } else { - holder.reactionsContainer.isVisible = true - holder.reactionsContainer.removeAllViews() - reactions.take(8).forEach { reaction -> - val reactionButton = ReactionButton(holder.view.context) - reactionButton.reactedListener = reactionClickListener - reactionButton.setTag(R.id.reactionsContainer, reaction.key) - reactionButton.reactionString = reaction.key - reactionButton.reactionCount = reaction.count - reactionButton.setChecked(reaction.addedByMe) - reactionButton.isEnabled = reaction.synced - holder.reactionsContainer.addView(reactionButton) - } - holder.reactionsContainer.setOnLongClickListener(attributes.itemLongClickListener) - } } - override fun unbind(holder: H) { - holder.readReceiptsView.unbind() - super.unbind(holder) - } - - open fun shouldShowReactionAtBottom(): Boolean { - return true - } - - override fun getEventIds(): List { - return listOf(attributes.informationData.eventId) - } - - protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) { - root.isClickable = attributes.informationData.sendState.isSent() - val state = if (attributes.informationData.hasPendingEdits) SendState.UNSENT else attributes.informationData.sendState - textView?.setTextColor(attributes.colorProvider.getMessageTextColor(state)) - failureIndicator?.isVisible = attributes.informationData.sendState.hasFailed() - } - - abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) { + abstract class Holder(@IdRes stubId: Int) : AbsBaseMessageItem.Holder(stubId) { val avatarImageView by bind(R.id.messageAvatarImageView) val memberNameView by bind(R.id.messageMemberNameView) val timeView by bind(R.id.messageTimeView) - val reactionsContainer by bind(R.id.reactionsContainer) } /** @@ -155,15 +91,15 @@ abstract class AbsMessageItem : BaseEventItem() { */ data class Attributes( val avatarSize: Int, - val informationData: MessageInformationData, - val avatarRenderer: AvatarRenderer, - val colorProvider: ColorProvider, - val itemLongClickListener: View.OnLongClickListener? = null, - val itemClickListener: View.OnClickListener? = null, + override val informationData: MessageInformationData, + override val avatarRenderer: AvatarRenderer, + override val colorProvider: ColorProvider, + override val itemLongClickListener: View.OnLongClickListener? = null, + override val itemClickListener: View.OnClickListener? = null, val memberClickListener: View.OnClickListener? = null, - val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null, + override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null, val avatarCallback: TimelineEventController.AvatarCallback? = null, - val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, + override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, val emojiTypeFace: Typeface? = null - ) + ) : AbsBaseMessageItem.Attributes } 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 2dd581ce6f..5c0b521106 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 @@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item import android.os.Parcelable import im.vector.matrix.android.api.session.room.send.SendState +import im.vector.matrix.android.internal.session.room.VerificationState import kotlinx.android.parcel.Parcelize @Parcelize @@ -33,7 +34,14 @@ data class MessageInformationData( val orderedReactionList: List? = null, val hasBeenEdited: Boolean = false, val hasPendingEdits: Boolean = false, - val readReceipts: List = emptyList() + val readReceipts: List = emptyList(), + val referencesInfoData: ReferencesInfoData? = null, + val sentByMe : Boolean +) : Parcelable + +@Parcelize +data class ReferencesInfoData( + val verificationStatus: VerificationState ) : Parcelable @Parcelize diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt new file mode 100644 index 0000000000..bddef2c130 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.home.room.detail.timeline.item + +import android.annotation.SuppressLint +import android.graphics.Typeface +import android.view.View +import android.widget.RelativeLayout +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.view.updateLayoutParams +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController + +@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state) +abstract class VerificationRequestConclusionItem : AbsBaseMessageItem() { + + override val baseAttributes: AbsBaseMessageItem.Attributes + get() = attributes + + @EpoxyAttribute + lateinit var attributes: Attributes + + override fun getViewType() = STUB_ID + + @SuppressLint("SetTextI18n") + override fun bind(holder: Holder) { + super.bind(holder) + holder.endGuideline.updateLayoutParams { + this.marginEnd = leftGuideline + } + holder.titleView.text = holder.view.context.getString(R.string.sas_verified) + holder.descriptionView.text = "${attributes.informationData.memberName} (${attributes.informationData.senderId})" + } + + class Holder : AbsBaseMessageItem.Holder(STUB_ID) { + val titleView by bind(R.id.itemVerificationDoneTitleTextView) + val descriptionView by bind(R.id.itemVerificationDoneDetailTextView) + val endGuideline by bind(R.id.messageEndGuideline) + } + + companion object { + private const val STUB_ID = R.id.messageVerificationDoneStub + } + + /** + * This class holds all the common attributes for timeline items. + */ + data class Attributes( + val toUserId: String, + val toUserName: String, + override val informationData: MessageInformationData, + override val avatarRenderer: AvatarRenderer, + override val colorProvider: ColorProvider, + override val itemLongClickListener: View.OnLongClickListener? = null, + override val itemClickListener: View.OnClickListener? = null, + override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null, + override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, + val emojiTypeFace: Typeface? = null + ) : AbsBaseMessageItem.Attributes +} 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 new file mode 100644 index 0000000000..1a0e89fc32 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt @@ -0,0 +1,177 @@ +/* + * 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.home.room.detail.timeline.item + +import android.annotation.SuppressLint +import android.graphics.Typeface +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.appcompat.widget.AppCompatTextView +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.internal.session.room.VerificationState +import im.vector.riotx.R +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.utils.DebouncedClickListener +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.RoomDetailAction +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController + +@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state) +abstract class VerificationRequestItem : AbsBaseMessageItem() { + + override val baseAttributes: AbsBaseMessageItem.Attributes + get() = attributes + + @EpoxyAttribute + lateinit var attributes: Attributes + + @EpoxyAttribute + var callback: TimelineEventController.Callback? = null + + override fun getViewType() = STUB_ID + + @SuppressLint("SetTextI18n") + override fun bind(holder: Holder) { + super.bind(holder) + + holder.endGuideline.updateLayoutParams { + this.marginEnd = leftGuideline + } + + holder.titleView.text = if (attributes.informationData.sentByMe) + holder.view.context.getString(R.string.verification_sent) +// + "\n ${attributes.informationData.referencesInfoData?.verificationStatus?.name +// ?: "??"}" + else + holder.view.context.getString(R.string.verification_request) +// + "\n ${attributes.informationData.referencesInfoData?.verificationStatus?.name +// ?: "??"}" + + holder.descriptionView.text = if (!attributes.informationData.sentByMe) + "${attributes.informationData.memberName} (${attributes.informationData.senderId})" + else + "${attributes.otherUserName} (${attributes.otherUserId})" + + when (attributes.informationData.referencesInfoData?.verificationStatus) { + VerificationState.REQUEST, + null -> { + holder.buttonBar.isVisible = !attributes.informationData.sentByMe + holder.statusTextView.text = null + holder.statusTextView.isVisible = false + } + VerificationState.CANCELED_BY_OTHER -> { + holder.buttonBar.isVisible = false + holder.statusTextView.text = holder.view.context.getString(R.string.verification_request_other_cancelled, attributes.informationData.memberName) + holder.statusTextView.isVisible = true + } + VerificationState.CANCELED_BY_ME -> { + holder.buttonBar.isVisible = false + holder.statusTextView.text = holder.view.context.getString(R.string.verification_request_you_cancelled) + holder.statusTextView.isVisible = true + } + VerificationState.WAITING -> { + holder.buttonBar.isVisible = false + holder.statusTextView.text = holder.view.context.getString(R.string.verification_request_waiting) + holder.statusTextView.isVisible = true + } + VerificationState.DONE -> { + holder.buttonBar.isVisible = false + holder.statusTextView.text = if (attributes.informationData.sentByMe) + holder.view.context.getString(R.string.verification_request_other_accepted, attributes.otherUserName) + else + holder.view.context.getString(R.string.verification_request_you_accepted) + holder.statusTextView.isVisible = true + } + else -> { + holder.buttonBar.isVisible = false + holder.statusTextView.text = null + holder.statusTextView.isVisible = false + } + } + + holder.callback = callback + holder.attributes = attributes + } + + override fun unbind(holder: Holder) { + super.unbind(holder) + holder.callback = null + holder.attributes = null + } + + class Holder : AbsBaseMessageItem.Holder(STUB_ID) { + + var callback: TimelineEventController.Callback? = null + var attributes: Attributes? = null + + private val _clickListener = DebouncedClickListener(View.OnClickListener { + val att = attributes ?: return@OnClickListener + if (it == acceptButton) { + callback?.onTimelineItemAction(RoomDetailAction.AcceptVerificationRequest( + att.referenceId, + att.otherUserId, + att.fromDevide)) + } else if (it == declineButton) { + callback?.onTimelineItemAction(RoomDetailAction.DeclineVerificationRequest(att.referenceId)) + } + }) + + val titleView by bind(R.id.itemVerificationTitleTextView) + val descriptionView by bind(R.id.itemVerificationDetailTextView) + val buttonBar by bind(R.id.itemVerificationButtonBar) + val statusTextView by bind(R.id.itemVerificationStatusText) + val endGuideline by bind(R.id.messageEndGuideline) + private val declineButton by bind