Keys share request handling

This commit is contained in:
Benoit Marty 2019-06-12 18:32:24 +02:00
parent a7c0e87f40
commit 480d197ffa
9 changed files with 316 additions and 6 deletions

View File

@ -28,6 +28,7 @@ import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody

@ -99,4 +100,7 @@ interface CryptoService {
fun getEncryptionAlgorithm(roomId: String): String?

fun shouldEncryptForInvitedMembers(roomId: String): Boolean

fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>)

}

View File

@ -1050,12 +1050,22 @@ internal class CryptoManager(
return unknownDevices
}

/* ==========================================================================================
* DEBUG INFO
* ========================================================================================== */
override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) {
CoroutineScope(coroutineDispatchers.crypto).launch {
deviceListManager
.downloadKeys(userIds, forceDownload)
.fold(
{ callback.onFailure(it) },
{ callback.onSuccess(it) }
)
}
}

/* ==========================================================================================
* DEBUG INFO
* ========================================================================================== */

override fun toString(): String {
return "CryptoManager of " + credentials.userId + " (" + credentials.deviceId + ")"

}
}

View File

@ -24,6 +24,7 @@ import com.squareup.moshi.JsonClass
*/
@JsonClass(generateAdapter = true)
data class RoomKeyRequestBody(
@Json(name = "algorithm")
var algorithm: String? = null,

@Json(name = "room_id")

View File

@ -56,6 +56,7 @@ import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.database.LiveEntityObserver
@ -390,6 +391,10 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
return cryptoService.shouldEncryptForInvitedMembers(roomId)
}

override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) {
cryptoService.downloadKeys(userIds, forceDownload, callback)
}

// Private methods *****************************************************************************

