Merge pull request #793 from vector-im/outgoing_dm_verif
BottomSheet UX for verification
This commit is contained in:
commit
06b41af467
|
@ -26,3 +26,8 @@ fun Throwable.is401() =
|
||||||
fun Throwable.isTokenError() =
|
fun Throwable.isTokenError() =
|
||||||
this is Failure.ServerError
|
this is Failure.ServerError
|
||||||
&& (error.code == MatrixError.M_UNKNOWN_TOKEN || error.code == MatrixError.M_MISSING_TOKEN)
|
&& (error.code == MatrixError.M_UNKNOWN_TOKEN || error.code == MatrixError.M_MISSING_TOKEN)
|
||||||
|
|
||||||
|
fun Throwable.shouldBeRetried(): Boolean {
|
||||||
|
return this is Failure.NetworkConnection
|
||||||
|
|| (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package im.vector.matrix.android.api.session.crypto.sas
|
package im.vector.matrix.android.api.session.crypto.sas
|
||||||
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://matrix.org/docs/spec/client_server/r0.5.0#key-verification-framework
|
* https://matrix.org/docs/spec/client_server/r0.5.0#key-verification-framework
|
||||||
|
@ -39,6 +40,10 @@ interface SasVerificationService {
|
||||||
|
|
||||||
fun getExistingTransaction(otherUser: String, tid: String): SasVerificationTransaction?
|
fun getExistingTransaction(otherUser: String, tid: String): SasVerificationTransaction?
|
||||||
|
|
||||||
|
fun getExistingVerificationRequest(otherUser: String): List<PendingVerificationRequest>?
|
||||||
|
|
||||||
|
fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut for KeyVerificationStart.VERIF_METHOD_SAS
|
* Shortcut for KeyVerificationStart.VERIF_METHOD_SAS
|
||||||
* @see beginKeyVerification
|
* @see beginKeyVerification
|
||||||
|
@ -50,7 +55,9 @@ interface SasVerificationService {
|
||||||
*/
|
*/
|
||||||
fun beginKeyVerification(method: String, userId: String, deviceID: String): String?
|
fun beginKeyVerification(method: String, userId: String, deviceID: String): String?
|
||||||
|
|
||||||
fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback<String>?)
|
fun requestKeyVerificationInDMs(userId: String, roomId: String): PendingVerificationRequest
|
||||||
|
|
||||||
|
fun declineVerificationRequestInDMs(otherUserId: String, otherDeviceId: String, transactionId: String, roomId: String)
|
||||||
|
|
||||||
fun beginKeyVerificationInDMs(method: String,
|
fun beginKeyVerificationInDMs(method: String,
|
||||||
transactionId: String,
|
transactionId: String,
|
||||||
|
@ -59,11 +66,33 @@ interface SasVerificationService {
|
||||||
otherDeviceId: String,
|
otherDeviceId: String,
|
||||||
callback: MatrixCallback<String>?): String?
|
callback: MatrixCallback<String>?): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns false if the request is unknwown
|
||||||
|
*/
|
||||||
|
fun readyPendingVerificationInDMs(otherUserId: String, roomId: String, transactionId: String): Boolean
|
||||||
|
|
||||||
// fun transactionUpdated(tx: SasVerificationTransaction)
|
// fun transactionUpdated(tx: SasVerificationTransaction)
|
||||||
|
|
||||||
interface SasVerificationListener {
|
interface SasVerificationListener {
|
||||||
fun transactionCreated(tx: SasVerificationTransaction)
|
fun transactionCreated(tx: SasVerificationTransaction)
|
||||||
fun transactionUpdated(tx: SasVerificationTransaction)
|
fun transactionUpdated(tx: SasVerificationTransaction)
|
||||||
fun markedAsManuallyVerified(userId: String, deviceId: String)
|
fun markedAsManuallyVerified(userId: String, deviceId: String) {}
|
||||||
|
|
||||||
|
fun verificationRequestCreated(pr: PendingVerificationRequest) {}
|
||||||
|
fun verificationRequestUpdated(pr: PendingVerificationRequest) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val TEN_MINTUTES_IN_MILLIS = 10 * 60 * 1000
|
||||||
|
private const val FIVE_MINTUTES_IN_MILLIS = 5 * 60 * 1000
|
||||||
|
|
||||||
|
fun isValidRequest(age: Long?): Boolean {
|
||||||
|
if (age == null) return false
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val tooInThePast = now - TEN_MINTUTES_IN_MILLIS
|
||||||
|
val tooInTheFuture = now + FIVE_MINTUTES_IN_MILLIS
|
||||||
|
return age in tooInThePast..tooInTheFuture
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,4 +47,6 @@ interface SasVerificationTransaction {
|
||||||
* both short codes do match
|
* both short codes do match
|
||||||
*/
|
*/
|
||||||
fun userHasVerifiedShortCode()
|
fun userHasVerifiedShortCode()
|
||||||
|
|
||||||
|
fun shortCodeDoesNotMatch()
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,7 @@ object EventType {
|
||||||
const val KEY_VERIFICATION_MAC = "m.key.verification.mac"
|
const val KEY_VERIFICATION_MAC = "m.key.verification.mac"
|
||||||
const val KEY_VERIFICATION_CANCEL = "m.key.verification.cancel"
|
const val KEY_VERIFICATION_CANCEL = "m.key.verification.cancel"
|
||||||
const val KEY_VERIFICATION_DONE = "m.key.verification.done"
|
const val KEY_VERIFICATION_DONE = "m.key.verification.done"
|
||||||
|
const val KEY_VERIFICATION_READY = "m.key.verification.ready"
|
||||||
|
|
||||||
// Relation Events
|
// Relation Events
|
||||||
const val REACTION = "m.reaction"
|
const val REACTION = "m.reaction"
|
||||||
|
|
|
@ -89,4 +89,6 @@ interface RoomService {
|
||||||
fun getRoomIdByAlias(roomAlias: String,
|
fun getRoomIdByAlias(roomAlias: String,
|
||||||
searchOnServer: Boolean,
|
searchOnServer: Boolean,
|
||||||
callback: MatrixCallback<Optional<String>>): Cancelable
|
callback: MatrixCallback<Optional<String>>): Cancelable
|
||||||
|
|
||||||
|
fun getExistingDirectRoomWithUser(otherUserId: String) : Room?
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ internal data class MessageVerificationAcceptContent(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toEventContent() = this.toContent()
|
override fun toEventContent() = toContent()
|
||||||
|
|
||||||
companion object : VerificationInfoAcceptFactory {
|
companion object : VerificationInfoAcceptFactory {
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ data class MessageVerificationCancelContent(
|
||||||
override val transactionID: String?
|
override val transactionID: String?
|
||||||
get() = relatesTo?.eventId
|
get() = relatesTo?.eventId
|
||||||
|
|
||||||
override fun toEventContent() = this.toContent()
|
override fun toEventContent() = toContent()
|
||||||
|
|
||||||
override fun isValid(): Boolean {
|
override fun isValid(): Boolean {
|
||||||
if (transactionID.isNullOrBlank() || code.isNullOrBlank()) {
|
if (transactionID.isNullOrBlank() || code.isNullOrBlank()) {
|
||||||
|
|
|
@ -17,6 +17,8 @@ package im.vector.matrix.android.api.session.room.model.message
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toContent
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||||
import im.vector.matrix.android.internal.crypto.verification.VerificationInfo
|
import im.vector.matrix.android.internal.crypto.verification.VerificationInfo
|
||||||
|
|
||||||
|
@ -24,5 +26,11 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationInfo
|
||||||
internal data class MessageVerificationDoneContent(
|
internal data class MessageVerificationDoneContent(
|
||||||
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
|
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
|
||||||
) : VerificationInfo {
|
) : VerificationInfo {
|
||||||
override fun isValid() = true
|
|
||||||
|
override val transactionID: String?
|
||||||
|
get() = relatesTo?.eventId
|
||||||
|
|
||||||
|
override fun isValid() = transactionID?.isNotEmpty() == true
|
||||||
|
|
||||||
|
override fun toEventContent(): Content? = toContent()
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ internal data class MessageVerificationKeyContent(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toEventContent() = this.toContent()
|
override fun toEventContent() = toContent()
|
||||||
|
|
||||||
companion object : VerificationInfoKeyFactory {
|
companion object : VerificationInfoKeyFactory {
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ internal data class MessageVerificationMacContent(
|
||||||
override val transactionID: String?
|
override val transactionID: String?
|
||||||
get() = relatesTo?.eventId
|
get() = relatesTo?.eventId
|
||||||
|
|
||||||
override fun toEventContent() = this.toContent()
|
override fun toEventContent() = toContent()
|
||||||
|
|
||||||
override fun isValid(): Boolean {
|
override fun isValid(): Boolean {
|
||||||
if (transactionID.isNullOrBlank() || keys.isNullOrBlank() || mac.isNullOrEmpty()) {
|
if (transactionID.isNullOrBlank() || keys.isNullOrBlank() || mac.isNullOrEmpty()) {
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.matrix.android.api.session.room.model.message
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.verification.MessageVerificationReadyFactory
|
||||||
|
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoReady
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class MessageVerificationReadyContent(
|
||||||
|
@Json(name = "from_device") override val fromDevice: String? = null,
|
||||||
|
@Json(name = "methods") override val methods: List<String>? = null,
|
||||||
|
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
|
||||||
|
) : VerificationInfoReady {
|
||||||
|
|
||||||
|
override val transactionID: String?
|
||||||
|
get() = relatesTo?.eventId
|
||||||
|
|
||||||
|
override fun toEventContent() = toContent()
|
||||||
|
|
||||||
|
override fun isValid(): Boolean {
|
||||||
|
if (transactionID.isNullOrBlank() || methods.isNullOrEmpty() || fromDevice.isNullOrEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MessageVerificationReadyFactory {
|
||||||
|
override fun create(tid: String, methods: List<String>, fromDevice: String): VerificationInfoReady {
|
||||||
|
return MessageVerificationReadyContent(
|
||||||
|
fromDevice = fromDevice,
|
||||||
|
methods = methods,
|
||||||
|
relatesTo = RelationDefaultContent(
|
||||||
|
RelationType.REFERENCE,
|
||||||
|
tid
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -62,5 +62,5 @@ internal data class MessageVerificationStartContent(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toEventContent() = this.toContent()
|
override fun toEventContent() = toContent()
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,6 +180,9 @@ internal abstract class CryptoModule {
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindEncryptEventTask(encryptEventTask: DefaultEncryptEventTask): EncryptEventTask
|
abstract fun bindEncryptEventTask(encryptEventTask: DefaultEncryptEventTask): EncryptEventTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindSendVerificationMessageTask(sendDefaultSendVerificationMessageTask: DefaultSendVerificationMessageTask): SendVerificationMessageTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindClaimOneTimeKeysForUsersDeviceTask(claimOneTimeKeysForUsersDevice: DefaultClaimOneTimeKeysForUsersDevice)
|
abstract fun bindClaimOneTimeKeysForUsersDeviceTask(claimOneTimeKeysForUsersDevice: DefaultClaimOneTimeKeysForUsersDevice)
|
||||||
: ClaimOneTimeKeysForUsersDeviceTask
|
: ClaimOneTimeKeysForUsersDeviceTask
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.matrix.android.internal.crypto.model.rest
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoReady
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests a key verification with another user's devices.
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class KeyVerificationReady(
|
||||||
|
@Json(name = "from_device") override val fromDevice: String?,
|
||||||
|
// TODO add qr?
|
||||||
|
@Json(name = "methods") override val methods: List<String>? = listOf(KeyVerificationStart.VERIF_METHOD_SAS),
|
||||||
|
@Json(name = "transaction_id") override var transactionID: String? = null
|
||||||
|
) : SendToDeviceObject, VerificationInfoReady {
|
||||||
|
|
||||||
|
override fun toSendToDeviceObject() = this
|
||||||
|
|
||||||
|
override fun isValid(): Boolean {
|
||||||
|
return !transactionID.isNullOrBlank() && !fromDevice.isNullOrBlank() && !methods.isNullOrEmpty()
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,7 +37,7 @@ internal data class KeyVerificationRequest(
|
||||||
val timestamp: Int,
|
val timestamp: Int,
|
||||||
|
|
||||||
@Json(name = "transaction_id")
|
@Json(name = "transaction_id")
|
||||||
var transactionID: String? = null
|
override var transactionID: String? = null
|
||||||
|
|
||||||
) : SendToDeviceObject, VerificationInfo {
|
) : SendToDeviceObject, VerificationInfo {
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ data class KeyVerificationStart(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val VERIF_METHOD_SAS = "m.sas.v1"
|
const val VERIF_METHOD_SAS = "m.sas.v1"
|
||||||
|
const val VERIF_METHOD_SCAN = "m.qr_code.scan.v1"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isValid(): Boolean {
|
override fun isValid(): Boolean {
|
||||||
|
|
|
@ -1,95 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2019 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package im.vector.matrix.android.internal.crypto.tasks
|
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
|
||||||
import im.vector.matrix.android.internal.network.executeRequest
|
|
||||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
|
||||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
|
||||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater
|
|
||||||
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
|
||||||
import im.vector.matrix.android.internal.task.Task
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal interface RequestVerificationDMTask : Task<RequestVerificationDMTask.Params, SendResponse> {
|
|
||||||
data class Params(
|
|
||||||
val event: Event,
|
|
||||||
val cryptoService: CryptoService
|
|
||||||
)
|
|
||||||
|
|
||||||
fun createParamsAndLocalEcho(roomId: String, from: String, methods: List<String>, to: String, cryptoService: CryptoService): Params
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DefaultRequestVerificationDMTask @Inject constructor(
|
|
||||||
private val localEchoUpdater: LocalEchoUpdater,
|
|
||||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
|
||||||
private val encryptEventTask: DefaultEncryptEventTask,
|
|
||||||
private val monarchy: Monarchy,
|
|
||||||
private val roomAPI: RoomAPI)
|
|
||||||
: RequestVerificationDMTask {
|
|
||||||
|
|
||||||
override fun createParamsAndLocalEcho(roomId: String, from: String, methods: List<String>, to: String, cryptoService: CryptoService)
|
|
||||||
: RequestVerificationDMTask.Params {
|
|
||||||
val event = localEchoEventFactory.createVerificationRequest(roomId, from, to, methods)
|
|
||||||
.also { localEchoEventFactory.saveLocalEcho(monarchy, it) }
|
|
||||||
return RequestVerificationDMTask.Params(
|
|
||||||
event,
|
|
||||||
cryptoService
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun execute(params: RequestVerificationDMTask.Params): SendResponse {
|
|
||||||
val event = handleEncryption(params)
|
|
||||||
val localID = event.eventId!!
|
|
||||||
|
|
||||||
try {
|
|
||||||
localEchoUpdater.updateSendState(localID, SendState.SENDING)
|
|
||||||
val executeRequest = executeRequest<SendResponse> {
|
|
||||||
apiCall = roomAPI.send(
|
|
||||||
localID,
|
|
||||||
roomId = event.roomId ?: "",
|
|
||||||
content = event.content,
|
|
||||||
eventType = event.type // message or room.encrypted
|
|
||||||
)
|
|
||||||
}
|
|
||||||
localEchoUpdater.updateSendState(localID, SendState.SENT)
|
|
||||||
return executeRequest
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
localEchoUpdater.updateSendState(localID, SendState.UNDELIVERED)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun handleEncryption(params: RequestVerificationDMTask.Params): Event {
|
|
||||||
val roomId = params.event.roomId ?: ""
|
|
||||||
if (params.cryptoService.isRoomEncrypted(roomId)) {
|
|
||||||
try {
|
|
||||||
return encryptEventTask.execute(EncryptEventTask.Params(
|
|
||||||
roomId,
|
|
||||||
params.event,
|
|
||||||
listOf("m.relates_to"),
|
|
||||||
params.cryptoService
|
|
||||||
))
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
// We said it's ok to send verification request in clear
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return params.event
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,151 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto.tasks
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
|
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||||
|
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.*
|
||||||
|
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
|
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
|
||||||
|
import im.vector.matrix.android.internal.di.DeviceId
|
||||||
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
|
import im.vector.matrix.android.internal.task.Task
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface RoomVerificationUpdateTask : Task<RoomVerificationUpdateTask.Params, Unit> {
|
||||||
|
data class Params(
|
||||||
|
val events: List<Event>,
|
||||||
|
val sasVerificationService: DefaultSasVerificationService,
|
||||||
|
val cryptoService: CryptoService
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
||||||
|
@UserId private val userId: String,
|
||||||
|
@DeviceId private val deviceId: String?,
|
||||||
|
private val cryptoService: CryptoService) : RoomVerificationUpdateTask {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// XXX what about multi-account?
|
||||||
|
private val transactionsHandledByOtherDevice = ArrayList<String>()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun execute(params: RoomVerificationUpdateTask.Params) {
|
||||||
|
// TODO ignore initial sync or back pagination?
|
||||||
|
|
||||||
|
params.events.forEach { event ->
|
||||||
|
Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
|
||||||
|
Timber.v("## SAS Verification live observer: received msgId: $event")
|
||||||
|
|
||||||
|
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
|
||||||
|
// the message should be ignored by the receiver.
|
||||||
|
|
||||||
|
if (!SasVerificationService.isValidRequest(event.ageLocalTs
|
||||||
|
?: event.originServerTs)) return@forEach Unit.also {
|
||||||
|
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated")
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrypt if needed?
|
||||||
|
if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
||||||
|
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
||||||
|
// for now decrypt sync
|
||||||
|
try {
|
||||||
|
val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString())
|
||||||
|
event.mxDecryptionResult = OlmDecryptionResult(
|
||||||
|
payload = result.clearEvent,
|
||||||
|
senderKey = result.senderCurve25519Key,
|
||||||
|
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||||
|
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||||
|
)
|
||||||
|
} catch (e: MXCryptoError) {
|
||||||
|
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
|
||||||
|
|
||||||
|
if (event.senderId == userId) {
|
||||||
|
// If it's send from me, we need to keep track of Requests or Start
|
||||||
|
// done from another device of mine
|
||||||
|
|
||||||
|
if (EventType.MESSAGE == event.type) {
|
||||||
|
val msgType = event.getClearContent().toModel<MessageContent>()?.type
|
||||||
|
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
|
||||||
|
event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
|
||||||
|
if (it.fromDevice != deviceId) {
|
||||||
|
// The verification is requested from another device
|
||||||
|
Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ")
|
||||||
|
event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (EventType.KEY_VERIFICATION_START == event.type) {
|
||||||
|
event.getClearContent().toModel<MessageVerificationStartContent>()?.let {
|
||||||
|
if (it.fromDevice != deviceId) {
|
||||||
|
// The verification is started from another device
|
||||||
|
Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ")
|
||||||
|
it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (EventType.KEY_VERIFICATION_READY == event.type) {
|
||||||
|
event.getClearContent().toModel<MessageVerificationReadyContent>()?.let {
|
||||||
|
if (it.fromDevice != deviceId) {
|
||||||
|
// The verification is started from another device
|
||||||
|
Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ")
|
||||||
|
it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) {
|
||||||
|
event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId?.let {
|
||||||
|
transactionsHandledByOtherDevice.remove(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
val relatesTo = event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId
|
||||||
|
if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) {
|
||||||
|
// Ignore this event, it is directed to another of my devices
|
||||||
|
Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
when (event.getClearType()) {
|
||||||
|
EventType.KEY_VERIFICATION_START,
|
||||||
|
EventType.KEY_VERIFICATION_ACCEPT,
|
||||||
|
EventType.KEY_VERIFICATION_KEY,
|
||||||
|
EventType.KEY_VERIFICATION_MAC,
|
||||||
|
EventType.KEY_VERIFICATION_CANCEL,
|
||||||
|
EventType.KEY_VERIFICATION_READY,
|
||||||
|
EventType.KEY_VERIFICATION_DONE -> {
|
||||||
|
params.sasVerificationService.onRoomEvent(event)
|
||||||
|
}
|
||||||
|
EventType.MESSAGE -> {
|
||||||
|
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
|
||||||
|
params.sasVerificationService.onRoomRequestReceived(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,64 +15,29 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.matrix.android.internal.crypto.tasks
|
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.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.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.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.network.executeRequest
|
||||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
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.LocalEchoUpdater
|
||||||
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
||||||
import im.vector.matrix.android.internal.task.Task
|
import im.vector.matrix.android.internal.task.Task
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, SendResponse> {
|
internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, String> {
|
||||||
data class Params(
|
data class Params(
|
||||||
val type: String,
|
|
||||||
val event: Event,
|
val event: Event,
|
||||||
val cryptoService: CryptoService?
|
val cryptoService: CryptoService?
|
||||||
)
|
)
|
||||||
|
|
||||||
fun createParamsAndLocalEcho(type: String,
|
|
||||||
roomId: String,
|
|
||||||
content: Content,
|
|
||||||
cryptoService: CryptoService?) : Params
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class DefaultSendVerificationMessageTask @Inject constructor(
|
internal class DefaultSendVerificationMessageTask @Inject constructor(
|
||||||
private val localEchoUpdater: LocalEchoUpdater,
|
private val localEchoUpdater: LocalEchoUpdater,
|
||||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
|
||||||
private val encryptEventTask: DefaultEncryptEventTask,
|
private val encryptEventTask: DefaultEncryptEventTask,
|
||||||
private val monarchy: Monarchy,
|
|
||||||
@UserId private val userId: String,
|
|
||||||
private val roomAPI: RoomAPI) : SendVerificationMessageTask {
|
private val roomAPI: RoomAPI) : SendVerificationMessageTask {
|
||||||
|
|
||||||
override fun createParamsAndLocalEcho(type: String, roomId: String, content: Content, cryptoService: CryptoService?): SendVerificationMessageTask.Params {
|
override suspend fun execute(params: SendVerificationMessageTask.Params): String {
|
||||||
val localID = LocalEcho.createLocalEchoId()
|
|
||||||
val event = Event(
|
|
||||||
roomId = roomId,
|
|
||||||
originServerTs = System.currentTimeMillis(),
|
|
||||||
senderId = userId,
|
|
||||||
eventId = localID,
|
|
||||||
type = type,
|
|
||||||
content = content,
|
|
||||||
unsignedData = UnsignedData(age = null, transactionId = localID)
|
|
||||||
).also {
|
|
||||||
localEchoEventFactory.saveLocalEcho(monarchy, it)
|
|
||||||
}
|
|
||||||
return SendVerificationMessageTask.Params(
|
|
||||||
type,
|
|
||||||
event,
|
|
||||||
cryptoService
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun execute(params: SendVerificationMessageTask.Params): SendResponse {
|
|
||||||
val event = handleEncryption(params)
|
val event = handleEncryption(params)
|
||||||
val localID = event.eventId!!
|
val localID = event.eventId!!
|
||||||
|
|
||||||
|
@ -87,7 +52,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
localEchoUpdater.updateSendState(localID, SendState.SENT)
|
localEchoUpdater.updateSendState(localID, SendState.SENT)
|
||||||
return executeRequest
|
return executeRequest.eventId
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
localEchoUpdater.updateSendState(localID, SendState.UNDELIVERED)
|
localEchoUpdater.updateSendState(localID, SendState.UNDELIVERED)
|
||||||
throw e
|
throw e
|
||||||
|
|
|
@ -33,7 +33,8 @@ internal class DefaultIncomingSASVerificationTransaction(
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
deviceFingerprint: String,
|
deviceFingerprint: String,
|
||||||
transactionId: String,
|
transactionId: String,
|
||||||
otherUserID: String
|
otherUserID: String,
|
||||||
|
val autoAccept: Boolean = false
|
||||||
) : SASVerificationTransaction(
|
) : SASVerificationTransaction(
|
||||||
setDeviceVerificationAction,
|
setDeviceVerificationAction,
|
||||||
credentials,
|
credentials,
|
||||||
|
@ -76,6 +77,10 @@ internal class DefaultIncomingSASVerificationTransaction(
|
||||||
this.startReq = startReq
|
this.startReq = startReq
|
||||||
state = SasVerificationTxState.OnStarted
|
state = SasVerificationTxState.OnStarted
|
||||||
this.otherDeviceId = startReq.fromDevice
|
this.otherDeviceId = startReq.fromDevice
|
||||||
|
|
||||||
|
if (autoAccept) {
|
||||||
|
performAccept()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun performAccept() {
|
override fun performAccept() {
|
||||||
|
|
|
@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||||
import im.vector.matrix.android.api.session.crypto.sas.safeValueOf
|
import im.vector.matrix.android.api.session.crypto.sas.safeValueOf
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
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.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.message.*
|
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.DeviceListManager
|
||||||
|
@ -37,12 +38,7 @@ import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.*
|
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.store.IMXCryptoStore
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultRequestVerificationDMTask
|
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.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 im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -60,11 +56,9 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
||||||
private val deviceListManager: DeviceListManager,
|
private val deviceListManager: DeviceListManager,
|
||||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||||
private val requestVerificationDMTask: DefaultRequestVerificationDMTask,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val sasTransportRoomMessageFactory: SasTransportRoomMessageFactory,
|
private val sasTransportRoomMessageFactory: SasTransportRoomMessageFactory,
|
||||||
private val sasTransportToDeviceFactory: SasTransportToDeviceFactory,
|
private val sasTransportToDeviceFactory: SasTransportToDeviceFactory
|
||||||
private val taskExecutor: TaskExecutor
|
|
||||||
) : VerificationTransaction.Listener, SasVerificationService {
|
) : VerificationTransaction.Listener, SasVerificationService {
|
||||||
|
|
||||||
private val uiHandler = Handler(Looper.getMainLooper())
|
private val uiHandler = Handler(Looper.getMainLooper())
|
||||||
|
@ -75,6 +69,12 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
// map [sender : [transaction]]
|
// map [sender : [transaction]]
|
||||||
private val txMap = HashMap<String, HashMap<String, VerificationTransaction>>()
|
private val txMap = HashMap<String, HashMap<String, VerificationTransaction>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map [sender: [PendingVerificationRequest]]
|
||||||
|
* For now we keep all requests (even terminated ones) during the lifetime of the app.
|
||||||
|
*/
|
||||||
|
private val pendingRequests = HashMap<String, ArrayList<PendingVerificationRequest>>()
|
||||||
|
|
||||||
// Event received from the sync
|
// Event received from the sync
|
||||||
fun onToDeviceEvent(event: Event) {
|
fun onToDeviceEvent(event: Event) {
|
||||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||||
|
@ -120,8 +120,11 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
EventType.KEY_VERIFICATION_MAC -> {
|
EventType.KEY_VERIFICATION_MAC -> {
|
||||||
onRoomMacReceived(event)
|
onRoomMacReceived(event)
|
||||||
}
|
}
|
||||||
|
EventType.KEY_VERIFICATION_READY -> {
|
||||||
|
onRoomReadyReceived(event)
|
||||||
|
}
|
||||||
EventType.KEY_VERIFICATION_DONE -> {
|
EventType.KEY_VERIFICATION_DONE -> {
|
||||||
// TODO?
|
onRoomDoneReceived(event)
|
||||||
}
|
}
|
||||||
EventType.MESSAGE -> {
|
EventType.MESSAGE -> {
|
||||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
|
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
|
||||||
|
@ -175,6 +178,30 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun dispatchRequestAdded(tx: PendingVerificationRequest) {
|
||||||
|
uiHandler.post {
|
||||||
|
listeners.forEach {
|
||||||
|
try {
|
||||||
|
it.verificationRequestCreated(tx)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Timber.e(e, "## Error while notifying listeners")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dispatchRequestUpdated(tx: PendingVerificationRequest) {
|
||||||
|
uiHandler.post {
|
||||||
|
listeners.forEach {
|
||||||
|
try {
|
||||||
|
it.verificationRequestUpdated(tx)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Timber.e(e, "## Error while notifying listeners")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) {
|
override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) {
|
||||||
setDeviceVerificationAction.handle(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED,
|
setDeviceVerificationAction.handle(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED,
|
||||||
deviceID,
|
deviceID,
|
||||||
|
@ -190,8 +217,50 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRoomRequestReceived(event: Event) {
|
fun onRoomRequestReceived(event: Event) {
|
||||||
// TODO
|
|
||||||
Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}")
|
Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}")
|
||||||
|
val requestInfo = event.getClearContent().toModel<MessageVerificationRequestContent>()
|
||||||
|
?: return
|
||||||
|
val senderId = event.senderId ?: return
|
||||||
|
|
||||||
|
if (requestInfo.toUserId != credentials.userId) {
|
||||||
|
// I should ignore this, it's not for me
|
||||||
|
Timber.w("## SAS Verification ignoring request from ${event.senderId}, not sent to me")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't want to block here
|
||||||
|
GlobalScope.launch {
|
||||||
|
if (checkKeysAreDownloaded(senderId, requestInfo.fromDevice) == null) {
|
||||||
|
Timber.e("## SAS Verification device ${requestInfo.fromDevice} is not knwon")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remember this request
|
||||||
|
val requestsForUser = pendingRequests[senderId]
|
||||||
|
?: ArrayList<PendingVerificationRequest>().also {
|
||||||
|
pendingRequests[event.senderId] = it
|
||||||
|
}
|
||||||
|
|
||||||
|
val pendingVerificationRequest = PendingVerificationRequest(
|
||||||
|
ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(),
|
||||||
|
isIncoming = true,
|
||||||
|
otherUserId = senderId, // requestInfo.toUserId,
|
||||||
|
transactionId = event.eventId,
|
||||||
|
requestInfo = requestInfo
|
||||||
|
)
|
||||||
|
requestsForUser.add(pendingVerificationRequest)
|
||||||
|
dispatchRequestAdded(pendingVerificationRequest)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* After the m.key.verification.ready event is sent, either party can send an m.key.verification.start event
|
||||||
|
* to begin the verification.
|
||||||
|
* If both parties send an m.key.verification.start event, and they both specify the same verification method,
|
||||||
|
* then the event sent by the user whose user ID is the smallest is used, and the other m.key.verification.start
|
||||||
|
* event is ignored.
|
||||||
|
* In the case of a single user verifying two of their devices, the device ID is compared instead.
|
||||||
|
* If both parties send an m.key.verification.start event, but they specify different verification methods,
|
||||||
|
* the verification should be cancelled with a code of m.unexpected_message.
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun onRoomStartRequestReceived(event: Event) {
|
private suspend fun onRoomStartRequestReceived(event: Event) {
|
||||||
|
@ -205,8 +274,9 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
if (startReq?.isValid()?.not() == true) {
|
if (startReq?.isValid()?.not() == true) {
|
||||||
Timber.e("## received invalid verification request")
|
Timber.e("## received invalid verification request")
|
||||||
if (startReq.transactionID != null) {
|
if (startReq.transactionID != null) {
|
||||||
sasTransportRoomMessageFactory.createTransport(event.roomId
|
sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId
|
||||||
?: "", cryptoService, null).cancelTransaction(
|
?: "", event.roomId
|
||||||
|
?: "", null).cancelTransaction(
|
||||||
startReq.transactionID ?: "",
|
startReq.transactionID ?: "",
|
||||||
otherUserId!!,
|
otherUserId!!,
|
||||||
startReq.fromDevice ?: event.getSenderKey()!!,
|
startReq.fromDevice ?: event.getSenderKey()!!,
|
||||||
|
@ -217,11 +287,13 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
handleStart(otherUserId, startReq as VerificationInfoStart) {
|
handleStart(otherUserId, startReq as VerificationInfoStart) {
|
||||||
it.transport = sasTransportRoomMessageFactory.createTransport(event.roomId
|
it.transport = sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId
|
||||||
?: "", cryptoService, it)
|
?: "", event.roomId
|
||||||
|
?: "", it)
|
||||||
}?.let {
|
}?.let {
|
||||||
sasTransportRoomMessageFactory.createTransport(event.roomId
|
sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId
|
||||||
?: "", cryptoService, null).cancelTransaction(
|
?: "", event.roomId
|
||||||
|
?: "", null).cancelTransaction(
|
||||||
startReq.transactionID ?: "",
|
startReq.transactionID ?: "",
|
||||||
otherUserId!!,
|
otherUserId!!,
|
||||||
startReq.fromDevice ?: event.getSenderKey()!!,
|
startReq.fromDevice ?: event.getSenderKey()!!,
|
||||||
|
@ -263,7 +335,7 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
|
|
||||||
private suspend fun handleStart(otherUserId: String?, startReq: VerificationInfoStart, txConfigure: (SASVerificationTransaction) -> Unit): CancelCode? {
|
private suspend fun handleStart(otherUserId: String?, startReq: VerificationInfoStart, txConfigure: (SASVerificationTransaction) -> Unit): CancelCode? {
|
||||||
Timber.d("## SAS onStartRequestReceived ${startReq.transactionID!!}")
|
Timber.d("## SAS onStartRequestReceived ${startReq.transactionID!!}")
|
||||||
if (checkKeysAreDownloaded(otherUserId!!, startReq) != null) {
|
if (checkKeysAreDownloaded(otherUserId!!, startReq.fromDevice ?: "") != null) {
|
||||||
Timber.v("## SAS onStartRequestReceived $startReq")
|
Timber.v("## SAS onStartRequestReceived $startReq")
|
||||||
val tid = startReq.transactionID!!
|
val tid = startReq.transactionID!!
|
||||||
val existing = getExistingTransaction(otherUserId, tid)
|
val existing = getExistingTransaction(otherUserId, tid)
|
||||||
|
@ -286,6 +358,10 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
// Ok we can create
|
// Ok we can create
|
||||||
if (KeyVerificationStart.VERIF_METHOD_SAS == startReq.method) {
|
if (KeyVerificationStart.VERIF_METHOD_SAS == startReq.method) {
|
||||||
Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}")
|
Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}")
|
||||||
|
// If there is a corresponding request, we can auto accept
|
||||||
|
// as we are the one requesting in first place (or we accepted the request)
|
||||||
|
val autoAccept = getExistingVerificationRequest(otherUserId)?.any { it.transactionId == startReq.transactionID }
|
||||||
|
?: false
|
||||||
val tx = DefaultIncomingSASVerificationTransaction(
|
val tx = DefaultIncomingSASVerificationTransaction(
|
||||||
// this,
|
// this,
|
||||||
setDeviceVerificationAction,
|
setDeviceVerificationAction,
|
||||||
|
@ -293,7 +369,8 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||||
startReq.transactionID!!,
|
startReq.transactionID!!,
|
||||||
otherUserId).also { txConfigure(it) }
|
otherUserId,
|
||||||
|
autoAccept).also { txConfigure(it) }
|
||||||
addTransaction(tx)
|
addTransaction(tx)
|
||||||
tx.acceptVerificationEvent(otherUserId, startReq)
|
tx.acceptVerificationEvent(otherUserId, startReq)
|
||||||
} else {
|
} else {
|
||||||
|
@ -311,11 +388,16 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkKeysAreDownloaded(otherUserId: String,
|
private suspend fun checkKeysAreDownloaded(otherUserId: String,
|
||||||
startReq: VerificationInfoStart): MXUsersDevicesMap<MXDeviceInfo>? {
|
fromDevice: String): MXUsersDevicesMap<MXDeviceInfo>? {
|
||||||
return try {
|
return try {
|
||||||
val keys = deviceListManager.downloadKeys(listOf(otherUserId), true)
|
var keys = deviceListManager.downloadKeys(listOf(otherUserId), false)
|
||||||
val deviceIds = keys.getUserDeviceIds(otherUserId) ?: return null
|
if (keys.getUserDeviceIds(otherUserId)?.contains(fromDevice) == true) {
|
||||||
keys.takeIf { deviceIds.contains(startReq.fromDevice) }
|
return keys
|
||||||
|
} else {
|
||||||
|
// force download
|
||||||
|
keys = deviceListManager.downloadKeys(listOf(otherUserId), true)
|
||||||
|
return keys.takeIf { keys.getUserDeviceIds(otherUserId)?.contains(fromDevice) == true }
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@ -333,6 +415,10 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
// TODO should we cancel?
|
// TODO should we cancel?
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
getExistingVerificationRequest(event.senderId ?: "", cancelReq.transactionID)?.let {
|
||||||
|
updatePendingRequest(it.copy(cancelConclusion = safeValueOf(cancelReq.code)))
|
||||||
|
// Should we remove it from the list?
|
||||||
|
}
|
||||||
handleOnCancel(event.senderId!!, cancelReq)
|
handleOnCancel(event.senderId!!, cancelReq)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -352,14 +438,20 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
|
|
||||||
private fun handleOnCancel(otherUserId: String, cancelReq: VerificationInfoCancel) {
|
private fun handleOnCancel(otherUserId: String, cancelReq: VerificationInfoCancel) {
|
||||||
Timber.v("## SAS onCancelReceived otherUser:$otherUserId reason:${cancelReq.reason}")
|
Timber.v("## SAS onCancelReceived otherUser:$otherUserId reason:${cancelReq.reason}")
|
||||||
val existing = getExistingTransaction(otherUserId, cancelReq.transactionID!!)
|
|
||||||
if (existing == null) {
|
val existingTransaction = getExistingTransaction(otherUserId, cancelReq.transactionID!!)
|
||||||
Timber.e("## Received invalid cancel request")
|
val existingRequest = getExistingVerificationRequest(otherUserId, cancelReq.transactionID!!)
|
||||||
return
|
|
||||||
|
if (existingRequest != null) {
|
||||||
|
// Mark this request as cancelled
|
||||||
|
updatePendingRequest(existingRequest.copy(
|
||||||
|
cancelConclusion = safeValueOf(cancelReq.code)
|
||||||
|
))
|
||||||
}
|
}
|
||||||
if (existing is SASVerificationTransaction) {
|
|
||||||
existing.cancelledReason = safeValueOf(cancelReq.code)
|
if (existingTransaction is SASVerificationTransaction) {
|
||||||
existing.state = SasVerificationTxState.OnCancelled
|
existingTransaction.cancelledReason = safeValueOf(cancelReq.code)
|
||||||
|
existingTransaction.state = SasVerificationTxState.OnCancelled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -456,6 +548,44 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
handleMacReceived(event.senderId, macReq)
|
handleMacReceived(event.senderId, macReq)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun onRoomReadyReceived(event: Event) {
|
||||||
|
val readyReq = event.getClearContent().toModel<MessageVerificationReadyContent>()
|
||||||
|
?.copy(
|
||||||
|
// relates_to is in clear in encrypted payload
|
||||||
|
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
|
||||||
|
)
|
||||||
|
if (readyReq == null || readyReq.isValid().not() || event.senderId == null) {
|
||||||
|
// ignore
|
||||||
|
Timber.e("## SAS Received invalid ready request")
|
||||||
|
// TODO should we cancel?
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice ?: "") == null) {
|
||||||
|
Timber.e("## SAS Verification device ${readyReq.fromDevice} is not knwown")
|
||||||
|
// TODO cancel?
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReadyReceived(event.senderId, readyReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onRoomDoneReceived(event: Event) {
|
||||||
|
val doneReq = event.getClearContent().toModel<MessageVerificationDoneContent>()
|
||||||
|
?.copy(
|
||||||
|
// relates_to is in clear in encrypted payload
|
||||||
|
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
|
||||||
|
)
|
||||||
|
|
||||||
|
if (doneReq == null || doneReq.isValid().not() || event.senderId == null) {
|
||||||
|
// ignore
|
||||||
|
Timber.e("## SAS Received invalid Done request")
|
||||||
|
// TODO should we cancel?
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDoneReceived(event.senderId, doneReq)
|
||||||
|
}
|
||||||
|
|
||||||
private fun onMacReceived(event: Event) {
|
private fun onMacReceived(event: Event) {
|
||||||
val macReq = event.getClearContent().toModel<KeyVerificationMac>()!!
|
val macReq = event.getClearContent().toModel<KeyVerificationMac>()!!
|
||||||
|
|
||||||
|
@ -481,12 +611,42 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleReadyReceived(senderId: String, readyReq: VerificationInfoReady) {
|
||||||
|
val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == readyReq.transactionID }
|
||||||
|
if (existingRequest == null) {
|
||||||
|
Timber.e("## SAS Received Ready for unknown request txId:${readyReq.transactionID} fromDevice ${readyReq.fromDevice}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updatePendingRequest(existingRequest.copy(readyInfo = readyReq))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDoneReceived(senderId: String, doneInfo: VerificationInfo) {
|
||||||
|
val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneInfo.transactionID }
|
||||||
|
if (existingRequest == null) {
|
||||||
|
Timber.e("## SAS Received Done for unknown request txId:${doneInfo.transactionID}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updatePendingRequest(existingRequest.copy(isSuccessful = true))
|
||||||
|
}
|
||||||
|
|
||||||
override fun getExistingTransaction(otherUser: String, tid: String): VerificationTransaction? {
|
override fun getExistingTransaction(otherUser: String, tid: String): VerificationTransaction? {
|
||||||
synchronized(lock = txMap) {
|
synchronized(lock = txMap) {
|
||||||
return txMap[otherUser]?.get(tid)
|
return txMap[otherUser]?.get(tid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getExistingVerificationRequest(otherUser: String): List<PendingVerificationRequest>? {
|
||||||
|
synchronized(lock = pendingRequests) {
|
||||||
|
return pendingRequests[otherUser]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getExistingVerificationRequest(otherUser: String, tid: String?): PendingVerificationRequest? {
|
||||||
|
synchronized(lock = pendingRequests) {
|
||||||
|
return tid?.let { tid -> pendingRequests[otherUser]?.firstOrNull { it.transactionId == tid } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getExistingTransactionsForUser(otherUser: String): Collection<VerificationTransaction>? {
|
private fun getExistingTransactionsForUser(otherUser: String): Collection<VerificationTransaction>? {
|
||||||
synchronized(txMap) {
|
synchronized(txMap) {
|
||||||
return txMap[otherUser]?.values
|
return txMap[otherUser]?.values
|
||||||
|
@ -536,28 +696,76 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback<String>?) {
|
override fun requestKeyVerificationInDMs(userId: String, roomId: String)
|
||||||
requestVerificationDMTask.configureWith(
|
: PendingVerificationRequest {
|
||||||
requestVerificationDMTask.createParamsAndLocalEcho(
|
Timber.i("## SAS Requesting verification to user: $userId in room $roomId")
|
||||||
roomId = roomId,
|
|
||||||
from = credentials.deviceId ?: "",
|
val requestsForUser = pendingRequests[userId]
|
||||||
methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS),
|
?: ArrayList<PendingVerificationRequest>().also {
|
||||||
to = userId,
|
pendingRequests[userId] = it
|
||||||
cryptoService = cryptoService
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.callback = object : MatrixCallback<SendResponse> {
|
|
||||||
override fun onSuccess(data: SendResponse) {
|
|
||||||
callback?.onSuccess(data.eventId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
val transport = sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId
|
||||||
callback?.onFailure(failure)
|
?: "", roomId, null)
|
||||||
|
|
||||||
|
// Cancel existing pending requests?
|
||||||
|
requestsForUser.forEach { existingRequest ->
|
||||||
|
existingRequest.transactionId?.let { tid ->
|
||||||
|
if (!existingRequest.isFinished) {
|
||||||
|
Timber.d("## SAS, cancelling pending requests to start a new one")
|
||||||
|
transport.cancelTransaction(tid, existingRequest.otherUserId, "", CancelCode.User)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
constraints = TaskConstraints(true)
|
}
|
||||||
retryCount = 3
|
|
||||||
}.executeBy(taskExecutor)
|
val localID = LocalEcho.createLocalEchoId()
|
||||||
|
|
||||||
|
val verificationRequest = PendingVerificationRequest(
|
||||||
|
ageLocalTs = System.currentTimeMillis(),
|
||||||
|
isIncoming = false,
|
||||||
|
localID = localID,
|
||||||
|
otherUserId = userId
|
||||||
|
)
|
||||||
|
|
||||||
|
transport.sendVerificationRequest(localID, userId, roomId) { syncedId, info ->
|
||||||
|
// We need to update with the syncedID
|
||||||
|
updatePendingRequest(verificationRequest.copy(
|
||||||
|
transactionId = syncedId,
|
||||||
|
requestInfo = info
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
requestsForUser.add(verificationRequest)
|
||||||
|
dispatchRequestAdded(verificationRequest)
|
||||||
|
|
||||||
|
return verificationRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun declineVerificationRequestInDMs(otherUserId: String, otherDeviceId: String, transactionId: String, roomId: String) {
|
||||||
|
sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId
|
||||||
|
?: "", roomId, null).cancelTransaction(transactionId, otherUserId, otherDeviceId, CancelCode.User)
|
||||||
|
|
||||||
|
getExistingVerificationRequest(otherUserId, transactionId)?.let {
|
||||||
|
updatePendingRequest(it.copy(
|
||||||
|
cancelConclusion = CancelCode.User
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePendingRequest(updated: PendingVerificationRequest) {
|
||||||
|
val requestsForUser = pendingRequests[updated.otherUserId]
|
||||||
|
?: ArrayList<PendingVerificationRequest>().also {
|
||||||
|
pendingRequests[updated.otherUserId] = it
|
||||||
|
}
|
||||||
|
val index = requestsForUser.indexOfFirst {
|
||||||
|
it.transactionId == updated.transactionId
|
||||||
|
|| it.transactionId == null && it.localID == updated.localID
|
||||||
|
}
|
||||||
|
if (index != -1) {
|
||||||
|
requestsForUser.removeAt(index)
|
||||||
|
}
|
||||||
|
requestsForUser.add(updated)
|
||||||
|
dispatchRequestUpdated(updated)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun beginKeyVerificationInDMs(method: String, transactionId: String, roomId: String,
|
override fun beginKeyVerificationInDMs(method: String, transactionId: String, roomId: String,
|
||||||
|
@ -572,7 +780,8 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
transactionId,
|
transactionId,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
otherDeviceId)
|
otherDeviceId)
|
||||||
tx.transport = sasTransportRoomMessageFactory.createTransport(roomId, cryptoService, tx)
|
tx.transport = sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId
|
||||||
|
?: "", roomId, tx)
|
||||||
addTransaction(tx)
|
addTransaction(tx)
|
||||||
|
|
||||||
tx.start()
|
tx.start()
|
||||||
|
@ -582,6 +791,36 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun readyPendingVerificationInDMs(otherUserId: String, roomId: String, transactionId: String): Boolean {
|
||||||
|
Timber.v("## SAS readyPendingVerificationInDMs $otherUserId room:$roomId tx:$transactionId")
|
||||||
|
// Let's find the related request
|
||||||
|
val existingRequest = getExistingVerificationRequest(otherUserId, transactionId)
|
||||||
|
if (existingRequest != null) {
|
||||||
|
// we need to send a ready event, with matching methods
|
||||||
|
val transport = sasTransportRoomMessageFactory.createTransport(credentials.userId, credentials.deviceId
|
||||||
|
?: "", roomId, null)
|
||||||
|
val methods = existingRequest.requestInfo?.methods?.intersect(listOf(KeyVerificationStart.VERIF_METHOD_SAS))?.toList()
|
||||||
|
if (methods.isNullOrEmpty()) {
|
||||||
|
Timber.i("Cannot ready this request, no common methods found txId:$transactionId")
|
||||||
|
// TODO buttons should not be shown in this case?
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// TODO this is not yet related to a transaction, maybe we should use another method like for cancel?
|
||||||
|
val readyMsg = transport.createReady(transactionId, credentials.deviceId ?: "", methods)
|
||||||
|
transport.sendToOther(EventType.KEY_VERIFICATION_READY, readyMsg,
|
||||||
|
SasVerificationTxState.None,
|
||||||
|
CancelCode.User,
|
||||||
|
null // TODO handle error?
|
||||||
|
)
|
||||||
|
updatePendingRequest(existingRequest.copy(readyInfo = readyMsg))
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
Timber.e("## SAS readyPendingVerificationInDMs Verification not found")
|
||||||
|
// :/ should not be possible... unless live observer very slow
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid
|
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid
|
||||||
*/
|
*/
|
||||||
|
@ -606,28 +845,4 @@ internal class DefaultSasVerificationService @Inject constructor(
|
||||||
this.removeTransaction(tx.otherUserId, tx.transactionId)
|
this.removeTransaction(tx.otherUserId, tx.transactionId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//
|
|
||||||
// fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode, roomId: String? = null) {
|
|
||||||
// val cancelMessage = KeyVerificationCancel.create(transactionId, code)
|
|
||||||
// val contentMap = MXUsersDevicesMap<Any>()
|
|
||||||
// contentMap.setObject(userId, userDevice, cancelMessage)
|
|
||||||
//
|
|
||||||
// if (roomId != null) {
|
|
||||||
//
|
|
||||||
// } else {
|
|
||||||
// sendToDeviceTask
|
|
||||||
// .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) {
|
|
||||||
// this.callback = object : MatrixCallback<Unit> {
|
|
||||||
// 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)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.matrix.android.internal.crypto.verification
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores current pending verification requests
|
||||||
|
*/
|
||||||
|
data class PendingVerificationRequest(
|
||||||
|
val ageLocalTs : Long,
|
||||||
|
val isIncoming: Boolean = false,
|
||||||
|
val localID: String = UUID.randomUUID().toString(),
|
||||||
|
val otherUserId: String,
|
||||||
|
val transactionId: String? = null,
|
||||||
|
val requestInfo: MessageVerificationRequestContent? = null,
|
||||||
|
val readyInfo: VerificationInfoReady? = null,
|
||||||
|
val cancelConclusion: CancelCode? = null,
|
||||||
|
val isSuccessful : Boolean = false
|
||||||
|
|
||||||
|
) {
|
||||||
|
|
||||||
|
val isReady: Boolean = readyInfo != null
|
||||||
|
val isSent: Boolean = transactionId != null
|
||||||
|
|
||||||
|
val isFinished: Boolean = isSuccessful || cancelConclusion != null
|
||||||
|
}
|
|
@ -169,6 +169,11 @@ internal abstract class SASVerificationTransaction(
|
||||||
} // if not wait for it
|
} // if not wait for it
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun shortCodeDoesNotMatch() {
|
||||||
|
Timber.v("## SAS short code do not match for id:$transactionId")
|
||||||
|
cancel(CancelCode.MismatchedSas)
|
||||||
|
}
|
||||||
|
|
||||||
override fun acceptVerificationEvent(senderId: String, info: VerificationInfo) {
|
override fun acceptVerificationEvent(senderId: String, info: VerificationInfo) {
|
||||||
when (info) {
|
when (info) {
|
||||||
is VerificationInfoStart -> onVerificationStart(info)
|
is VerificationInfoStart -> onVerificationStart(info)
|
||||||
|
@ -222,13 +227,14 @@ internal abstract class SASVerificationTransaction(
|
||||||
val keyIDNoPrefix = if (it.startsWith("ed25519:")) it.substring("ed25519:".length) else it
|
val keyIDNoPrefix = if (it.startsWith("ed25519:")) it.substring("ed25519:".length) else it
|
||||||
val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint()
|
val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint()
|
||||||
if (otherDeviceKey == null) {
|
if (otherDeviceKey == null) {
|
||||||
Timber.e("Verification: Could not find device $keyIDNoPrefix to verify")
|
Timber.e("## SAS Verification: Could not find device $keyIDNoPrefix to verify")
|
||||||
// just ignore and continue
|
// just ignore and continue
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it)
|
val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it)
|
||||||
if (mac != theirMac?.mac?.get(it)) {
|
if (mac != theirMac?.mac?.get(it)) {
|
||||||
// WRONG!
|
// WRONG!
|
||||||
|
Timber.e("## SAS Verification: mac mismatch for $otherDeviceKey with id $keyIDNoPrefix")
|
||||||
cancel(CancelCode.MismatchedKeys)
|
cancel(CancelCode.MismatchedKeys)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.crypto.verification
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SAS verification can be performed using toDevice events or via DM.
|
* SAS verification can be performed using toDevice events or via DM.
|
||||||
|
@ -33,7 +34,9 @@ internal interface SasTransport {
|
||||||
onErrorReason: CancelCode,
|
onErrorReason: CancelCode,
|
||||||
onDone: (() -> Unit)?)
|
onDone: (() -> Unit)?)
|
||||||
|
|
||||||
fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode)
|
fun sendVerificationRequest(localID: String, otherUserId: String, roomId: String, callback: (String?, MessageVerificationRequestContent?) -> Unit)
|
||||||
|
|
||||||
|
fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDevice: String, code: CancelCode)
|
||||||
|
|
||||||
fun done(transactionId: String)
|
fun done(transactionId: String)
|
||||||
/**
|
/**
|
||||||
|
@ -58,4 +61,6 @@ internal interface SasTransport {
|
||||||
shortAuthenticationStrings: List<String>) : VerificationInfoStart
|
shortAuthenticationStrings: List<String>) : VerificationInfoStart
|
||||||
|
|
||||||
fun createMac(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac
|
fun createMac(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac
|
||||||
|
|
||||||
|
fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,31 +15,36 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.matrix.android.internal.crypto.verification
|
package im.vector.matrix.android.internal.crypto.verification
|
||||||
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import android.content.Context
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.work.*
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import im.vector.matrix.android.R
|
||||||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
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.*
|
||||||
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.message.*
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
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.model.rest.KeyVerificationStart
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask
|
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
||||||
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
import im.vector.matrix.android.internal.worker.WorkManagerUtil
|
||||||
import im.vector.matrix.android.internal.task.TaskConstraints
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import kotlinx.coroutines.Dispatchers
|
||||||
import im.vector.matrix.android.internal.task.TaskThread
|
import kotlinx.coroutines.GlobalScope
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class SasTransportRoomMessage(
|
internal class SasTransportRoomMessage(
|
||||||
|
private val context: Context,
|
||||||
|
private val userId: String,
|
||||||
|
private val userDevice: String,
|
||||||
private val roomId: String,
|
private val roomId: String,
|
||||||
private val cryptoService: CryptoService,
|
private val monarchy: Monarchy,
|
||||||
private val tx: SASVerificationTransaction?,
|
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||||
private val sendVerificationMessageTask: SendVerificationMessageTask,
|
private val tx: SASVerificationTransaction?
|
||||||
private val taskExecutor: TaskExecutor
|
|
||||||
) : SasTransport {
|
) : SasTransport {
|
||||||
|
|
||||||
override fun sendToOther(type: String,
|
override fun sendToOther(type: String,
|
||||||
|
@ -49,83 +54,167 @@ internal class SasTransportRoomMessage(
|
||||||
onDone: (() -> Unit)?) {
|
onDone: (() -> Unit)?) {
|
||||||
Timber.d("## SAS sending msg type $type")
|
Timber.d("## SAS sending msg type $type")
|
||||||
Timber.v("## SAS sending msg info $verificationInfo")
|
Timber.v("## SAS sending msg info $verificationInfo")
|
||||||
sendVerificationMessageTask.configureWith(
|
val event = createEventAndLocalEcho(
|
||||||
sendVerificationMessageTask.createParamsAndLocalEcho(
|
type = type,
|
||||||
type,
|
roomId = roomId,
|
||||||
roomId,
|
content = verificationInfo.toEventContent()!!
|
||||||
verificationInfo.toEventContent()!!,
|
|
||||||
cryptoService
|
|
||||||
)
|
)
|
||||||
) {
|
|
||||||
callbackThread = TaskThread.DM_VERIF
|
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
|
||||||
executionThread = TaskThread.DM_VERIF
|
userId = userId,
|
||||||
constraints = TaskConstraints(true)
|
event = event
|
||||||
callback = object : MatrixCallback<SendResponse> {
|
))
|
||||||
override fun onSuccess(data: SendResponse) {
|
val enqueueInfo = enqueueSendWork(workerParams)
|
||||||
|
|
||||||
|
// I cannot just listen to the given work request, because when used in a uniqueWork,
|
||||||
|
// The callback is called while it is still Running ...
|
||||||
|
|
||||||
|
// Futures.addCallback(enqueueInfo.first.result, object : FutureCallback<Operation.State.SUCCESS> {
|
||||||
|
// override fun onSuccess(result: Operation.State.SUCCESS?) {
|
||||||
|
// if (onDone != null) {
|
||||||
|
// onDone()
|
||||||
|
// } else {
|
||||||
|
// tx?.state = nextState
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// override fun onFailure(t: Throwable) {
|
||||||
|
// Timber.e("## SAS verification [${tx?.transactionId}] failed to send toDevice in state : ${tx?.state}, reason: ${t.localizedMessage}")
|
||||||
|
// tx?.cancel(onErrorReason)
|
||||||
|
// }
|
||||||
|
// }, listenerExecutor)
|
||||||
|
|
||||||
|
val workLiveData = WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork")
|
||||||
|
|
||||||
|
val observer = object : Observer<List<WorkInfo>> {
|
||||||
|
override fun onChanged(workInfoList: List<WorkInfo>?) {
|
||||||
|
workInfoList
|
||||||
|
?.filter { it.state == WorkInfo.State.SUCCEEDED }
|
||||||
|
?.firstOrNull { it.id == enqueueInfo.second }
|
||||||
|
?.let { wInfo ->
|
||||||
|
if (wInfo.outputData.getBoolean("failed", false)) {
|
||||||
|
Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}")
|
||||||
|
tx?.cancel(onErrorReason)
|
||||||
|
} else {
|
||||||
if (onDone != null) {
|
if (onDone != null) {
|
||||||
onDone()
|
onDone()
|
||||||
} else {
|
} else {
|
||||||
tx?.state = nextState
|
tx?.state = nextState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
workLiveData.removeObserver(this)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) {
|
// TODO listen to DB to get synced info
|
||||||
Timber.d("## SAS canceling transaction $transactionId for reason $code")
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
sendVerificationMessageTask.configureWith(
|
workLiveData.observeForever(observer)
|
||||||
sendVerificationMessageTask.createParamsAndLocalEcho(
|
}
|
||||||
EventType.KEY_VERIFICATION_CANCEL,
|
}
|
||||||
roomId,
|
|
||||||
MessageVerificationCancelContent.create(transactionId, code).toContent(),
|
override fun sendVerificationRequest(localID: String, otherUserId: String, roomId: String,
|
||||||
cryptoService
|
callback: (String?, MessageVerificationRequestContent?) -> Unit) {
|
||||||
|
val info = MessageVerificationRequestContent(
|
||||||
|
body = context.getString(R.string.key_verification_request_fallback_message, userId),
|
||||||
|
fromDevice = userDevice,
|
||||||
|
toUserId = otherUserId,
|
||||||
|
methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS)
|
||||||
)
|
)
|
||||||
) {
|
val content = info.toContent()
|
||||||
callbackThread = TaskThread.DM_VERIF
|
|
||||||
executionThread = TaskThread.DM_VERIF
|
val event = createEventAndLocalEcho(
|
||||||
constraints = TaskConstraints(true)
|
localID,
|
||||||
retryCount = 3
|
EventType.MESSAGE,
|
||||||
callback = object : MatrixCallback<SendResponse> {
|
roomId,
|
||||||
override fun onSuccess(data: SendResponse) {
|
content
|
||||||
Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}")
|
)
|
||||||
|
|
||||||
|
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
|
||||||
|
userId = userId,
|
||||||
|
event = event
|
||||||
|
))
|
||||||
|
|
||||||
|
val workRequest = WorkManagerUtil.matrixOneTimeWorkRequestBuilder<SendVerificationMessageWorker>()
|
||||||
|
.setConstraints(WorkManagerUtil.workConstraints)
|
||||||
|
.setInputData(workerParams)
|
||||||
|
.setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
WorkManager.getInstance(context)
|
||||||
|
.beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest)
|
||||||
|
.enqueue()
|
||||||
|
|
||||||
|
// I cannot just listen to the given work request, because when used in a uniqueWork,
|
||||||
|
// The callback is called while it is still Running ...
|
||||||
|
|
||||||
|
val workLiveData = WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork")
|
||||||
|
|
||||||
|
val observer = object : Observer<List<WorkInfo>> {
|
||||||
|
override fun onChanged(workInfoList: List<WorkInfo>?) {
|
||||||
|
workInfoList
|
||||||
|
?.filter { it.state == WorkInfo.State.SUCCEEDED }
|
||||||
|
?.firstOrNull { it.id == workRequest.id }
|
||||||
|
?.let { wInfo ->
|
||||||
|
if (wInfo.outputData.getBoolean("failed", false)) {
|
||||||
|
callback(null, null)
|
||||||
|
} else if (wInfo.outputData.getString(localID) != null) {
|
||||||
|
callback(wInfo.outputData.getString(localID), info)
|
||||||
|
} else {
|
||||||
|
callback(null, null)
|
||||||
|
}
|
||||||
|
workLiveData.removeObserver(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
// TODO listen to DB to get synced info
|
||||||
Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.")
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
workLiveData.observeForever(observer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.executeBy(taskExecutor)
|
override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDevice: String, code: CancelCode) {
|
||||||
|
Timber.d("## SAS canceling transaction $transactionId for reason $code")
|
||||||
|
val event = createEventAndLocalEcho(
|
||||||
|
type = EventType.KEY_VERIFICATION_CANCEL,
|
||||||
|
roomId = roomId,
|
||||||
|
content = MessageVerificationCancelContent.create(transactionId, code).toContent()
|
||||||
|
)
|
||||||
|
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
|
||||||
|
userId = this.userId,
|
||||||
|
event = event
|
||||||
|
))
|
||||||
|
enqueueSendWork(workerParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun done(transactionId: String) {
|
override fun done(transactionId: String) {
|
||||||
sendVerificationMessageTask.configureWith(
|
val event = createEventAndLocalEcho(
|
||||||
sendVerificationMessageTask.createParamsAndLocalEcho(
|
type = EventType.KEY_VERIFICATION_DONE,
|
||||||
EventType.KEY_VERIFICATION_DONE,
|
roomId = roomId,
|
||||||
roomId,
|
content = MessageVerificationDoneContent(
|
||||||
MessageVerificationDoneContent(
|
|
||||||
relatesTo = RelationDefaultContent(
|
relatesTo = RelationDefaultContent(
|
||||||
RelationType.REFERENCE,
|
RelationType.REFERENCE,
|
||||||
transactionId
|
transactionId
|
||||||
)
|
)
|
||||||
).toContent(),
|
).toContent()
|
||||||
cryptoService
|
|
||||||
)
|
)
|
||||||
) {
|
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
|
||||||
callbackThread = TaskThread.DM_VERIF
|
userId = userId,
|
||||||
executionThread = TaskThread.DM_VERIF
|
event = event
|
||||||
constraints = TaskConstraints(true)
|
))
|
||||||
retryCount = 3
|
enqueueSendWork(workerParams)
|
||||||
}
|
}
|
||||||
.executeBy(taskExecutor)
|
|
||||||
|
private fun enqueueSendWork(workerParams: Data): Pair<Operation, UUID> {
|
||||||
|
val workRequest = WorkManagerUtil.matrixOneTimeWorkRequestBuilder<SendVerificationMessageWorker>()
|
||||||
|
.setConstraints(WorkManagerUtil.workConstraints)
|
||||||
|
.setInputData(workerParams)
|
||||||
|
.setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
|
||||||
|
.build()
|
||||||
|
return WorkManager.getInstance(context)
|
||||||
|
.beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest)
|
||||||
|
.enqueue() to workRequest.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createAccept(tid: String,
|
override fun createAccept(tid: String,
|
||||||
|
@ -167,16 +256,40 @@ internal class SasTransportRoomMessage(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady {
|
||||||
|
return MessageVerificationReadyContent(
|
||||||
|
fromDevice = fromDevice,
|
||||||
|
relatesTo = RelationDefaultContent(
|
||||||
|
type = RelationType.REFERENCE,
|
||||||
|
eventId = tid
|
||||||
|
),
|
||||||
|
methods = methods
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createEventAndLocalEcho(localID: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event {
|
||||||
|
return Event(
|
||||||
|
roomId = roomId,
|
||||||
|
originServerTs = System.currentTimeMillis(),
|
||||||
|
senderId = userId,
|
||||||
|
eventId = localID,
|
||||||
|
type = type,
|
||||||
|
content = content,
|
||||||
|
unsignedData = UnsignedData(age = null, transactionId = localID)
|
||||||
|
).also {
|
||||||
|
localEchoEventFactory.saveLocalEcho(monarchy, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class SasTransportRoomMessageFactory @Inject constructor(
|
internal class SasTransportRoomMessageFactory @Inject constructor(
|
||||||
private val sendVerificationMessageTask: DefaultSendVerificationMessageTask,
|
private val context: Context,
|
||||||
private val taskExecutor: TaskExecutor) {
|
private val monarchy: Monarchy,
|
||||||
|
private val localEchoEventFactory: LocalEchoEventFactory) {
|
||||||
|
|
||||||
fun createTransport(roomId: String,
|
fun createTransport(userId: String, userDevice: String, roomId: String, tx: SASVerificationTransaction?
|
||||||
cryptoService: CryptoService,
|
|
||||||
tx: SASVerificationTransaction?
|
|
||||||
): SasTransportRoomMessage {
|
): SasTransportRoomMessage {
|
||||||
return SasTransportRoomMessage(roomId, cryptoService, tx, sendVerificationMessageTask, taskExecutor)
|
return SasTransportRoomMessage(context, userId, userDevice, roomId, monarchy, localEchoEventFactory, tx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
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.EventType
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.*
|
import im.vector.matrix.android.internal.crypto.model.rest.*
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||||
|
@ -33,6 +34,11 @@ internal class SasTransportToDevice(
|
||||||
private var taskExecutor: TaskExecutor
|
private var taskExecutor: TaskExecutor
|
||||||
) : SasTransport {
|
) : SasTransport {
|
||||||
|
|
||||||
|
override fun sendVerificationRequest(localID: String, otherUserId: String, roomId: String,
|
||||||
|
callback: (String?, MessageVerificationRequestContent?) -> Unit) {
|
||||||
|
// TODO "not implemented"
|
||||||
|
}
|
||||||
|
|
||||||
override fun sendToOther(type: String,
|
override fun sendToOther(type: String,
|
||||||
verificationInfo: VerificationInfo,
|
verificationInfo: VerificationInfo,
|
||||||
nextState: SasVerificationTxState,
|
nextState: SasVerificationTxState,
|
||||||
|
@ -72,11 +78,11 @@ internal class SasTransportToDevice(
|
||||||
// To device do not do anything here
|
// To device do not do anything here
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) {
|
override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDevice: String, code: CancelCode) {
|
||||||
Timber.d("## SAS canceling transaction $transactionId for reason $code")
|
Timber.d("## SAS canceling transaction $transactionId for reason $code")
|
||||||
val cancelMessage = KeyVerificationCancel.create(transactionId, code)
|
val cancelMessage = KeyVerificationCancel.create(transactionId, code)
|
||||||
val contentMap = MXUsersDevicesMap<Any>()
|
val contentMap = MXUsersDevicesMap<Any>()
|
||||||
contentMap.setObject(userId, userDevice, cancelMessage)
|
contentMap.setObject(otherUserId, otherUserDevice, cancelMessage)
|
||||||
sendToDeviceTask
|
sendToDeviceTask
|
||||||
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) {
|
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) {
|
||||||
this.callback = object : MatrixCallback<Unit> {
|
this.callback = object : MatrixCallback<Unit> {
|
||||||
|
@ -126,6 +132,14 @@ internal class SasTransportToDevice(
|
||||||
messageAuthenticationCodes,
|
messageAuthenticationCodes,
|
||||||
shortAuthenticationStrings)
|
shortAuthenticationStrings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady {
|
||||||
|
return KeyVerificationReady(
|
||||||
|
transactionID = tid,
|
||||||
|
fromDevice = fromDevice,
|
||||||
|
methods = methods
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class SasTransportToDeviceFactory @Inject constructor(
|
internal class SasTransportToDeviceFactory @Inject constructor(
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.matrix.android.internal.crypto.verification
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.failure.shouldBeRetried
|
||||||
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask
|
||||||
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
|
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class SendVerificationMessageWorker constructor(context: Context, params: WorkerParameters)
|
||||||
|
: CoroutineWorker(context, params) {
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class Params(
|
||||||
|
val userId: String,
|
||||||
|
val event: Event
|
||||||
|
)
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var sendVerificationMessageTask: SendVerificationMessageTask
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var cryptoService: CryptoService
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
val errorOutputData = Data.Builder().putBoolean("failed", true).build()
|
||||||
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
|
?: return Result.success(errorOutputData)
|
||||||
|
|
||||||
|
val sessionComponent = getSessionComponent(params.userId)
|
||||||
|
?: return Result.success(errorOutputData).also {
|
||||||
|
// TODO, can this happen? should I update local echo?
|
||||||
|
Timber.e("Unknown Session, cannot send message, userId:${params.userId}")
|
||||||
|
}
|
||||||
|
sessionComponent.inject(this)
|
||||||
|
val localId = params.event.eventId ?: ""
|
||||||
|
return try {
|
||||||
|
val eventId = sendVerificationMessageTask.execute(
|
||||||
|
SendVerificationMessageTask.Params(
|
||||||
|
event = params.event,
|
||||||
|
cryptoService = cryptoService
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Result.success(Data.Builder().putString(localId, eventId).build())
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
if (exception.shouldBeRetried()) {
|
||||||
|
Result.retry()
|
||||||
|
} else {
|
||||||
|
Result.success(errorOutputData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,8 +18,9 @@ package im.vector.matrix.android.internal.crypto.verification
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceObject
|
import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceObject
|
||||||
|
|
||||||
internal interface VerificationInfo {
|
interface VerificationInfo {
|
||||||
fun toEventContent(): Content? = null
|
fun toEventContent(): Content? = null
|
||||||
fun toSendToDeviceObject(): SendToDeviceObject? = null
|
fun toSendToDeviceObject(): SendToDeviceObject? = null
|
||||||
fun isValid() : Boolean
|
fun isValid() : Boolean
|
||||||
|
val transactionID: String?
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.crypto.verification
|
||||||
|
|
||||||
internal interface VerificationInfoAccept : VerificationInfo {
|
internal interface VerificationInfoAccept : VerificationInfo {
|
||||||
|
|
||||||
val transactionID: String?
|
override val transactionID: String?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device
|
* The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device
|
||||||
|
|
|
@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.crypto.verification
|
||||||
|
|
||||||
internal interface VerificationInfoCancel : VerificationInfo {
|
internal interface VerificationInfoCancel : VerificationInfo {
|
||||||
|
|
||||||
val transactionID: String?
|
override val transactionID: String?
|
||||||
/**
|
/**
|
||||||
* machine-readable reason for cancelling, see [CancelCode]
|
* machine-readable reason for cancelling, see [CancelCode]
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -20,7 +20,7 @@ package im.vector.matrix.android.internal.crypto.verification
|
||||||
*/
|
*/
|
||||||
internal interface VerificationInfoKey : VerificationInfo {
|
internal interface VerificationInfoKey : VerificationInfo {
|
||||||
|
|
||||||
val transactionID: String?
|
override val transactionID: String?
|
||||||
/**
|
/**
|
||||||
* The device’s ephemeral public key, as an unpadded base64 string
|
* The device’s ephemeral public key, as an unpadded base64 string
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.crypto.verification
|
||||||
|
|
||||||
internal interface VerificationInfoMac : VerificationInfo {
|
internal interface VerificationInfoMac : VerificationInfo {
|
||||||
|
|
||||||
val transactionID: String?
|
override val transactionID: String?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A map of key ID to the MAC of the key, as an unpadded base64 string, calculated using the MAC key
|
* A map of key ID to the MAC of the key, as an unpadded base64 string, calculated using the MAC key
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.matrix.android.internal.crypto.verification
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A new event type is added to the key verification framework: m.key.verification.ready,
|
||||||
|
* which may be sent by the target of the m.key.verification.request message, upon receipt of the m.key.verification.request event.
|
||||||
|
*
|
||||||
|
* The m.key.verification.ready event is optional; the recipient of the m.key.verification.request event may respond directly
|
||||||
|
* with a m.key.verification.start event instead.
|
||||||
|
*/
|
||||||
|
interface VerificationInfoReady : VerificationInfo {
|
||||||
|
|
||||||
|
override val transactionID: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the device that sent the m.key.verification.ready message
|
||||||
|
*/
|
||||||
|
val fromDevice: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of verification methods that the device supports
|
||||||
|
*/
|
||||||
|
val methods: List<String>?
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface MessageVerificationReadyFactory {
|
||||||
|
fun create(tid: String, methods: List<String>, fromDevice: String): VerificationInfoReady
|
||||||
|
}
|
|
@ -28,7 +28,7 @@ internal interface VerificationInfoStart : VerificationInfo {
|
||||||
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid.
|
* 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.
|
* Alice’s device should record this ID and use it in future messages in this transaction.
|
||||||
*/
|
*/
|
||||||
val transactionID: String?
|
override val transactionID: String?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of key agreement protocols that Alice’s client understands.
|
* An array of key agreement protocols that Alice’s client understands.
|
||||||
|
|
|
@ -17,38 +17,31 @@ package im.vector.matrix.android.internal.crypto.verification
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
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.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.internal.crypto.tasks.DefaultRoomVerificationUpdateTask
|
||||||
import im.vector.matrix.android.api.session.room.model.message.*
|
import im.vector.matrix.android.internal.crypto.tasks.RoomVerificationUpdateTask
|
||||||
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.RealmLiveEntityObserver
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
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.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.query.types
|
import im.vector.matrix.android.internal.database.query.types
|
||||||
import im.vector.matrix.android.internal.di.DeviceId
|
|
||||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||||
import im.vector.matrix.android.internal.di.UserId
|
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
import io.realm.OrderedCollectionChangeSet
|
import io.realm.OrderedCollectionChangeSet
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import io.realm.RealmResults
|
import io.realm.RealmResults
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
internal class VerificationMessageLiveObserver @Inject constructor(
|
internal class VerificationMessageLiveObserver @Inject constructor(
|
||||||
@SessionDatabase realmConfiguration: RealmConfiguration,
|
@SessionDatabase realmConfiguration: RealmConfiguration,
|
||||||
@UserId private val userId: String,
|
private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask,
|
||||||
@DeviceId private val deviceId: String?,
|
|
||||||
private val cryptoService: CryptoService,
|
private val cryptoService: CryptoService,
|
||||||
private val sasVerificationService: DefaultSasVerificationService,
|
private val sasVerificationService: DefaultSasVerificationService,
|
||||||
private val taskExecutor: TaskExecutor
|
private val taskExecutor: TaskExecutor
|
||||||
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
||||||
|
|
||||||
override val query = Monarchy.Query<EventEntity> {
|
override val query = Monarchy.Query {
|
||||||
EventEntity.types(it, listOf(
|
EventEntity.types(it, listOf(
|
||||||
EventType.KEY_VERIFICATION_START,
|
EventType.KEY_VERIFICATION_START,
|
||||||
EventType.KEY_VERIFICATION_ACCEPT,
|
EventType.KEY_VERIFICATION_ACCEPT,
|
||||||
|
@ -56,16 +49,14 @@ internal class VerificationMessageLiveObserver @Inject constructor(
|
||||||
EventType.KEY_VERIFICATION_MAC,
|
EventType.KEY_VERIFICATION_MAC,
|
||||||
EventType.KEY_VERIFICATION_CANCEL,
|
EventType.KEY_VERIFICATION_CANCEL,
|
||||||
EventType.KEY_VERIFICATION_DONE,
|
EventType.KEY_VERIFICATION_DONE,
|
||||||
|
EventType.KEY_VERIFICATION_READY,
|
||||||
EventType.MESSAGE,
|
EventType.MESSAGE,
|
||||||
EventType.ENCRYPTED)
|
EventType.ENCRYPTED)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val transactionsHandledByOtherDevice = ArrayList<String>()
|
|
||||||
|
|
||||||
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||||
// TODO do that in a task
|
// Should we ignore when it's an initial sync?
|
||||||
// TODO how to ignore when it's an initial sync?
|
|
||||||
val events = changeSet.insertions
|
val events = changeSet.insertions
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.mapNotNull { results[it]?.asDomain() }
|
.mapNotNull { results[it]?.asDomain() }
|
||||||
|
@ -75,102 +66,8 @@ internal class VerificationMessageLiveObserver @Inject constructor(
|
||||||
}
|
}
|
||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
// TODO ignore initial sync or back pagination?
|
roomVerificationUpdateTask.configureWith(
|
||||||
|
RoomVerificationUpdateTask.Params(events, sasVerificationService, cryptoService)
|
||||||
val now = System.currentTimeMillis()
|
).executeBy(taskExecutor)
|
||||||
val tooInThePast = now - (10 * 60 * 1000)
|
|
||||||
val fiveMinInMs = 5 * 60 * 1000
|
|
||||||
val tooInTheFuture = System.currentTimeMillis() + fiveMinInMs
|
|
||||||
|
|
||||||
events.forEach { event ->
|
|
||||||
Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
|
|
||||||
Timber.v("## SAS Verification live observer: received msgId: $event")
|
|
||||||
|
|
||||||
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
|
|
||||||
// the message should be ignored by the receiver.
|
|
||||||
val ageLocalTs = event.ageLocalTs
|
|
||||||
if (ageLocalTs != null && (now - ageLocalTs) > fiveMinInMs) {
|
|
||||||
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is too old (age: ${(now - ageLocalTs)})")
|
|
||||||
return@forEach
|
|
||||||
} else {
|
|
||||||
val eventOrigin = event.originServerTs ?: -1
|
|
||||||
if (eventOrigin < tooInThePast || eventOrigin > tooInTheFuture) {
|
|
||||||
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is too old (ts: $eventOrigin")
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// decrypt if needed?
|
|
||||||
if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
|
||||||
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
|
||||||
// for now decrypt sync
|
|
||||||
try {
|
|
||||||
val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString())
|
|
||||||
event.mxDecryptionResult = OlmDecryptionResult(
|
|
||||||
payload = result.clearEvent,
|
|
||||||
senderKey = result.senderCurve25519Key,
|
|
||||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
|
||||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
|
||||||
)
|
|
||||||
} catch (e: MXCryptoError) {
|
|
||||||
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
|
|
||||||
|
|
||||||
if (event.senderId == userId) {
|
|
||||||
// If it's send from me, we need to keep track of Requests or Start
|
|
||||||
// done from another device of mine
|
|
||||||
|
|
||||||
if (EventType.MESSAGE == event.type) {
|
|
||||||
val msgType = event.getClearContent().toModel<MessageContent>()?.type
|
|
||||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
|
|
||||||
event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
|
|
||||||
if (it.fromDevice != deviceId) {
|
|
||||||
// The verification is requested from another device
|
|
||||||
Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ")
|
|
||||||
event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (EventType.KEY_VERIFICATION_START == event.type) {
|
|
||||||
event.getClearContent().toModel<MessageVerificationStartContent>()?.let {
|
|
||||||
if (it.fromDevice != deviceId) {
|
|
||||||
// The verification is started from another device
|
|
||||||
Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ")
|
|
||||||
it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) {
|
|
||||||
event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId?.let {
|
|
||||||
transactionsHandledByOtherDevice.remove(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
|
|
||||||
val relatesTo = event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId
|
|
||||||
if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) {
|
|
||||||
// Ignore this event, it is directed to another of my devices
|
|
||||||
Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ")
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
when (event.getClearType()) {
|
|
||||||
EventType.KEY_VERIFICATION_START,
|
|
||||||
EventType.KEY_VERIFICATION_ACCEPT,
|
|
||||||
EventType.KEY_VERIFICATION_KEY,
|
|
||||||
EventType.KEY_VERIFICATION_MAC,
|
|
||||||
EventType.KEY_VERIFICATION_CANCEL,
|
|
||||||
EventType.KEY_VERIFICATION_DONE -> {
|
|
||||||
sasVerificationService.onRoomEvent(event)
|
|
||||||
}
|
|
||||||
EventType.MESSAGE -> {
|
|
||||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
|
|
||||||
sasVerificationService.onRoomRequestReceived(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import dagger.Component
|
||||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.internal.crypto.CryptoModule
|
import im.vector.matrix.android.internal.crypto.CryptoModule
|
||||||
|
import im.vector.matrix.android.internal.crypto.verification.SendVerificationMessageWorker
|
||||||
import im.vector.matrix.android.internal.di.MatrixComponent
|
import im.vector.matrix.android.internal.di.MatrixComponent
|
||||||
import im.vector.matrix.android.internal.di.SessionAssistedInjectModule
|
import im.vector.matrix.android.internal.di.SessionAssistedInjectModule
|
||||||
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
|
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
|
||||||
|
@ -98,6 +99,8 @@ internal interface SessionComponent {
|
||||||
|
|
||||||
fun inject(addHttpPusherWorker: AddHttpPusherWorker)
|
fun inject(addHttpPusherWorker: AddHttpPusherWorker)
|
||||||
|
|
||||||
|
fun inject(sendVerificationMessageWorker: SendVerificationMessageWorker)
|
||||||
|
|
||||||
@Component.Factory
|
@Component.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
|
|
|
@ -71,6 +71,20 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getExistingDirectRoomWithUser(otherUserId: String): Room? {
|
||||||
|
Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||||
|
val roomId = RoomSummaryEntity.where(realm)
|
||||||
|
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
|
||||||
|
.findAll()?.let { dms ->
|
||||||
|
dms.firstOrNull {
|
||||||
|
it.otherMemberIds.contains(otherUserId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?.roomId ?: return null
|
||||||
|
return RoomEntity.where(realm, roomId).findFirst()?.let { roomFactory.create(roomId) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getRoomSummary(roomIdOrAlias: String): RoomSummary? {
|
override fun getRoomSummary(roomIdOrAlias: String): RoomSummary? {
|
||||||
return monarchy
|
return monarchy
|
||||||
.fetchCopyMap({
|
.fetchCopyMap({
|
||||||
|
|
|
@ -20,8 +20,7 @@ import android.content.Context
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.failure.Failure
|
import im.vector.matrix.android.api.failure.shouldBeRetried
|
||||||
import im.vector.matrix.android.api.failure.MatrixError
|
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
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.Event
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
|
@ -77,11 +76,6 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Throwable.shouldBeRetried(): Boolean {
|
|
||||||
return this is Failure.NetworkConnection
|
|
||||||
|| (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) {
|
private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) {
|
||||||
localEchoUpdater.updateSendState(eventId, SendState.SENDING)
|
localEchoUpdater.updateSendState(eventId, SendState.SENDING)
|
||||||
executeRequest<SendResponse> {
|
executeRequest<SendResponse> {
|
||||||
|
|
|
@ -23,10 +23,7 @@ import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.multibindings.IntoMap
|
import dagger.multibindings.IntoMap
|
||||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
|
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
|
||||||
import im.vector.riotx.features.crypto.verification.SASVerificationIncomingFragment
|
import im.vector.riotx.features.crypto.verification.*
|
||||||
import im.vector.riotx.features.crypto.verification.SASVerificationShortCodeFragment
|
|
||||||
import im.vector.riotx.features.crypto.verification.SASVerificationStartFragment
|
|
||||||
import im.vector.riotx.features.crypto.verification.SASVerificationVerifiedFragment
|
|
||||||
import im.vector.riotx.features.home.HomeDetailFragment
|
import im.vector.riotx.features.home.HomeDetailFragment
|
||||||
import im.vector.riotx.features.home.HomeDrawerFragment
|
import im.vector.riotx.features.home.HomeDrawerFragment
|
||||||
import im.vector.riotx.features.home.LoadingFragment
|
import im.vector.riotx.features.home.LoadingFragment
|
||||||
|
@ -272,4 +269,24 @@ interface FragmentModule {
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(SoftLogoutFragment::class)
|
@FragmentKey(SoftLogoutFragment::class)
|
||||||
fun bindSoftLogoutFragment(fragment: SoftLogoutFragment): Fragment
|
fun bindSoftLogoutFragment(fragment: SoftLogoutFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(VerificationRequestFragment::class)
|
||||||
|
fun bindVerificationRequestFragment(fragment: VerificationRequestFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(VerificationChooseMethodFragment::class)
|
||||||
|
fun bindVerificationMethodChooserFragment(fragment: VerificationChooseMethodFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(SASVerificationCodeFragment::class)
|
||||||
|
fun bindVerificationSasCodeFragment(fragment: SASVerificationCodeFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(VerificationConclusionFragment::class)
|
||||||
|
fun bindVerificationSasConclusionFragment(fragment: VerificationConclusionFragment): Fragment
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import im.vector.riotx.core.error.ErrorFormatter
|
||||||
import im.vector.riotx.core.preference.UserAvatarPreference
|
import im.vector.riotx.core.preference.UserAvatarPreference
|
||||||
import im.vector.riotx.features.MainActivity
|
import im.vector.riotx.features.MainActivity
|
||||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||||
|
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
|
||||||
import im.vector.riotx.features.home.HomeActivity
|
import im.vector.riotx.features.home.HomeActivity
|
||||||
import im.vector.riotx.features.home.HomeModule
|
import im.vector.riotx.features.home.HomeModule
|
||||||
import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity
|
import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity
|
||||||
|
@ -133,6 +134,8 @@ interface ScreenComponent {
|
||||||
|
|
||||||
fun inject(activity: SoftLogoutActivity)
|
fun inject(activity: SoftLogoutActivity)
|
||||||
|
|
||||||
|
fun inject(verificationBottomSheet: VerificationBottomSheet)
|
||||||
|
|
||||||
fun inject(permalinkHandlerActivity: PermalinkHandlerActivity)
|
fun inject(permalinkHandlerActivity: PermalinkHandlerActivity)
|
||||||
|
|
||||||
@Component.Factory
|
@Component.Factory
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.riotx.core.utils
|
||||||
|
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.style.BulletSpan
|
||||||
|
import android.text.style.ClickableSpan
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import android.text.style.StyleSpan
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import me.gujun.android.span.Span
|
||||||
|
|
||||||
|
fun Spannable.styleMatchingText(match: String, typeFace: Int): Spannable {
|
||||||
|
if (match.isEmpty()) return this
|
||||||
|
indexOf(match).takeIf { it != -1 }?.let { start ->
|
||||||
|
this.setSpan(StyleSpan(typeFace), start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Spannable.colorizeMatchingText(match: String, @ColorInt color: Int): Spannable {
|
||||||
|
if (match.isEmpty()) return this
|
||||||
|
indexOf(match).takeIf { it != -1 }?.let { start ->
|
||||||
|
this.setSpan(ForegroundColorSpan(color), start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Spannable.tappableMatchingText(match: String, clickSpan: ClickableSpan): Spannable {
|
||||||
|
if (match.isEmpty()) return this
|
||||||
|
indexOf(match).takeIf { it != -1 }?.let { start ->
|
||||||
|
this.setSpan(clickSpan, start, start + match.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Span.bullet(text: CharSequence = "",
|
||||||
|
init: Span.() -> Unit = {}): Span = apply {
|
||||||
|
append(Span(parent = this).apply {
|
||||||
|
this.text = text
|
||||||
|
this.spans.add(BulletSpan())
|
||||||
|
init()
|
||||||
|
build()
|
||||||
|
})
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ import im.vector.riotx.core.extensions.observeEvent
|
||||||
import im.vector.riotx.core.platform.SimpleFragmentActivity
|
import im.vector.riotx.core.platform.SimpleFragmentActivity
|
||||||
import im.vector.riotx.core.platform.WaitingViewData
|
import im.vector.riotx.core.platform.WaitingViewData
|
||||||
|
|
||||||
|
// TODO Deprecated("replaced by bottomsheet UX")
|
||||||
class SASVerificationActivity : SimpleFragmentActivity() {
|
class SASVerificationActivity : SimpleFragmentActivity() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.riotx.features.crypto.verification
|
||||||
|
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import butterknife.BindView
|
||||||
|
import butterknife.OnClick
|
||||||
|
import com.airbnb.mvrx.*
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
|
import kotlinx.android.synthetic.main.fragment_bottom_sas_verification_code.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class SASVerificationCodeFragment @Inject constructor(
|
||||||
|
val viewModelFactory: SASVerificationCodeViewModel.Factory
|
||||||
|
) : VectorBaseFragment() {
|
||||||
|
|
||||||
|
override fun getLayoutResId() = R.layout.fragment_bottom_sas_verification_code
|
||||||
|
|
||||||
|
@BindView(R.id.sas_emoji_grid)
|
||||||
|
lateinit var emojiGrid: ViewGroup
|
||||||
|
|
||||||
|
@BindView(R.id.sas_decimal_code)
|
||||||
|
lateinit var decimalTextView: TextView
|
||||||
|
|
||||||
|
@BindView(R.id.emoji0)
|
||||||
|
lateinit var emoji0View: ViewGroup
|
||||||
|
@BindView(R.id.emoji1)
|
||||||
|
lateinit var emoji1View: ViewGroup
|
||||||
|
@BindView(R.id.emoji2)
|
||||||
|
lateinit var emoji2View: ViewGroup
|
||||||
|
@BindView(R.id.emoji3)
|
||||||
|
lateinit var emoji3View: ViewGroup
|
||||||
|
@BindView(R.id.emoji4)
|
||||||
|
lateinit var emoji4View: ViewGroup
|
||||||
|
@BindView(R.id.emoji5)
|
||||||
|
lateinit var emoji5View: ViewGroup
|
||||||
|
@BindView(R.id.emoji6)
|
||||||
|
lateinit var emoji6View: ViewGroup
|
||||||
|
|
||||||
|
private val viewModel by fragmentViewModel(SASVerificationCodeViewModel::class)
|
||||||
|
|
||||||
|
private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
|
|
||||||
|
if (state.supportsEmoji) {
|
||||||
|
decimalTextView.isVisible = false
|
||||||
|
when (val emojiDescription = state.emojiDescription) {
|
||||||
|
is Success -> {
|
||||||
|
sasLoadingProgress.isVisible = false
|
||||||
|
emojiGrid.isVisible = true
|
||||||
|
ButtonsVisibilityGroup.isVisible = true
|
||||||
|
emojiDescription.invoke().forEachIndexed { index, emojiRepresentation ->
|
||||||
|
when (index) {
|
||||||
|
0 -> {
|
||||||
|
emoji0View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
|
||||||
|
emoji0View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
|
||||||
|
}
|
||||||
|
1 -> {
|
||||||
|
emoji1View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
|
||||||
|
emoji1View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
emoji2View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
|
||||||
|
emoji2View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
|
||||||
|
}
|
||||||
|
3 -> {
|
||||||
|
emoji3View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
|
||||||
|
emoji3View.findViewById<TextView>(R.id.item_emoji_name_tv)?.setText(emojiRepresentation.nameResId)
|
||||||
|
}
|
||||||
|
4 -> {
|
||||||
|
emoji4View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
|
||||||
|
emoji4View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
|
||||||
|
}
|
||||||
|
5 -> {
|
||||||
|
emoji5View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
|
||||||
|
emoji5View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
|
||||||
|
}
|
||||||
|
6 -> {
|
||||||
|
emoji6View.findViewById<TextView>(R.id.item_emoji_tv).text = emojiRepresentation.emoji
|
||||||
|
emoji6View.findViewById<TextView>(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.isWaitingFromOther) {
|
||||||
|
// hide buttons
|
||||||
|
ButtonsVisibilityGroup.isInvisible = true
|
||||||
|
sasCodeWaitingPartnerText.isVisible = true
|
||||||
|
} else {
|
||||||
|
ButtonsVisibilityGroup.isVisible = true
|
||||||
|
sasCodeWaitingPartnerText.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Fail -> {
|
||||||
|
sasLoadingProgress.isVisible = false
|
||||||
|
emojiGrid.isInvisible = true
|
||||||
|
ButtonsVisibilityGroup.isInvisible = true
|
||||||
|
// TODO?
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
sasLoadingProgress.isVisible = true
|
||||||
|
emojiGrid.isInvisible = true
|
||||||
|
ButtonsVisibilityGroup.isInvisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Decimal
|
||||||
|
emojiGrid.isInvisible = true
|
||||||
|
decimalTextView.isVisible = true
|
||||||
|
val decimalCode = state.decimalDescription.invoke()
|
||||||
|
decimalTextView.text = decimalCode
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
if (state.isWaitingFromOther) {
|
||||||
|
// hide buttons
|
||||||
|
ButtonsVisibilityGroup.isInvisible = true
|
||||||
|
sasCodeWaitingPartnerText.isVisible = true
|
||||||
|
} else {
|
||||||
|
ButtonsVisibilityGroup.isVisible = decimalCode != null
|
||||||
|
sasCodeWaitingPartnerText.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.sas_request_continue_button)
|
||||||
|
fun onMatchButtonTapped() = withState(viewModel) { state ->
|
||||||
|
val otherUserId = state.otherUser?.id ?: return@withState
|
||||||
|
val txId = state.transactionId ?: return@withState
|
||||||
|
// UX echo
|
||||||
|
ButtonsVisibilityGroup.isInvisible = true
|
||||||
|
sasCodeWaitingPartnerText.isVisible = true
|
||||||
|
sharedViewModel.handle(VerificationAction.SASMatchAction(otherUserId, txId))
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.sas_request_cancel_button)
|
||||||
|
fun onDoNotMatchButtonTapped() = withState(viewModel) { state ->
|
||||||
|
val otherUserId = state.otherUser?.id ?: return@withState
|
||||||
|
val txId = state.transactionId ?: return@withState
|
||||||
|
// UX echo
|
||||||
|
ButtonsVisibilityGroup.isInvisible = true
|
||||||
|
sasCodeWaitingPartnerText.isVisible = true
|
||||||
|
sharedViewModel.handle(VerificationAction.SASDoNotMatchAction(otherUserId, txId))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.riotx.features.crypto.verification
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.*
|
||||||
|
import com.squareup.inject.assisted.Assisted
|
||||||
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation
|
||||||
|
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
|
||||||
|
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
|
||||||
|
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||||
|
import im.vector.matrix.android.api.util.MatrixItem
|
||||||
|
import im.vector.matrix.android.api.util.toMatrixItem
|
||||||
|
import im.vector.riotx.core.di.HasScreenInjector
|
||||||
|
import im.vector.riotx.core.platform.EmptyAction
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
|
|
||||||
|
data class SASVerificationCodeViewState(
|
||||||
|
val transactionId: String?,
|
||||||
|
val otherUser: MatrixItem? = null,
|
||||||
|
val supportsEmoji: Boolean = true,
|
||||||
|
val emojiDescription: Async<List<EmojiRepresentation>> = Uninitialized,
|
||||||
|
val decimalDescription: Async<String> = Uninitialized,
|
||||||
|
val isWaitingFromOther: Boolean = false
|
||||||
|
) : MvRxState
|
||||||
|
|
||||||
|
class SASVerificationCodeViewModel @AssistedInject constructor(
|
||||||
|
@Assisted initialState: SASVerificationCodeViewState,
|
||||||
|
private val session: Session
|
||||||
|
) : VectorViewModel<SASVerificationCodeViewState, EmptyAction>(initialState), SasVerificationService.SasVerificationListener {
|
||||||
|
|
||||||
|
init {
|
||||||
|
withState { state ->
|
||||||
|
refreshStateFromTx(session.getSasVerificationService()
|
||||||
|
.getExistingTransaction(state.otherUser?.id ?: "", state.transactionId
|
||||||
|
?: ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
session.getSasVerificationService().addListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
session.getSasVerificationService().removeListener(this)
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshStateFromTx(sasTx: SasVerificationTransaction?) {
|
||||||
|
when (sasTx?.state) {
|
||||||
|
SasVerificationTxState.None,
|
||||||
|
SasVerificationTxState.SendingStart,
|
||||||
|
SasVerificationTxState.Started,
|
||||||
|
SasVerificationTxState.OnStarted,
|
||||||
|
SasVerificationTxState.SendingAccept,
|
||||||
|
SasVerificationTxState.Accepted,
|
||||||
|
SasVerificationTxState.OnAccepted,
|
||||||
|
SasVerificationTxState.SendingKey,
|
||||||
|
SasVerificationTxState.KeySent,
|
||||||
|
SasVerificationTxState.OnKeyReceived -> {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
isWaitingFromOther = false,
|
||||||
|
supportsEmoji = sasTx.supportsEmoji(),
|
||||||
|
emojiDescription = Loading<List<EmojiRepresentation>>()
|
||||||
|
.takeIf { sasTx.supportsEmoji() }
|
||||||
|
?: Uninitialized,
|
||||||
|
decimalDescription = Loading<String>()
|
||||||
|
.takeIf { sasTx.supportsEmoji().not() }
|
||||||
|
?: Uninitialized
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SasVerificationTxState.ShortCodeReady -> {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
isWaitingFromOther = false,
|
||||||
|
supportsEmoji = sasTx.supportsEmoji(),
|
||||||
|
emojiDescription = if (sasTx.supportsEmoji()) Success(sasTx.getEmojiCodeRepresentation())
|
||||||
|
else Uninitialized,
|
||||||
|
decimalDescription = if (!sasTx.supportsEmoji()) Success(sasTx.getDecimalCodeRepresentation())
|
||||||
|
else Uninitialized
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SasVerificationTxState.ShortCodeAccepted,
|
||||||
|
SasVerificationTxState.SendingMac,
|
||||||
|
SasVerificationTxState.MacSent,
|
||||||
|
SasVerificationTxState.Verifying,
|
||||||
|
SasVerificationTxState.Verified -> {
|
||||||
|
setState {
|
||||||
|
copy(isWaitingFromOther = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SasVerificationTxState.Cancelled,
|
||||||
|
SasVerificationTxState.OnCancelled -> {
|
||||||
|
// The fragment should not be rendered in this state,
|
||||||
|
// it should have been replaced by a conclusion fragment
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
isWaitingFromOther = false,
|
||||||
|
supportsEmoji = sasTx.supportsEmoji(),
|
||||||
|
emojiDescription = Fail(Throwable("Transaction Cancelled")),
|
||||||
|
decimalDescription = Fail(Throwable("Transaction Cancelled"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
null -> {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
isWaitingFromOther = false,
|
||||||
|
emojiDescription = Fail(Throwable("Unknown Transaction")),
|
||||||
|
decimalDescription = Fail(Throwable("Unknown Transaction"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun transactionCreated(tx: SasVerificationTransaction) {
|
||||||
|
transactionUpdated(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun transactionUpdated(tx: SasVerificationTransaction) = withState { state ->
|
||||||
|
if (tx.transactionId == state.transactionId) {
|
||||||
|
refreshStateFromTx(tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AssistedInject.Factory
|
||||||
|
interface Factory {
|
||||||
|
fun create(initialState: SASVerificationCodeViewState): SASVerificationCodeViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<SASVerificationCodeViewModel, SASVerificationCodeViewState> {
|
||||||
|
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: SASVerificationCodeViewState): SASVerificationCodeViewModel? {
|
||||||
|
val factory = (viewModelContext as FragmentViewModelContext).fragment<SASVerificationCodeFragment>().viewModelFactory
|
||||||
|
return factory.create(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initialState(viewModelContext: ViewModelContext): SASVerificationCodeViewState? {
|
||||||
|
val args = viewModelContext.args<VerificationBottomSheet.VerificationArgs>()
|
||||||
|
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
|
||||||
|
val matrixItem = session.getUser(args.otherUserId)?.toMatrixItem()
|
||||||
|
|
||||||
|
return SASVerificationCodeViewState(
|
||||||
|
transactionId = args.verificationId,
|
||||||
|
otherUser = matrixItem
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: EmptyAction) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
// TODO Deprecated("replaced by bottomsheet UX")
|
||||||
class SASVerificationIncomingFragment @Inject constructor(
|
class SASVerificationIncomingFragment @Inject constructor(
|
||||||
private var avatarRenderer: AvatarRenderer
|
private var avatarRenderer: AvatarRenderer
|
||||||
) : VectorBaseFragment() {
|
) : VectorBaseFragment() {
|
||||||
|
|
|
@ -29,6 +29,7 @@ import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
// TODO Deprecated("replaced by bottomsheet UX")
|
||||||
class SASVerificationShortCodeFragment @Inject constructor(): VectorBaseFragment() {
|
class SASVerificationShortCodeFragment @Inject constructor(): VectorBaseFragment() {
|
||||||
|
|
||||||
private lateinit var viewModel: SasVerificationViewModel
|
private lateinit var viewModel: SasVerificationViewModel
|
||||||
|
|
|
@ -32,6 +32,7 @@ import im.vector.riotx.core.platform.VectorBaseActivity
|
||||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
// TODO Deprecated("replaced by bottomsheet UX")
|
||||||
class SASVerificationStartFragment @Inject constructor(): VectorBaseFragment() {
|
class SASVerificationStartFragment @Inject constructor(): VectorBaseFragment() {
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.fragment_sas_verification_start
|
override fun getLayoutResId() = R.layout.fragment_sas_verification_start
|
||||||
|
@ -91,7 +92,7 @@ class SASVerificationStartFragment @Inject constructor(): VectorBaseFragment() {
|
||||||
(requireActivity() as VectorBaseActivity).notImplemented()
|
(requireActivity() as VectorBaseActivity).notImplemented()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
viewModel.session.crypto?.getDeviceInfo(viewModel.otherUserId ?: "", viewModel.otherDeviceId
|
viewModel.session.crypto?.getDeviceInfo(viewModel.otherUserMxItem ?: "", viewModel.otherDeviceId
|
||||||
?: "", object : SimpleApiCallback<MXDeviceInfo>() {
|
?: "", object : SimpleApiCallback<MXDeviceInfo>() {
|
||||||
override fun onSuccess(info: MXDeviceInfo?) {
|
override fun onSuccess(info: MXDeviceInfo?) {
|
||||||
info?.let {
|
info?.let {
|
||||||
|
|
|
@ -21,6 +21,7 @@ import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
// TODO Deprecated("replaced by bottomsheet UX")
|
||||||
class SASVerificationVerifiedFragment @Inject constructor() : VectorBaseFragment() {
|
class SASVerificationVerifiedFragment @Inject constructor() : VectorBaseFragment() {
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.fragment_sas_verification_verified
|
override fun getLayoutResId() = R.layout.fragment_sas_verification_verified
|
||||||
|
|
|
@ -27,6 +27,7 @@ import im.vector.matrix.android.api.session.user.model.User
|
||||||
import im.vector.riotx.core.utils.LiveEvent
|
import im.vector.riotx.core.utils.LiveEvent
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
// TODO Deprecated("replaced by bottomsheet UX")
|
||||||
class SasVerificationViewModel @Inject constructor() : ViewModel(),
|
class SasVerificationViewModel @Inject constructor() : ViewModel(),
|
||||||
SasVerificationService.SasVerificationListener {
|
SasVerificationService.SasVerificationListener {
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,235 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.riotx.features.crypto.verification
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.text.toSpannable
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.transition.AutoTransition
|
||||||
|
import androidx.transition.TransitionManager
|
||||||
|
import butterknife.BindView
|
||||||
|
import butterknife.ButterKnife
|
||||||
|
import butterknife.Unbinder
|
||||||
|
import com.airbnb.mvrx.MvRx
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
|
import im.vector.riotx.core.extensions.commitTransactionNow
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
||||||
|
import im.vector.riotx.core.utils.colorizeMatchingText
|
||||||
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
import im.vector.riotx.features.themes.ThemeUtils
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import kotlinx.android.synthetic.main.bottom_sheet_verification.*
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class VerificationArgs(
|
||||||
|
val otherUserId: String,
|
||||||
|
val verificationId: String? = null,
|
||||||
|
val roomId: String? = null
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var verificationRequestViewModelFactory: VerificationRequestViewModel.Factory
|
||||||
|
@Inject
|
||||||
|
lateinit var verificationViewModelFactory: VerificationBottomSheetViewModel.Factory
|
||||||
|
@Inject
|
||||||
|
lateinit var avatarRenderer: AvatarRenderer
|
||||||
|
|
||||||
|
private val viewModel by fragmentViewModel(VerificationBottomSheetViewModel::class)
|
||||||
|
|
||||||
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
|
injector.inject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
@BindView(R.id.verificationRequestName)
|
||||||
|
lateinit var otherUserNameText: TextView
|
||||||
|
|
||||||
|
@BindView(R.id.verificationRequestAvatar)
|
||||||
|
lateinit var otherUserAvatarImageView: ImageView
|
||||||
|
|
||||||
|
private var unBinder: Unbinder? = null
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val view = inflater.inflate(R.layout.bottom_sheet_verification, container, false)
|
||||||
|
unBinder = ButterKnife.bind(this, view)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
unBinder?.unbind()
|
||||||
|
unBinder = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
viewModel.requestLiveData.observe(viewLifecycleOwner, Observer {
|
||||||
|
it.peekContent().let { va ->
|
||||||
|
when (va) {
|
||||||
|
is Success -> {
|
||||||
|
if (va.invoke() is VerificationAction.GotItConclusion) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) {
|
||||||
|
it.otherUserMxItem?.let { matrixItem ->
|
||||||
|
val displayName = matrixItem.displayName ?: ""
|
||||||
|
otherUserNameText.text = getString(R.string.verification_request_alert_title, displayName)
|
||||||
|
.toSpannable()
|
||||||
|
.colorizeMatchingText(displayName, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color))
|
||||||
|
|
||||||
|
avatarRenderer.render(matrixItem, otherUserAvatarImageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Did the request result in a SAS transaction?
|
||||||
|
if (it.sasTransactionState != null) {
|
||||||
|
when (it.sasTransactionState) {
|
||||||
|
SasVerificationTxState.None,
|
||||||
|
SasVerificationTxState.SendingStart,
|
||||||
|
SasVerificationTxState.Started,
|
||||||
|
SasVerificationTxState.OnStarted,
|
||||||
|
SasVerificationTxState.SendingAccept,
|
||||||
|
SasVerificationTxState.Accepted,
|
||||||
|
SasVerificationTxState.OnAccepted,
|
||||||
|
SasVerificationTxState.SendingKey,
|
||||||
|
SasVerificationTxState.KeySent,
|
||||||
|
SasVerificationTxState.OnKeyReceived,
|
||||||
|
SasVerificationTxState.ShortCodeReady,
|
||||||
|
SasVerificationTxState.ShortCodeAccepted,
|
||||||
|
SasVerificationTxState.SendingMac,
|
||||||
|
SasVerificationTxState.MacSent,
|
||||||
|
SasVerificationTxState.Verifying -> {
|
||||||
|
showFragment(SASVerificationCodeFragment::class, Bundle().apply {
|
||||||
|
putParcelable(MvRx.KEY_ARG, VerificationArgs(
|
||||||
|
it.otherUserMxItem?.id ?: "",
|
||||||
|
it.pendingRequest?.transactionId))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
SasVerificationTxState.Verified,
|
||||||
|
SasVerificationTxState.Cancelled,
|
||||||
|
SasVerificationTxState.OnCancelled -> {
|
||||||
|
showFragment(VerificationConclusionFragment::class, Bundle().apply {
|
||||||
|
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(
|
||||||
|
it.sasTransactionState == SasVerificationTxState.Verified,
|
||||||
|
it.cancelCode?.value))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return@withState
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point there is no transaction for this request
|
||||||
|
|
||||||
|
// Transaction has not yet started
|
||||||
|
if (it.pendingRequest?.cancelConclusion != null) {
|
||||||
|
// The request has been declined, we should dismiss
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's an outgoing
|
||||||
|
if (it.pendingRequest == null || !it.pendingRequest.isIncoming) {
|
||||||
|
Timber.v("## SAS show bottom sheet for outgoing request")
|
||||||
|
if (it.pendingRequest?.isReady == true) {
|
||||||
|
Timber.v("## SAS show bottom sheet for outgoing and ready request")
|
||||||
|
// Show choose method fragment with waiting
|
||||||
|
showFragment(VerificationChooseMethodFragment::class, Bundle().apply {
|
||||||
|
putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id
|
||||||
|
?: "", it.pendingRequest.transactionId))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Stay on the start fragment
|
||||||
|
showFragment(VerificationRequestFragment::class, Bundle().apply {
|
||||||
|
putParcelable(MvRx.KEY_ARG, VerificationArgs(
|
||||||
|
it.otherUserMxItem?.id ?: "",
|
||||||
|
it.pendingRequest?.transactionId,
|
||||||
|
it.roomId))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (it.pendingRequest.isIncoming) {
|
||||||
|
Timber.v("## SAS show bottom sheet for Incoming request")
|
||||||
|
// For incoming we can switch to choose method because ready is being sent or already sent
|
||||||
|
showFragment(VerificationChooseMethodFragment::class, Bundle().apply {
|
||||||
|
putParcelable(MvRx.KEY_ARG, VerificationArgs(it.otherUserMxItem?.id
|
||||||
|
?: "", it.pendingRequest.transactionId))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
super.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
|
||||||
|
if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
|
||||||
|
// We want to animate the bottomsheet bound changes
|
||||||
|
bottomSheetFragmentContainer.getParentCoordinatorLayout()?.let { coordinatorLayout ->
|
||||||
|
TransitionManager.beginDelayedTransition(coordinatorLayout, AutoTransition().apply { duration = 150 })
|
||||||
|
}
|
||||||
|
// Commit now, to ensure changes occurs before next rendering frame (or bottomsheet want animate)
|
||||||
|
childFragmentManager.commitTransactionNow {
|
||||||
|
replace(R.id.bottomSheetFragmentContainer,
|
||||||
|
fragmentClass.java,
|
||||||
|
bundle,
|
||||||
|
fragmentClass.simpleName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun withArgs(roomId: String, otherUserId: String, transactionId: String? = null): VerificationBottomSheet {
|
||||||
|
return VerificationBottomSheet().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs(
|
||||||
|
otherUserId = otherUserId,
|
||||||
|
roomId = roomId,
|
||||||
|
verificationId = transactionId
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.getParentCoordinatorLayout(): CoordinatorLayout? {
|
||||||
|
var current = this as? View
|
||||||
|
while (current != null) {
|
||||||
|
if (current is CoordinatorLayout) return current
|
||||||
|
current = current.parent as? View
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
|
@ -0,0 +1,172 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.riotx.features.crypto.verification
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.airbnb.mvrx.*
|
||||||
|
import com.squareup.inject.assisted.Assisted
|
||||||
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||||
|
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
|
||||||
|
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
|
||||||
|
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||||
|
import im.vector.matrix.android.api.util.MatrixItem
|
||||||
|
import im.vector.matrix.android.api.util.toMatrixItem
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
|
||||||
|
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
|
||||||
|
import im.vector.riotx.core.di.HasScreenInjector
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||||
|
import im.vector.riotx.core.utils.LiveEvent
|
||||||
|
|
||||||
|
data class VerificationBottomSheetViewState(
|
||||||
|
val otherUserMxItem: MatrixItem? = null,
|
||||||
|
val roomId: String? = null,
|
||||||
|
val pendingRequest: PendingVerificationRequest? = null,
|
||||||
|
val sasTransactionState: SasVerificationTxState? = null,
|
||||||
|
val cancelCode: CancelCode? = null
|
||||||
|
) : MvRxState
|
||||||
|
|
||||||
|
sealed class VerificationAction : VectorViewModelAction {
|
||||||
|
data class RequestVerificationByDM(val userID: String, val roomId: String?) : VerificationAction()
|
||||||
|
data class StartSASVerification(val userID: String, val pendingRequestTransactionId: String) : VerificationAction()
|
||||||
|
data class SASMatchAction(val userID: String, val sasTransactionId: String) : VerificationAction()
|
||||||
|
data class SASDoNotMatchAction(val userID: String, val sasTransactionId: String) : VerificationAction()
|
||||||
|
object GotItConclusion : VerificationAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: VerificationBottomSheetViewState,
|
||||||
|
private val session: Session)
|
||||||
|
: VectorViewModel<VerificationBottomSheetViewState, VerificationAction>(initialState),
|
||||||
|
SasVerificationService.SasVerificationListener {
|
||||||
|
|
||||||
|
// Can be used for several actions, for a one shot result
|
||||||
|
private val _requestLiveData = MutableLiveData<LiveEvent<Async<VerificationAction>>>()
|
||||||
|
val requestLiveData: LiveData<LiveEvent<Async<VerificationAction>>>
|
||||||
|
get() = _requestLiveData
|
||||||
|
|
||||||
|
init {
|
||||||
|
session.getSasVerificationService().addListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
session.getSasVerificationService().removeListener(this)
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
|
@AssistedInject.Factory
|
||||||
|
interface Factory {
|
||||||
|
fun create(initialState: VerificationBottomSheetViewState): VerificationBottomSheetViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<VerificationBottomSheetViewModel, VerificationBottomSheetViewState> {
|
||||||
|
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: VerificationBottomSheetViewState): VerificationBottomSheetViewModel? {
|
||||||
|
val fragment: VerificationBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||||
|
val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args()
|
||||||
|
|
||||||
|
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
|
||||||
|
|
||||||
|
val userItem = session.getUser(args.otherUserId)
|
||||||
|
|
||||||
|
val pr = session.getSasVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
|
||||||
|
|
||||||
|
val sasTx = pr?.transactionId?.let {
|
||||||
|
session.getSasVerificationService().getExistingTransaction(args.otherUserId, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fragment.verificationViewModelFactory.create(VerificationBottomSheetViewState(
|
||||||
|
otherUserMxItem = userItem?.toMatrixItem(),
|
||||||
|
sasTransactionState = sasTx?.state,
|
||||||
|
pendingRequest = pr,
|
||||||
|
roomId = args.roomId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: VerificationAction) = withState { state ->
|
||||||
|
val otherUserId = state.otherUserMxItem?.id ?: return@withState
|
||||||
|
val roomId = state.roomId
|
||||||
|
?: session.getExistingDirectRoomWithUser(otherUserId)?.roomId
|
||||||
|
?: return@withState
|
||||||
|
when (action) {
|
||||||
|
is VerificationAction.RequestVerificationByDM -> {
|
||||||
|
// session
|
||||||
|
setState {
|
||||||
|
copy(pendingRequest = session.getSasVerificationService().requestKeyVerificationInDMs(otherUserId, roomId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is VerificationAction.StartSASVerification -> {
|
||||||
|
val request = session.getSasVerificationService().getExistingVerificationRequest(otherUserId, action.pendingRequestTransactionId)
|
||||||
|
?: return@withState
|
||||||
|
|
||||||
|
val otherDevice = if (request.isIncoming) request.requestInfo?.fromDevice else request.readyInfo?.fromDevice
|
||||||
|
session.getSasVerificationService().beginKeyVerificationInDMs(
|
||||||
|
KeyVerificationStart.VERIF_METHOD_SAS,
|
||||||
|
transactionId = action.pendingRequestTransactionId,
|
||||||
|
roomId = roomId,
|
||||||
|
otherUserId = request.otherUserId,
|
||||||
|
otherDeviceId = otherDevice ?: "",
|
||||||
|
callback = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is VerificationAction.SASMatchAction -> {
|
||||||
|
session.getSasVerificationService()
|
||||||
|
.getExistingTransaction(action.userID, action.sasTransactionId)
|
||||||
|
?.userHasVerifiedShortCode()
|
||||||
|
}
|
||||||
|
is VerificationAction.SASDoNotMatchAction -> {
|
||||||
|
session.getSasVerificationService()
|
||||||
|
.getExistingTransaction(action.userID, action.sasTransactionId)
|
||||||
|
?.shortCodeDoesNotMatch()
|
||||||
|
}
|
||||||
|
is VerificationAction.GotItConclusion -> {
|
||||||
|
_requestLiveData.postValue(LiveEvent(Success(action)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun transactionCreated(tx: SasVerificationTransaction) {
|
||||||
|
transactionUpdated(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun transactionUpdated(tx: SasVerificationTransaction) = withState { state ->
|
||||||
|
if (tx.transactionId == state.pendingRequest?.transactionId) {
|
||||||
|
// A SAS tx has been started following this request
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
sasTransactionState = tx.state,
|
||||||
|
cancelCode = tx.cancelledReason
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
|
||||||
|
verificationRequestUpdated(pr)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
|
||||||
|
|
||||||
|
if (pr.localID == state.pendingRequest?.localID || state.pendingRequest?.transactionId == pr.transactionId) {
|
||||||
|
setState {
|
||||||
|
copy(pendingRequest = pr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.riotx.features.crypto.verification
|
||||||
|
|
||||||
|
import android.text.style.ClickableSpan
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.text.toSpannable
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import butterknife.OnClick
|
||||||
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
import com.airbnb.mvrx.parentFragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.riotx.core.utils.tappableMatchingText
|
||||||
|
import kotlinx.android.synthetic.main.fragment_verification_choose_method.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class VerificationChooseMethodFragment @Inject constructor(
|
||||||
|
val verificationChooseMethodViewModelFactory: VerificationChooseMethodViewModel.Factory
|
||||||
|
) : VectorBaseFragment() {
|
||||||
|
|
||||||
|
override fun getLayoutResId() = R.layout.fragment_verification_choose_method
|
||||||
|
|
||||||
|
private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
|
||||||
|
|
||||||
|
private val viewModel by fragmentViewModel(VerificationChooseMethodViewModel::class)
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
|
if (state.QRModeAvailable) {
|
||||||
|
val cSpan = object : ClickableSpan() {
|
||||||
|
override fun onClick(widget: View) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val openLink = getString(R.string.verify_open_camera_link)
|
||||||
|
val descCharSequence =
|
||||||
|
getString(R.string.verify_by_scanning_description, openLink)
|
||||||
|
.toSpannable()
|
||||||
|
.tappableMatchingText(openLink, cSpan)
|
||||||
|
verifyQRDescription.text = descCharSequence
|
||||||
|
verifyQRGroup.isVisible = true
|
||||||
|
} else {
|
||||||
|
verifyQRGroup.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyEmojiGroup.isVisible = state.SASMOdeAvailable
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.verificationByEmojiButton)
|
||||||
|
fun doVerifyBySas() = withState(sharedViewModel) {
|
||||||
|
sharedViewModel.handle(VerificationAction.StartSASVerification(it.otherUserMxItem?.id ?: "", it.pendingRequest?.transactionId
|
||||||
|
?: ""))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.riotx.features.crypto.verification
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import com.squareup.inject.assisted.Assisted
|
||||||
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
|
||||||
|
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
|
||||||
|
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
|
||||||
|
import im.vector.riotx.core.di.HasScreenInjector
|
||||||
|
import im.vector.riotx.core.platform.EmptyAction
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
|
|
||||||
|
data class VerificationChooseMethodViewState(
|
||||||
|
val otherUserId: String = "",
|
||||||
|
val transactionId: String = "",
|
||||||
|
val QRModeAvailable: Boolean = false,
|
||||||
|
val SASMOdeAvailable: Boolean = false
|
||||||
|
) : MvRxState
|
||||||
|
|
||||||
|
class VerificationChooseMethodViewModel @AssistedInject constructor(
|
||||||
|
@Assisted initialState: VerificationChooseMethodViewState,
|
||||||
|
private val session: Session
|
||||||
|
) : VectorViewModel<VerificationChooseMethodViewState, EmptyAction>(initialState), SasVerificationService.SasVerificationListener {
|
||||||
|
|
||||||
|
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||||
|
|
||||||
|
override fun transactionUpdated(tx: SasVerificationTransaction) {}
|
||||||
|
|
||||||
|
override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
|
||||||
|
val pvr = session.getSasVerificationService().getExistingVerificationRequest(state.otherUserId, state.transactionId)
|
||||||
|
val qrAvailable = pvr?.readyInfo?.methods?.contains(KeyVerificationStart.VERIF_METHOD_SCAN)
|
||||||
|
?: false
|
||||||
|
val emojiAvailable = pvr?.readyInfo?.methods?.contains(KeyVerificationStart.VERIF_METHOD_SAS)
|
||||||
|
?: false
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
QRModeAvailable = qrAvailable,
|
||||||
|
SASMOdeAvailable = emojiAvailable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AssistedInject.Factory
|
||||||
|
interface Factory {
|
||||||
|
fun create(initialState: VerificationChooseMethodViewState): VerificationChooseMethodViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
session.getSasVerificationService().addListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
session.getSasVerificationService().removeListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<VerificationChooseMethodViewModel, VerificationChooseMethodViewState> {
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: VerificationChooseMethodViewState): VerificationChooseMethodViewModel? {
|
||||||
|
val fragment: VerificationChooseMethodFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||||
|
return fragment.verificationChooseMethodViewModelFactory.create(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initialState(viewModelContext: ViewModelContext): VerificationChooseMethodViewState? {
|
||||||
|
val args: VerificationBottomSheet.VerificationArgs = viewModelContext.args()
|
||||||
|
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
|
||||||
|
val pvr = session.getSasVerificationService().getExistingVerificationRequest(args.otherUserId, args.verificationId)
|
||||||
|
val qrAvailable = pvr?.readyInfo?.methods?.contains(KeyVerificationStart.VERIF_METHOD_SCAN)
|
||||||
|
?: false
|
||||||
|
val emojiAvailable = pvr?.readyInfo?.methods?.contains(KeyVerificationStart.VERIF_METHOD_SAS)
|
||||||
|
?: false
|
||||||
|
|
||||||
|
return VerificationChooseMethodViewState(otherUserId = args.otherUserId,
|
||||||
|
transactionId = args.verificationId ?: "",
|
||||||
|
QRModeAvailable = qrAvailable,
|
||||||
|
SASMOdeAvailable = emojiAvailable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: EmptyAction) {}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.riotx.features.crypto.verification
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import butterknife.OnClick
|
||||||
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
import com.airbnb.mvrx.parentFragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.extensions.setTextOrHide
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
|
import io.noties.markwon.Markwon
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import kotlinx.android.synthetic.main.fragment_verification_conclusion.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class VerificationConclusionFragment @Inject constructor() : VectorBaseFragment() {
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Args(
|
||||||
|
val isSuccessFull: Boolean,
|
||||||
|
val cancelReason: String?
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
override fun getLayoutResId() = R.layout.fragment_verification_conclusion
|
||||||
|
|
||||||
|
private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
|
||||||
|
|
||||||
|
private val viewModel by fragmentViewModel(VerificationConclusionViewModel::class)
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) {
|
||||||
|
when (it.conclusionState) {
|
||||||
|
ConclusionState.SUCCESS -> {
|
||||||
|
verificationConclusionTitle.text = getString(R.string.sas_verified)
|
||||||
|
verifyConclusionDescription.setTextOrHide(getString(R.string.sas_verified_successful_description))
|
||||||
|
verifyConclusionBottomDescription.text = getString(R.string.verification_green_shield)
|
||||||
|
verifyConclusionImageView.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_shield_trusted))
|
||||||
|
}
|
||||||
|
ConclusionState.WARNING -> {
|
||||||
|
verificationConclusionTitle.text = getString(R.string.verification_conclusion_not_secure)
|
||||||
|
verifyConclusionDescription.isVisible = false
|
||||||
|
verifyConclusionImageView.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_shield_warning))
|
||||||
|
|
||||||
|
verifyConclusionBottomDescription.text = Markwon.builder(requireContext())
|
||||||
|
.build()
|
||||||
|
.toMarkdown(getString(R.string.verification_conclusion_compromised))
|
||||||
|
}
|
||||||
|
ConclusionState.CANCELLED -> {
|
||||||
|
// Just dismiss in this case
|
||||||
|
sharedViewModel.handle(VerificationAction.GotItConclusion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.verificationConclusionButton)
|
||||||
|
fun onButtonTapped() {
|
||||||
|
sharedViewModel.handle(VerificationAction.GotItConclusion)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.riotx.features.crypto.verification
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
|
||||||
|
import im.vector.matrix.android.api.session.crypto.sas.safeValueOf
|
||||||
|
import im.vector.riotx.core.platform.EmptyAction
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
|
|
||||||
|
data class SASVerificationConclusionViewState(
|
||||||
|
val conclusionState: ConclusionState = ConclusionState.CANCELLED
|
||||||
|
) : MvRxState
|
||||||
|
|
||||||
|
enum class ConclusionState {
|
||||||
|
SUCCESS,
|
||||||
|
WARNING,
|
||||||
|
CANCELLED
|
||||||
|
}
|
||||||
|
|
||||||
|
class VerificationConclusionViewModel(initialState: SASVerificationConclusionViewState)
|
||||||
|
: VectorViewModel<SASVerificationConclusionViewState, EmptyAction>(initialState) {
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<VerificationConclusionViewModel, SASVerificationConclusionViewState> {
|
||||||
|
|
||||||
|
override fun initialState(viewModelContext: ViewModelContext): SASVerificationConclusionViewState? {
|
||||||
|
val args = viewModelContext.args<VerificationConclusionFragment.Args>()
|
||||||
|
|
||||||
|
return when (safeValueOf(args.cancelReason)) {
|
||||||
|
CancelCode.MismatchedSas,
|
||||||
|
CancelCode.MismatchedCommitment,
|
||||||
|
CancelCode.MismatchedKeys -> {
|
||||||
|
SASVerificationConclusionViewState(ConclusionState.WARNING)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
SASVerificationConclusionViewState(
|
||||||
|
if (args.isSuccessFull) ConclusionState.SUCCESS
|
||||||
|
else ConclusionState.CANCELLED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: EmptyAction) {}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.riotx.features.crypto.verification
|
||||||
|
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import androidx.core.text.toSpannable
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import butterknife.OnClick
|
||||||
|
import com.airbnb.mvrx.Loading
|
||||||
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
import com.airbnb.mvrx.parentFragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.riotx.core.utils.colorizeMatchingText
|
||||||
|
import im.vector.riotx.core.utils.styleMatchingText
|
||||||
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
import im.vector.riotx.features.themes.ThemeUtils
|
||||||
|
import kotlinx.android.synthetic.main.fragment_verification_request.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class VerificationRequestFragment @Inject constructor(
|
||||||
|
val verificationRequestViewModelFactory: VerificationRequestViewModel.Factory,
|
||||||
|
val avatarRenderer: AvatarRenderer
|
||||||
|
) : VectorBaseFragment() {
|
||||||
|
|
||||||
|
private val viewModel by fragmentViewModel(VerificationRequestViewModel::class)
|
||||||
|
|
||||||
|
private val sharedViewModel by parentFragmentViewModel(VerificationBottomSheetViewModel::class)
|
||||||
|
|
||||||
|
override fun getLayoutResId() = R.layout.fragment_verification_request
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
|
state.matrixItem.let {
|
||||||
|
val styledText = getString(R.string.verification_request_alert_description, it.id)
|
||||||
|
.toSpannable()
|
||||||
|
.styleMatchingText(it.id, Typeface.BOLD)
|
||||||
|
.colorizeMatchingText(it.id, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color))
|
||||||
|
verificationRequestText.text = styledText
|
||||||
|
}
|
||||||
|
|
||||||
|
when (state.started) {
|
||||||
|
is Loading -> {
|
||||||
|
// Hide the start button, show waiting
|
||||||
|
verificationStartButton.isInvisible = true
|
||||||
|
verificationWaitingText.isVisible = true
|
||||||
|
val otherUser = state.matrixItem.displayName ?: state.matrixItem.id
|
||||||
|
verificationWaitingText.text = getString(R.string.verification_request_waiting_for, otherUser)
|
||||||
|
.toSpannable()
|
||||||
|
.styleMatchingText(otherUser, Typeface.BOLD)
|
||||||
|
.colorizeMatchingText(otherUser, ThemeUtils.getColor(requireContext(), R.attr.vctr_notice_text_color))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
verificationStartButton.isEnabled = true
|
||||||
|
verificationStartButton.isVisible = true
|
||||||
|
verificationWaitingText.isInvisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.verificationStartButton)
|
||||||
|
fun onClickOnVerificationStart() = withState(viewModel) { state ->
|
||||||
|
verificationStartButton.isEnabled = false
|
||||||
|
sharedViewModel.handle(VerificationAction.RequestVerificationByDM(state.matrixItem.id, state.roomId))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.riotx.features.crypto.verification
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.*
|
||||||
|
import com.squareup.inject.assisted.Assisted
|
||||||
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
|
||||||
|
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
|
||||||
|
import im.vector.matrix.android.api.util.MatrixItem
|
||||||
|
import im.vector.matrix.android.api.util.toMatrixItem
|
||||||
|
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
|
||||||
|
import im.vector.riotx.core.di.HasScreenInjector
|
||||||
|
import im.vector.riotx.core.platform.EmptyAction
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
|
|
||||||
|
data class VerificationRequestViewState(
|
||||||
|
val roomId: String? = null,
|
||||||
|
val matrixItem: MatrixItem,
|
||||||
|
val started: Async<Boolean> = Success(false)
|
||||||
|
) : MvRxState
|
||||||
|
|
||||||
|
class VerificationRequestViewModel @AssistedInject constructor(
|
||||||
|
@Assisted initialState: VerificationRequestViewState,
|
||||||
|
private val session: Session
|
||||||
|
) : VectorViewModel<VerificationRequestViewState, EmptyAction>(initialState), SasVerificationService.SasVerificationListener {
|
||||||
|
|
||||||
|
@AssistedInject.Factory
|
||||||
|
interface Factory {
|
||||||
|
fun create(initialState: VerificationRequestViewState): VerificationRequestViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
session.getSasVerificationService().addListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
session.getSasVerificationService().removeListener(this)
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<VerificationRequestViewModel, VerificationRequestViewState> {
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: VerificationRequestViewState): VerificationRequestViewModel? {
|
||||||
|
val fragment: VerificationRequestFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||||
|
return fragment.verificationRequestViewModelFactory.create(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initialState(viewModelContext: ViewModelContext): VerificationRequestViewState? {
|
||||||
|
val args = viewModelContext.args<VerificationBottomSheet.VerificationArgs>()
|
||||||
|
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
|
||||||
|
|
||||||
|
val pr = session.getSasVerificationService()
|
||||||
|
.getExistingVerificationRequest(args.otherUserId, args.verificationId)
|
||||||
|
return session.getUser(args.otherUserId)?.let {
|
||||||
|
VerificationRequestViewState(
|
||||||
|
started = Success(false).takeIf { pr == null }
|
||||||
|
?: Success(true).takeIf { pr?.isReady == true }
|
||||||
|
?: Loading(),
|
||||||
|
matrixItem = it.toMatrixItem()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: EmptyAction) {}
|
||||||
|
|
||||||
|
override fun transactionCreated(tx: SasVerificationTransaction) {}
|
||||||
|
|
||||||
|
override fun transactionUpdated(tx: SasVerificationTransaction) {}
|
||||||
|
|
||||||
|
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
|
||||||
|
verificationRequestUpdated(pr)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun verificationRequestUpdated(pr: PendingVerificationRequest) = withState { state ->
|
||||||
|
if (pr.otherUserId == state.matrixItem.id) {
|
||||||
|
if (pr.isReady) {
|
||||||
|
setState {
|
||||||
|
copy(started = Success(true))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setState {
|
||||||
|
copy(started = Loading())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -65,5 +65,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
||||||
object ResendAll : RoomDetailAction()
|
object ResendAll : RoomDetailAction()
|
||||||
|
|
||||||
data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String, val otherdDeviceId: String) : RoomDetailAction()
|
data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String, val otherdDeviceId: String) : RoomDetailAction()
|
||||||
data class DeclineVerificationRequest(val transactionId: String) : RoomDetailAction()
|
data class DeclineVerificationRequest(val transactionId: String, val otherUserId: String, val otherdDeviceId: String) : RoomDetailAction()
|
||||||
|
|
||||||
|
data class RequestVerification(val userId: String) : RoomDetailAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,6 +90,7 @@ import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter
|
||||||
import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter
|
import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter
|
||||||
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
|
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
|
||||||
import im.vector.riotx.features.command.Command
|
import im.vector.riotx.features.command.Command
|
||||||
|
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
import im.vector.riotx.features.home.getColorFromUserId
|
import im.vector.riotx.features.home.getColorFromUserId
|
||||||
import im.vector.riotx.features.home.room.detail.composer.TextComposerAction
|
import im.vector.riotx.features.home.room.detail.composer.TextComposerAction
|
||||||
|
@ -431,7 +432,8 @@ class RoomDetailFragment @Inject constructor(
|
||||||
composerLayout.sendButton.setContentDescription(getString(descriptionRes))
|
composerLayout.sendButton.setContentDescription(getString(descriptionRes))
|
||||||
|
|
||||||
avatarRenderer.render(
|
avatarRenderer.render(
|
||||||
MatrixItem.UserItem(event.root.senderId ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
|
MatrixItem.UserItem(event.root.senderId
|
||||||
|
?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
|
||||||
composerLayout.composerRelatedMessageAvatar
|
composerLayout.composerRelatedMessageAvatar
|
||||||
)
|
)
|
||||||
composerLayout.expand {
|
composerLayout.expand {
|
||||||
|
@ -960,6 +962,21 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is RoomDetailAction.RequestVerification -> {
|
||||||
|
Timber.v("## SAS RequestVerification action")
|
||||||
|
VerificationBottomSheet.withArgs(
|
||||||
|
roomDetailArgs.roomId,
|
||||||
|
data.userId
|
||||||
|
).show(parentFragmentManager, "REQ")
|
||||||
|
}
|
||||||
|
is RoomDetailAction.AcceptVerificationRequest -> {
|
||||||
|
Timber.v("## SAS AcceptVerificationRequest action")
|
||||||
|
VerificationBottomSheet.withArgs(
|
||||||
|
roomDetailArgs.roomId,
|
||||||
|
data.otherUserId,
|
||||||
|
data.transactionId
|
||||||
|
).show(parentFragmentManager, "REQ")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1114,7 +1131,8 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAvatarClicked(informationData: MessageInformationData) {
|
override fun onAvatarClicked(informationData: MessageInformationData) {
|
||||||
vectorBaseActivity.notImplemented("Click on user avatar")
|
// vectorBaseActivity.notImplemented("Click on user avatar")
|
||||||
|
roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.senderId))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMemberNameClicked(informationData: MessageInformationData) {
|
override fun onMemberNameClicked(informationData: MessageInformationData) {
|
||||||
|
|
|
@ -27,7 +27,6 @@ import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.MatrixPatterns
|
import im.vector.matrix.android.api.MatrixPatterns
|
||||||
import im.vector.matrix.android.api.failure.Failure
|
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.isImageMessage
|
import im.vector.matrix.android.api.session.events.model.isImageMessage
|
||||||
|
@ -49,7 +48,6 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||||
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
|
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
|
||||||
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
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.event.EncryptedEventContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
|
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
import im.vector.matrix.rx.unwrap
|
import im.vector.matrix.rx.unwrap
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
|
@ -186,6 +184,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
|
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
|
||||||
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
|
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
|
||||||
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
|
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
|
||||||
|
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -398,7 +397,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
is ParsedCommand.VerifyUser -> {
|
is ParsedCommand.VerifyUser -> {
|
||||||
session.getSasVerificationService().requestKeyVerificationInDMs(slashCommandResult.userId, room.roomId, null)
|
session.getSasVerificationService().requestKeyVerificationInDMs(slashCommandResult.userId, room.roomId)
|
||||||
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
|
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
|
@ -796,18 +795,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) {
|
private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) {
|
||||||
session.getSasVerificationService().beginKeyVerificationInDMs(
|
Timber.v("## SAS handleAcceptVerification ${action.otherUserId}, roomId:${room.roomId}, txId:${action.transactionId}")
|
||||||
KeyVerificationStart.VERIF_METHOD_SAS,
|
if (session.getSasVerificationService().readyPendingVerificationInDMs(action.otherUserId, room.roomId,
|
||||||
action.transactionId,
|
action.transactionId)) {
|
||||||
room.roomId,
|
_requestLiveData.postValue(LiveEvent(Success(action)))
|
||||||
action.otherUserId,
|
}
|
||||||
action.otherdDeviceId,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) {
|
private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) {
|
||||||
Timber.e("TODO implement $action")
|
session.getSasVerificationService().declineVerificationRequestInDMs(
|
||||||
|
action.otherUserId,
|
||||||
|
action.otherdDeviceId,
|
||||||
|
action.transactionId,
|
||||||
|
room.roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRequestVerification(action: RoomDetailAction.RequestVerification) {
|
||||||
|
_requestLiveData.postValue(LiveEvent(Success(action)))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeSyncState() {
|
private fun observeSyncState() {
|
||||||
|
|
|
@ -46,6 +46,7 @@ class EncryptionItemFactory @Inject constructor(private val stringProvider: Stri
|
||||||
eventId = event.root.eventId ?: "?",
|
eventId = event.root.eventId ?: "?",
|
||||||
senderId = event.root.senderId ?: "",
|
senderId = event.root.senderId ?: "",
|
||||||
sendState = event.root.sendState,
|
sendState = event.root.sendState,
|
||||||
|
ageLocalTS = event.root.ageLocalTs,
|
||||||
avatarUrl = event.senderAvatar,
|
avatarUrl = event.senderAvatar,
|
||||||
memberName = event.getDisambiguatedDisplayName(),
|
memberName = event.getDisambiguatedDisplayName(),
|
||||||
showInformation = false,
|
showInformation = false,
|
||||||
|
|
|
@ -151,18 +151,18 @@ class MessageItemFactory @Inject constructor(
|
||||||
return VerificationRequestItem_()
|
return VerificationRequestItem_()
|
||||||
.attributes(
|
.attributes(
|
||||||
VerificationRequestItem.Attributes(
|
VerificationRequestItem.Attributes(
|
||||||
otherUserId,
|
otherUserId = otherUserId,
|
||||||
otherUserName.toString(),
|
otherUserName = otherUserName.toString(),
|
||||||
messageContent.fromDevice,
|
fromDevide = messageContent.fromDevice,
|
||||||
informationData.eventId,
|
referenceId = informationData.eventId,
|
||||||
informationData,
|
informationData = informationData,
|
||||||
attributes.avatarRenderer,
|
avatarRenderer = attributes.avatarRenderer,
|
||||||
attributes.colorProvider,
|
colorProvider = attributes.colorProvider,
|
||||||
attributes.itemLongClickListener,
|
itemLongClickListener = attributes.itemLongClickListener,
|
||||||
attributes.itemClickListener,
|
itemClickListener = attributes.itemClickListener,
|
||||||
attributes.reactionPillCallback,
|
reactionPillCallback = attributes.reactionPillCallback,
|
||||||
attributes.readReceiptsCallback,
|
readReceiptsCallback = attributes.readReceiptsCallback,
|
||||||
attributes.emojiTypeFace
|
emojiTypeFace = attributes.emojiTypeFace
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.callback(callback)
|
.callback(callback)
|
||||||
|
|
|
@ -70,6 +70,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||||
EventType.KEY_VERIFICATION_ACCEPT,
|
EventType.KEY_VERIFICATION_ACCEPT,
|
||||||
EventType.KEY_VERIFICATION_START,
|
EventType.KEY_VERIFICATION_START,
|
||||||
EventType.KEY_VERIFICATION_KEY,
|
EventType.KEY_VERIFICATION_KEY,
|
||||||
|
EventType.KEY_VERIFICATION_READY,
|
||||||
EventType.KEY_VERIFICATION_MAC -> {
|
EventType.KEY_VERIFICATION_MAC -> {
|
||||||
// These events are filtered from timeline in normal case
|
// These events are filtered from timeline in normal case
|
||||||
// Only visible in developer mode
|
// Only visible in developer mode
|
||||||
|
|
|
@ -52,6 +52,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
|
||||||
EventType.KEY_VERIFICATION_MAC,
|
EventType.KEY_VERIFICATION_MAC,
|
||||||
EventType.KEY_VERIFICATION_DONE,
|
EventType.KEY_VERIFICATION_DONE,
|
||||||
EventType.KEY_VERIFICATION_KEY,
|
EventType.KEY_VERIFICATION_KEY,
|
||||||
|
EventType.KEY_VERIFICATION_READY,
|
||||||
EventType.REDACTION -> formatDebug(timelineEvent.root)
|
EventType.REDACTION -> formatDebug(timelineEvent.root)
|
||||||
else -> {
|
else -> {
|
||||||
Timber.v("Type $type not handled by this formatter")
|
Timber.v("Type $type not handled by this formatter")
|
||||||
|
|
|
@ -73,6 +73,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
||||||
senderId = event.root.senderId ?: "",
|
senderId = event.root.senderId ?: "",
|
||||||
sendState = event.root.sendState,
|
sendState = event.root.sendState,
|
||||||
time = time,
|
time = time,
|
||||||
|
ageLocalTS = event.root.ageLocalTs,
|
||||||
avatarUrl = avatarUrl,
|
avatarUrl = avatarUrl,
|
||||||
memberName = formattedMemberName,
|
memberName = formattedMemberName,
|
||||||
showInformation = showInformation,
|
showInformation = showInformation,
|
||||||
|
|
|
@ -50,7 +50,8 @@ object TimelineDisplayableEvents {
|
||||||
EventType.KEY_VERIFICATION_ACCEPT,
|
EventType.KEY_VERIFICATION_ACCEPT,
|
||||||
EventType.KEY_VERIFICATION_START,
|
EventType.KEY_VERIFICATION_START,
|
||||||
EventType.KEY_VERIFICATION_MAC,
|
EventType.KEY_VERIFICATION_MAC,
|
||||||
EventType.KEY_VERIFICATION_KEY
|
EventType.KEY_VERIFICATION_KEY,
|
||||||
|
EventType.KEY_VERIFICATION_READY
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ data class MessageInformationData(
|
||||||
val senderId: String,
|
val senderId: String,
|
||||||
val sendState: SendState,
|
val sendState: SendState,
|
||||||
val time: CharSequence? = null,
|
val time: CharSequence? = null,
|
||||||
|
val ageLocalTS : Long?,
|
||||||
val avatarUrl: String?,
|
val avatarUrl: String?,
|
||||||
val memberName: CharSequence? = null,
|
val memberName: CharSequence? = null,
|
||||||
val showInformation: Boolean = true,
|
val showInformation: Boolean = true,
|
||||||
|
|
|
@ -28,6 +28,7 @@ import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
|
||||||
import im.vector.matrix.android.internal.session.room.VerificationState
|
import im.vector.matrix.android.internal.session.room.VerificationState
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.resources.ColorProvider
|
import im.vector.riotx.core.resources.ColorProvider
|
||||||
|
@ -108,6 +109,11 @@ abstract class VerificationRequestItem : AbsBaseMessageItem<VerificationRequestI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always hide buttons if request is too old
|
||||||
|
if (!SasVerificationService.isValidRequest(attributes.informationData.ageLocalTS)) {
|
||||||
|
holder.buttonBar.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
holder.callback = callback
|
holder.callback = callback
|
||||||
holder.attributes = attributes
|
holder.attributes = attributes
|
||||||
|
|
||||||
|
@ -133,7 +139,7 @@ abstract class VerificationRequestItem : AbsBaseMessageItem<VerificationRequestI
|
||||||
att.otherUserId,
|
att.otherUserId,
|
||||||
att.fromDevide))
|
att.fromDevide))
|
||||||
} else if (it == declineButton) {
|
} else if (it == declineButton) {
|
||||||
callback?.onTimelineItemAction(RoomDetailAction.DeclineVerificationRequest(att.referenceId))
|
callback?.onTimelineItemAction(RoomDetailAction.DeclineVerificationRequest(att.referenceId, att.otherUserId, att.fromDevide))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/bottomSheetScrollView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:animateLayoutChanges="true"
|
||||||
|
android:fadeScrollbars="false"
|
||||||
|
android:scrollbars="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingTop="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/verificationRequestAvatar"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:background="@drawable/circle"
|
||||||
|
android:contentDescription="@string/avatar"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/verificationRequestName"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/verification_request_alert_title"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/bottomSheetFragmentContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
|
@ -0,0 +1,188 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:colorBackground">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/sas_emoji_description"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/layout_horizontal_margin"
|
||||||
|
android:layout_marginTop="@dimen/layout_vertical_margin"
|
||||||
|
android:text="@string/verify_by_emoji_title"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/sas_emoji_description_2"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/layout_vertical_margin"
|
||||||
|
android:text="@string/verify_user_sas_emoji_help_text"
|
||||||
|
android:textColor="?riotx_text_secondary"
|
||||||
|
android:textSize="15sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/sas_emoji_description" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/sas_decimal_code"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="28sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/sas_emoji_grid"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/sas_emoji_grid"
|
||||||
|
tools:text="1234-4320-3905"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/sasLoadingProgress"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/sas_emoji_grid"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/sas_emoji_grid" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/sas_emoji_grid"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/layout_vertical_margin"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constrainedWidth="true"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/sas_emoji_description_2"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/emoji0"
|
||||||
|
layout="@layout/item_emoji_verif" />
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/emoji1"
|
||||||
|
layout="@layout/item_emoji_verif" />
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/emoji2"
|
||||||
|
layout="@layout/item_emoji_verif" />
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/emoji3"
|
||||||
|
layout="@layout/item_emoji_verif" />
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/emoji4"
|
||||||
|
layout="@layout/item_emoji_verif" />
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/emoji5"
|
||||||
|
layout="@layout/item_emoji_verif" />
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/emoji6"
|
||||||
|
layout="@layout/item_emoji_verif" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.helper.widget.Flow
|
||||||
|
android:id="@+id/sas_emoji_grid_flow"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:constraint_referenced_ids="emoji0,emoji1,emoji2,emoji3,emoji4,emoji5,emoji6"
|
||||||
|
app:flow_horizontalBias="0.5"
|
||||||
|
app:flow_horizontalGap="16dp"
|
||||||
|
app:flow_horizontalStyle="packed"
|
||||||
|
app:flow_verticalBias="0"
|
||||||
|
app:flow_verticalGap="8dp"
|
||||||
|
app:flow_wrapMode="chain"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/sas_request_continue_button"
|
||||||
|
style="@style/VectorButtonStylePositive"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_margin="@dimen/layout_vertical_margin"
|
||||||
|
android:minWidth="160dp"
|
||||||
|
android:text="@string/verification_sas_match"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/centerGuideLine"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/sas_emoji_grid"
|
||||||
|
tools:text="A very long translation thats too big" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/centerGuideLine"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintGuide_percent="0.5" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/sas_request_cancel_button"
|
||||||
|
style="@style/VectorButtonStyleDestructive"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_margin="@dimen/layout_vertical_margin"
|
||||||
|
android:text="@string/verification_sas_do_not_match"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/centerGuideLine"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/sas_emoji_grid" />
|
||||||
|
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/bottomBarrier"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:barrierDirection="bottom"
|
||||||
|
app:barrierMargin="8dp"
|
||||||
|
app:constraint_referenced_ids="sas_request_cancel_button,sas_request_continue_button" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/sasCodeWaitingPartnerText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/sas_waiting_for_partner"
|
||||||
|
android:textColor="?attr/vctr_notice_secondary"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/bottomBarrier"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/sas_emoji_grid"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/sasEmojiSecurityTip"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/layout_vertical_margin"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/verify_user_sas_emoji_security_tip"
|
||||||
|
android:textColor="?riotx_text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/bottomBarrier" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Group
|
||||||
|
android:id="@+id/ButtonsVisibilityGroup"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:constraint_referenced_ids="sas_request_continue_button,sas_request_cancel_button"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,117 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <ImageView-->
|
||||||
|
<!-- android:id="@+id/verificationRequestAvatar"-->
|
||||||
|
<!-- android:layout_width="32dp"-->
|
||||||
|
<!-- android:layout_height="32dp"-->
|
||||||
|
<!-- android:adjustViewBounds="true"-->
|
||||||
|
<!-- android:background="@drawable/circle"-->
|
||||||
|
<!-- android:contentDescription="@string/avatar"-->
|
||||||
|
<!-- android:scaleType="centerCrop"-->
|
||||||
|
<!-- android:transitionName="bottomSheetAvatar"-->
|
||||||
|
<!-- app:layout_constraintStart_toStartOf="parent"-->
|
||||||
|
<!-- app:layout_constraintTop_toTopOf="parent"-->
|
||||||
|
<!-- app:layout_constraintVertical_bias="0"-->
|
||||||
|
<!-- tools:src="@tools:sample/avatars" />-->
|
||||||
|
|
||||||
|
<!-- <TextView-->
|
||||||
|
<!-- android:id="@+id/verificationRequestName"-->
|
||||||
|
<!-- android:layout_width="0dp"-->
|
||||||
|
<!-- android:layout_height="wrap_content"-->
|
||||||
|
<!-- android:layout_marginStart="16dp"-->
|
||||||
|
<!-- android:text="@string/verification_request_alert_title"-->
|
||||||
|
<!-- android:textColor="?riotx_text_primary"-->
|
||||||
|
<!-- android:textSize="20sp"-->
|
||||||
|
<!-- android:textStyle="bold"-->
|
||||||
|
<!-- android:transitionName="bottomSheetDisplayName"-->
|
||||||
|
<!-- app:layout_constraintBottom_toBottomOf="@id/verificationRequestAvatar"-->
|
||||||
|
<!-- app:layout_constraintEnd_toEndOf="parent"-->
|
||||||
|
<!-- app:layout_constraintStart_toEndOf="@id/verificationRequestAvatar"-->
|
||||||
|
<!-- app:layout_constraintTop_toTopOf="@id/verificationRequestAvatar" />-->
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/verificationQRTitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:text="@string/verify_by_scanning_title"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/verifyQRDescription"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/verify_by_scanning_description"
|
||||||
|
android:textColor="?riotx_text_secondary"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/verificationQRTitle"
|
||||||
|
tools:text="@string/verify_by_scanning_description" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/verifyQRImageView"
|
||||||
|
android:layout_width="180dp"
|
||||||
|
android:layout_height="180dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:background="?riotx_header_panel_background"
|
||||||
|
android:contentDescription="@string/aria_qr_code_description"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/verifyQRDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/verificationEmojiTitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="40dp"
|
||||||
|
android:text="@string/verify_by_emoji_title"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/verifyQRImageView"
|
||||||
|
app:layout_goneMarginTop="0dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/verifyEmojiDescription"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/verify_by_emoji_description"
|
||||||
|
android:textColor="?riotx_text_secondary"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/verificationEmojiTitle" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/verificationByEmojiButton"
|
||||||
|
style="@style/VectorButtonStylePositive"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/verify_by_emoji_title"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/verifyEmojiDescription" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Group
|
||||||
|
android:id="@+id/verifyQRGroup"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible"
|
||||||
|
app:constraint_referenced_ids="verifyQRDescription,verificationQRTitle,verifyQRImageView" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Group
|
||||||
|
android:id="@+id/verifyEmojiGroup"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:constraint_referenced_ids="verifyEmojiDescription,verificationEmojiTitle,verificationByEmojiButton" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/verificationConclusionTitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:text="@string/sas_verified"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="@string/sas_verified" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/verifyConclusionDescription"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/sas_verified_successful_description"
|
||||||
|
android:textColor="?riotx_text_secondary"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/verificationConclusionTitle"
|
||||||
|
tools:text="@string/sas_verified_successful_description" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/verifyConclusionImageView"
|
||||||
|
android:layout_width="180dp"
|
||||||
|
android:layout_height="180dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/verifyConclusionDescription"
|
||||||
|
tools:background="@drawable/ic_shield_trusted" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/verifyConclusionBottomDescription"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:textColor="?riotx_text_secondary"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/verifyConclusionImageView"
|
||||||
|
tools:text="@string/verification_green_shield" />
|
||||||
|
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/verificationConclusionButton"
|
||||||
|
style="@style/VectorButtonStylePositive"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/sas_got_it"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/verifyConclusionBottomDescription" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/verificationRequestText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:textColor="?riotx_text_secondary"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="@string/verification_request_alert_description" />
|
||||||
|
|
||||||
|
<!-- app:layout_constraintTop_toBottomOf="@id/verificationRequestAvatar"-->
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/verificationStartButton"
|
||||||
|
style="@style/VectorButtonStylePositive"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/start_verification"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/verificationRequestText" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/verificationWaitingText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:textColor="?vctr_notice_secondary"
|
||||||
|
android:textSize="17sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="invisible"
|
||||||
|
tools:visibility="visible"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/verificationStartButton"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/verificationStartButton"
|
||||||
|
tools:text="@string/verification_request_waiting_for" />
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1500,7 +1500,7 @@ Why choose Riot.im?
|
||||||
|
|
||||||
<string name="sas_verified">Verified!</string>
|
<string name="sas_verified">Verified!</string>
|
||||||
<string name="sas_verified_successful">You\'ve successfully verified this device.</string>
|
<string name="sas_verified_successful">You\'ve successfully verified this device.</string>
|
||||||
<string name="sas_verified_successful_description">Secure messages with this user are end-to-end encrypted and not able to be read by third parties.</string>
|
<string name="sas_verified_successful_description">Messages with this user in this room are end-to-end encrypted and can‘t be read by third parties.</string>
|
||||||
<string name="sas_got_it">Got it</string>
|
<string name="sas_got_it">Got it</string>
|
||||||
|
|
||||||
<string name="sas_verifying_keys">Nothing appearing? Not all clients supports interactive verification yet. Use legacy verification.</string>
|
<string name="sas_verifying_keys">Nothing appearing? Not all clients supports interactive verification yet. Use legacy verification.</string>
|
||||||
|
|
|
@ -5,15 +5,25 @@
|
||||||
<string name="command_description_verify">Request to verify the given userID</string>
|
<string name="command_description_verify">Request to verify the given userID</string>
|
||||||
<string name="command_description_shrug">Prepends ¯\\_(ツ)_/¯ to a plain-text message</string>
|
<string name="command_description_shrug">Prepends ¯\\_(ツ)_/¯ to a plain-text message</string>
|
||||||
|
|
||||||
|
|
||||||
<string name="notification_initial_sync">Initial Sync…</string>
|
<string name="notification_initial_sync">Initial Sync…</string>
|
||||||
|
|
||||||
<string name="sent_a_file">File</string>
|
|
||||||
<string name="sent_an_audio_file">Audio</string>
|
|
||||||
<string name="sent_an_image">Image.</string>
|
|
||||||
<string name="sent_a_video">Video.</string>
|
|
||||||
|
|
||||||
|
|
||||||
<string name="verification_conclusion_warning">Untrusted sign in</string>
|
<string name="verification_conclusion_warning">Untrusted sign in</string>
|
||||||
|
<string name="verification_sas_match">They match</string>
|
||||||
|
<string name="verification_sas_do_not_match">They don\'t match</string>
|
||||||
|
<string name="verify_user_sas_emoji_help_text">Verify this user by confirming the following unique emoji appear on their screen, in the same order."</string>
|
||||||
|
<string name="verify_user_sas_emoji_security_tip">For ultimate security, use another trusted means of communication or do this in person.</string>
|
||||||
|
<string name="verification_green_shield">Look for the green shield to ensure a user is trusted. Trust all users in a room to ensure the room is secure.</string>
|
||||||
|
|
||||||
|
<string name="verification_conclusion_not_secure">Not secure</string>
|
||||||
|
<string name="verification_conclusion_compromised">One of the following may be compromised:\n\n - Your homeserver\n - The homeserver the user you’re verifying is connected to\n - Yours, or the other users’ internet connection\n - Yours, or the other users’ device
|
||||||
|
</string>
|
||||||
|
|
||||||
|
<string name="sent_a_video">Video.</string>
|
||||||
|
<string name="sent_an_image">Image.</string>
|
||||||
|
<string name="sent_an_audio_file">Audio</string>
|
||||||
|
<string name="sent_a_file">File</string>
|
||||||
|
|
||||||
<string name="verification_request_waiting">Waiting…</string>
|
<string name="verification_request_waiting">Waiting…</string>
|
||||||
<string name="verification_request_other_cancelled">%s cancelled</string>
|
<string name="verification_request_other_cancelled">%s cancelled</string>
|
||||||
<string name="verification_request_you_cancelled">You cancelled</string>
|
<string name="verification_request_you_cancelled">You cancelled</string>
|
||||||
|
@ -21,4 +31,22 @@
|
||||||
<string name="verification_request_you_accepted">You accepted</string>
|
<string name="verification_request_you_accepted">You accepted</string>
|
||||||
<string name="verification_sent">Verification Sent</string>
|
<string name="verification_sent">Verification Sent</string>
|
||||||
<string name="verification_request">Verification Request</string>
|
<string name="verification_request">Verification Request</string>
|
||||||
|
|
||||||
|
<!-- Sender name of a message when it is send by you, e.g. You: Hello!-->
|
||||||
|
<string name="you">You</string>
|
||||||
|
|
||||||
|
<string name="verify_by_scanning_title">Verify by scanning</string>
|
||||||
|
<!-- the %s will be replaced by verify_open_camera_link that will be clickable -->
|
||||||
|
<string name="verify_by_scanning_description">Ask the other user to scan this code, or %s to scan theirs</string>
|
||||||
|
<!-- This part is inserted in verify_by_scanning_description-->
|
||||||
|
<string name="verify_open_camera_link">open your camera</string>
|
||||||
|
|
||||||
|
<string name="verify_by_emoji_title">Verify by Emoji</string>
|
||||||
|
<string name="verify_by_emoji_description">If you can’t scan the code above, verify by comparing a short, unique selection of emoji.</string>
|
||||||
|
|
||||||
|
<string name="aria_qr_code_description">QR code image</string>
|
||||||
|
|
||||||
|
<string name="verification_request_alert_title">Verify %s</string>
|
||||||
|
<string name="verification_request_waiting_for">Waiting for %s…</string>
|
||||||
|
<string name="verification_request_alert_description">For extra security, verify %s by checking a one-time code on both your devices.\n\nFor maximum security, do this in person.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in New Issue