diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt index ee2ae132..e720a75c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -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, forceDownload: Boolean, callback: MatrixCallback>) + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt index 4825699e..74105a18 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt @@ -1050,12 +1050,22 @@ internal class CryptoManager( return unknownDevices } -/* ========================================================================================== - * DEBUG INFO - * ========================================================================================== */ + override fun downloadKeys(userIds: List, forceDownload: Boolean, callback: MatrixCallback>) { + 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 + ")" - } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt index 4dc0d1d0..06f70ee2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt @@ -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") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 12e52576..76f0ccea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -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, forceDownload: Boolean, callback: MatrixCallback>) { + cryptoService.downloadKeys(userIds, forceDownload, callback) + } + // Private methods ***************************************************************************** private fun assertMainThread() { diff --git a/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt b/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt index fb7ad75e..5b875fd1 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt @@ -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()) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/keysrequest/KeyRequestHandler.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysrequest/KeyRequestHandler.kt new file mode 100644 index 00000000..30d16a62 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysrequest/KeyRequestHandler.kt @@ -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>() + + 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().apply { this.add(request) } + + //Add a notification for every incoming request + session.downloadKeys(Arrays.asList(userId), false, object : MatrixCallback> { + override fun onSuccess(data: MXUsersDevicesMap) { + 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 { + 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" +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/IncomingVerificationRequestHandler.kt index 3f89a161..87cb3b54 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -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) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt index bdd3a1b9..c507224d 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt @@ -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() + // TODO Move this elsewhere + private val keyRequestHandler by inject() private var progress: ProgressDialog? = null @@ -105,6 +108,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { } incomingVerificationRequestHandler.ensureStarted() + keyRequestHandler.ensureStarted() } override fun onDestroy() { diff --git a/vector/src/main/res/values/colors_riot.xml b/vector/src/main/res/values/colors_riot.xml index 08f35da8..365a6bee 100644 --- a/vector/src/main/res/values/colors_riot.xml +++ b/vector/src/main/res/values/colors_riot.xml @@ -131,6 +131,6 @@ #368BD6 - + #ff812d