private fun assertMainThread() {

View File

@ -25,6 +25,7 @@ import im.vector.riotredesign.core.resources.LocaleProvider
import im.vector.riotredesign.core.resources.StringArrayProvider
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.configuration.VectorConfiguration
import im.vector.riotredesign.features.crypto.keysrequest.KeyRequestHandler
import im.vector.riotredesign.features.crypto.verification.IncomingVerificationRequestHandler
import im.vector.riotredesign.features.home.HomeRoomListObservableStore
import im.vector.riotredesign.features.home.group.SelectedGroupStore
@ -87,6 +88,10 @@ class AppModule(private val context: Context) {
Matrix.getInstance().currentSession!!
}

single {
KeyRequestHandler(context, get())
}

single {
IncomingVerificationRequestHandler(context, get())
}

View File

@ -0,0 +1,279 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 New Vector Ltd
* 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.riotredesign.features.crypto.keysrequest

import android.content.Context
import android.text.TextUtils
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
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.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCancellation
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotredesign.R
import im.vector.riotredesign.features.crypto.verification.SASVerificationActivity
import im.vector.riotredesign.features.popup.PopupAlertManager
import timber.log.Timber
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap

/**
* Manage the key share events.
* Listens for incoming key request and display an alert to the user asking him to ignore / verify
* calling device / or accept without verifying.
* If several requests come from same user/device, a single alert is displayed (this alert will accept/reject all request
* depending on user action)
*/
class KeyRequestHandler(val context: Context,
val session: Session)
: RoomKeysRequestListener,
SasVerificationService.SasVerificationListener {

private val alertsToRequests = HashMap<String, ArrayList<IncomingRoomKeyRequest>>()

init {
session.getSasVerificationService().addListener(this)

session.addRoomKeysRequestListener(this)
}

fun ensureStarted() = Unit

/**
* Handle incoming key request.
*
* @param request the key request.
*/
override fun onRoomKeyRequest(request: IncomingRoomKeyRequest) {
val userId = request.userId
val deviceId = request.deviceId
val requestId = request.requestId

if (userId.isNullOrBlank() || deviceId.isNullOrBlank() || requestId.isNullOrBlank()) {
Timber.e("## handleKeyRequest() : invalid parameters")
return
}

//Do we already have alerts for this user/device
val mappingKey = keyForMap(deviceId, userId)
if (alertsToRequests.containsKey(mappingKey)) {
//just add the request, there is already an alert for this
alertsToRequests[mappingKey]?.add(request)
return
}

alertsToRequests[mappingKey] = ArrayList<IncomingRoomKeyRequest>().apply { this.add(request) }

//Add a notification for every incoming request
session.downloadKeys(Arrays.asList(userId), false, object : MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>> {
override fun onSuccess(data: MXUsersDevicesMap<MXDeviceInfo>) {
val deviceInfo = data.getObject(deviceId, userId)

if (null == deviceInfo) {
Timber.e("## displayKeyShareDialog() : No details found for device $userId:$deviceId")
//ignore
return
}

if (deviceInfo.isUnknown) {
session.setDeviceVerification(MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED, deviceId, userId)

deviceInfo.verified = MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED

//can we get more info on this device?
session.getDevicesList(object : MatrixCallback<DevicesListResponse> {
override fun onSuccess(data: DevicesListResponse) {
data.devices?.find { it.deviceId == deviceId }?.let {
postAlert(context, userId, deviceId, true, deviceInfo, it)
} ?: run {
postAlert(context, userId, deviceId, true, deviceInfo)
}
}

override fun onFailure(failure: Throwable) {
postAlert(context, userId, deviceId, true, deviceInfo)
}

})
} else {
postAlert(context, userId, deviceId, false, deviceInfo)
}
}

override fun onFailure(failure: Throwable) {
//ignore
Timber.e(failure, "## displayKeyShareDialog : downloadKeys")
}
})

}

private fun postAlert(context: Context,
userId: String,
deviceId: String,
wasNewDevice: Boolean,
deviceInfo: MXDeviceInfo?,
moreInfo: DeviceInfo? = null) {
val deviceName = if (TextUtils.isEmpty(deviceInfo!!.displayName())) deviceInfo.deviceId else deviceInfo.displayName()
val dialogText: String?

if (moreInfo != null) {
val lastSeenIp = if (moreInfo.lastSeenIp.isNullOrBlank()) {
context.getString(R.string.encryption_information_unknown_ip)
} else {
moreInfo.lastSeenIp
}

val lastSeenTime = moreInfo.lastSeenTs?.let { ts ->
val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
val date = Date(ts)

val time = dateFormatTime.format(date)
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())

dateFormat.format(date) + ", " + time
} ?: "-"

val lastSeenInfo = context.getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime)
dialogText = if (wasNewDevice) {
context.getString(R.string.you_added_a_new_device_with_info, deviceName, lastSeenInfo)
} else {
context.getString(R.string.your_unverified_device_requesting_with_info, deviceName, lastSeenInfo)
}
} else {
dialogText = if (wasNewDevice) {
context.getString(R.string.you_added_a_new_device, deviceName)
} else {
context.getString(R.string.your_unverified_device_requesting, deviceName)
}
}


val alert = PopupAlertManager.VectorAlert(
alertManagerId(deviceId, userId),
context.getString(R.string.key_share_request),
dialogText,
R.drawable.key_small
)

alert.colorRes = R.color.key_share_req_accent_color

val mappingKey = keyForMap(deviceId, userId)
alert.dismissedAction = Runnable {
denyAllRequests(mappingKey)
}

alert.addButton(
context.getString(R.string.start_verification_short_label),
Runnable {
alert.weakCurrentActivity?.get()?.let {
val intent = SASVerificationActivity.outgoingIntent(it,
session.sessionParams.credentials.userId,
userId, deviceId)
it.startActivity(intent)
}
},
false
)

alert.addButton(context.getString(R.string.share_without_verifying_short_label), Runnable {
shareAllSessions(mappingKey)
})

alert.addButton(context.getString(R.string.ignore_request_short_label), Runnable {
denyAllRequests(mappingKey)
})

PopupAlertManager.postVectorAlert(alert)
}

private fun denyAllRequests(mappingKey: String) {
alertsToRequests[mappingKey]?.forEach {
it.ignore?.run()
}
alertsToRequests.remove(mappingKey)
}

private fun shareAllSessions(mappingKey: String) {
alertsToRequests[mappingKey]?.forEach {
it.share?.run()
}
alertsToRequests.remove(mappingKey)
}

/**
* Manage a cancellation request.
*
* @param request the cancellation request.
*/
override fun onRoomKeyRequestCancellation(request: IncomingRoomKeyRequestCancellation) {
// see if we can find the request in the queue
val userId = request.userId
val deviceId = request.deviceId
val requestId = request.requestId

if (TextUtils.isEmpty(userId) || TextUtils.isEmpty(deviceId) || TextUtils.isEmpty(requestId)) {
Timber.e("## handleKeyRequestCancellation() : invalid parameters")
return
}

val alertMgrUniqueKey = alertManagerId(deviceId!!, userId!!)
alertsToRequests[alertMgrUniqueKey]?.removeAll {
it.deviceId == request.deviceId
&& it.userId == request.userId
&& it.requestId == request.requestId
}
if (alertsToRequests[alertMgrUniqueKey]?.isEmpty() == true) {
PopupAlertManager.cancelAlert(alertMgrUniqueKey)
alertsToRequests.remove(keyForMap(deviceId, userId))
}
}

override fun transactionCreated(tx: SasVerificationTransaction) {
}

override fun transactionUpdated(tx: SasVerificationTransaction) {
val state = tx.state
if (state == SasVerificationTxState.Verified) {
//ok it's verified, see if we have key request for that
shareAllSessions("${tx.otherDeviceId}${tx.otherUserId}")
PopupAlertManager.cancelAlert("ikr_${tx.otherDeviceId}${tx.otherUserId}")
}
}

override fun markedAsManuallyVerified(userId: String, deviceId: String) {
//accept related requests
shareAllSessions(keyForMap(deviceId, userId))
PopupAlertManager.cancelAlert(alertManagerId(deviceId, userId))
}

private fun keyForMap(deviceId: String, userId: String) = "$deviceId$userId"

private fun alertManagerId(deviceId: String, userId: String) = "ikr_$deviceId$userId"
}

View File

@ -30,10 +30,12 @@ import im.vector.riotredesign.features.popup.PopupAlertManager
class IncomingVerificationRequestHandler(val context: Context,
private val session: Session) : SasVerificationService.SasVerificationListener {

fun ensureStarted() {
init {
session.getSasVerificationService().addListener(this)
}

fun ensureStarted() = Unit

override fun transactionCreated(tx: SasVerificationTransaction) {}

override fun transactionUpdated(tx: SasVerificationTransaction) {

View File

@ -37,6 +37,7 @@ import im.vector.riotredesign.core.extensions.replaceFragment
import im.vector.riotredesign.core.platform.OnBackPressed
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseActivity
import im.vector.riotredesign.features.crypto.keysrequest.KeyRequestHandler
import im.vector.riotredesign.features.crypto.verification.IncomingVerificationRequestHandler
import im.vector.riotredesign.features.rageshake.BugReporter
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
@ -60,6 +61,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {

// TODO Move this elsewhere
private val incomingVerificationRequestHandler by inject<IncomingVerificationRequestHandler>()
// TODO Move this elsewhere
private val keyRequestHandler by inject<KeyRequestHandler>()

private var progress: ProgressDialog? = null

@ -105,6 +108,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
}

incomingVerificationRequestHandler.ensureStarted()
keyRequestHandler.ensureStarted()
}

override fun onDestroy() {

View File

@ -131,6 +131,6 @@

<!-- Notification (do not depends on theme -->
<color name="notification_accent_color">#368BD6</color>

<color name="key_share_req_accent_color">#ff812d</color>

</resources>