forked from GitHub-Mirror/riotX-android
1073 lines
42 KiB
Kotlin
Executable File
1073 lines
42 KiB
Kotlin
Executable File
/*
|
|
* Copyright 2016 OpenMarket Ltd
|
|
* Copyright 2017 Vector Creations Ltd
|
|
* Copyright 2018 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
|
|
|
|
import android.content.Context
|
|
import android.os.Handler
|
|
import android.os.Looper
|
|
import arrow.core.Try
|
|
import com.squareup.moshi.Types
|
|
import com.zhuinden.monarchy.Monarchy
|
|
import dagger.Lazy
|
|
import im.vector.matrix.android.api.MatrixCallback
|
|
import im.vector.matrix.android.api.auth.data.Credentials
|
|
import im.vector.matrix.android.api.failure.Failure
|
|
import im.vector.matrix.android.api.listeners.ProgressListener
|
|
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.keysbackup.KeysBackupService
|
|
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.events.model.Content
|
|
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.Membership
|
|
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
|
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
|
|
import im.vector.matrix.android.api.session.room.model.RoomMember
|
|
import im.vector.matrix.android.internal.crypto.actions.MegolmSessionDataImporter
|
|
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
|
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
|
|
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
|
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
|
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
|
|
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.event.RoomKeyContent
|
|
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
|
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
|
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
|
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
|
import im.vector.matrix.android.internal.crypto.tasks.*
|
|
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
|
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
|
import im.vector.matrix.android.internal.database.query.where
|
|
import im.vector.matrix.android.internal.di.CryptoDatabase
|
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
|
import im.vector.matrix.android.internal.extensions.foldToCallback
|
|
import im.vector.matrix.android.internal.session.SessionScope
|
|
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
|
|
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
|
|
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
|
|
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
|
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
|
import im.vector.matrix.android.internal.task.configureWith
|
|
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
|
import im.vector.matrix.android.internal.util.fetchCopied
|
|
import kotlinx.coroutines.*
|
|
import org.matrix.olm.OlmManager
|
|
import timber.log.Timber
|
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
import javax.inject.Inject
|
|
import kotlin.math.max
|
|
|
|
/**
|
|
* A `CryptoService` class instance manages the end-to-end crypto for a session.
|
|
*
|
|
*
|
|
* Messages posted by the user are automatically redirected to CryptoService in order to be encrypted
|
|
* before sending.
|
|
* In the other hand, received events goes through CryptoService for decrypting.
|
|
* CryptoService maintains all necessary keys and their sharing with other devices required for the crypto.
|
|
* Specially, it tracks all room membership changes events in order to do keys updates.
|
|
*/
|
|
@SessionScope
|
|
internal class DefaultCryptoService @Inject constructor(
|
|
// Olm Manager
|
|
private val olmManager: OlmManager,
|
|
// The credentials,
|
|
private val credentials: Credentials,
|
|
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
|
// the crypto store
|
|
private val cryptoStore: IMXCryptoStore,
|
|
// Olm device
|
|
private val olmDevice: MXOlmDevice,
|
|
// Set of parameters used to configure/customize the end-to-end crypto.
|
|
private val cryptoConfig: MXCryptoConfig = MXCryptoConfig(),
|
|
// Device list manager
|
|
private val deviceListManager: DeviceListManager,
|
|
// The key backup service.
|
|
private val keysBackup: KeysBackup,
|
|
//
|
|
private val objectSigner: ObjectSigner,
|
|
//
|
|
private val oneTimeKeysUploader: OneTimeKeysUploader,
|
|
//
|
|
private val roomDecryptorProvider: RoomDecryptorProvider,
|
|
// The SAS verification service.
|
|
private val sasVerificationService: DefaultSasVerificationService,
|
|
//
|
|
private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager,
|
|
//
|
|
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
|
|
// Actions
|
|
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
|
private val megolmSessionDataImporter: MegolmSessionDataImporter,
|
|
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
|
// Repository
|
|
private val megolmEncryptionFactory: MXMegolmEncryptionFactory,
|
|
private val olmEncryptionFactory: MXOlmEncryptionFactory,
|
|
private val deleteDeviceTask: DeleteDeviceTask,
|
|
private val deleteDeviceWithUserPasswordTask: DeleteDeviceWithUserPasswordTask,
|
|
// Tasks
|
|
private val getDevicesTask: GetDevicesTask,
|
|
private val setDeviceNameTask: SetDeviceNameTask,
|
|
private val uploadKeysTask: UploadKeysTask,
|
|
private val loadRoomMembersTask: LoadRoomMembersTask,
|
|
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
|
|
private val monarchy: Monarchy,
|
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
private val taskExecutor: TaskExecutor
|
|
) : CryptoService {
|
|
|
|
private val uiHandler = Handler(Looper.getMainLooper())
|
|
|
|
// MXEncrypting instance for each room.
|
|
private val roomEncryptors: MutableMap<String, IMXEncrypting> = HashMap()
|
|
private val isStarting = AtomicBoolean(false)
|
|
private val isStarted = AtomicBoolean(false)
|
|
|
|
fun onStateEvent(roomId: String, event: Event) {
|
|
when {
|
|
event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
|
|
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
|
|
event.getClearType() == EventType.STATE_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
|
|
}
|
|
}
|
|
|
|
fun onLiveEvent(roomId: String, event: Event) {
|
|
when {
|
|
event.getClearType() == EventType.ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
|
|
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
|
|
event.getClearType() == EventType.STATE_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
|
|
}
|
|
}
|
|
|
|
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
|
|
setDeviceNameTask
|
|
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) {
|
|
this.callback = callback
|
|
}
|
|
.executeBy(taskExecutor)
|
|
}
|
|
|
|
override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) {
|
|
deleteDeviceTask
|
|
.configureWith(DeleteDeviceTask.Params(deviceId)) {
|
|
this.callback = callback
|
|
}
|
|
.executeBy(taskExecutor)
|
|
}
|
|
|
|
override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>) {
|
|
deleteDeviceWithUserPasswordTask
|
|
.configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password)) {
|
|
this.callback = callback
|
|
}
|
|
.executeBy(taskExecutor)
|
|
}
|
|
|
|
override fun getCryptoVersion(context: Context, longFormat: Boolean): String {
|
|
return if (longFormat) olmManager.getDetailedVersion(context) else olmManager.version
|
|
}
|
|
|
|
override fun getMyDevice(): MXDeviceInfo {
|
|
return myDeviceInfoHolder.get().myDevice
|
|
}
|
|
|
|
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
|
|
getDevicesTask
|
|
.configureWith {
|
|
this.callback = callback
|
|
}
|
|
.executeBy(taskExecutor)
|
|
}
|
|
|
|
override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int {
|
|
return cryptoStore.inboundGroupSessionsCount(onlyBackedUp)
|
|
}
|
|
|
|
/**
|
|
* Provides the tracking status
|
|
*
|
|
* @param userId the user id
|
|
* @return the tracking status
|
|
*/
|
|
override fun getDeviceTrackingStatus(userId: String): Int {
|
|
return cryptoStore.getDeviceTrackingStatus(userId, DeviceListManager.TRACKING_STATUS_NOT_TRACKED)
|
|
}
|
|
|
|
/**
|
|
* Tell if the MXCrypto is started
|
|
*
|
|
* @return true if the crypto is started
|
|
*/
|
|
fun isStarted(): Boolean {
|
|
return isStarted.get()
|
|
}
|
|
|
|
/**
|
|
* Tells if the MXCrypto is starting.
|
|
*
|
|
* @return true if the crypto is starting
|
|
*/
|
|
fun isStarting(): Boolean {
|
|
return isStarting.get()
|
|
}
|
|
|
|
/**
|
|
* Start the crypto module.
|
|
* Device keys will be uploaded, then one time keys if there are not enough on the homeserver
|
|
* and, then, if this is the first time, this new device will be announced to all other users
|
|
* devices.
|
|
*
|
|
* @param isInitialSync true if it starts from an initial sync
|
|
*/
|
|
fun start(isInitialSync: Boolean) {
|
|
if (isStarted.get() || isStarting.get()) {
|
|
return
|
|
}
|
|
isStarting.set(true)
|
|
GlobalScope.launch(coroutineDispatchers.crypto) {
|
|
internalStart(isInitialSync)
|
|
}
|
|
}
|
|
|
|
private suspend fun internalStart(isInitialSync: Boolean) {
|
|
// Open the store
|
|
cryptoStore.open()
|
|
runCatching {
|
|
uploadDeviceKeys()
|
|
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
|
outgoingRoomKeyRequestManager.start()
|
|
keysBackup.checkAndStartKeysBackup()
|
|
if (isInitialSync) {
|
|
// refresh the devices list for each known room members
|
|
deviceListManager.invalidateAllDeviceLists()
|
|
deviceListManager.refreshOutdatedDeviceLists()
|
|
} else {
|
|
incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
|
|
}
|
|
}.fold(
|
|
{
|
|
isStarting.set(false)
|
|
isStarted.set(true)
|
|
},
|
|
{
|
|
Timber.e("Start failed: $it")
|
|
delay(1000)
|
|
isStarting.set(false)
|
|
internalStart(isInitialSync)
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Close the crypto
|
|
*/
|
|
fun close() = runBlocking(coroutineDispatchers.crypto) {
|
|
olmDevice.release()
|
|
cryptoStore.close()
|
|
outgoingRoomKeyRequestManager.stop()
|
|
}
|
|
|
|
override fun isCryptoEnabled(): Boolean {
|
|
// TODO Check that this test is correct
|
|
return olmDevice != null
|
|
}
|
|
|
|
/**
|
|
* @return the Keys backup Service
|
|
*/
|
|
override fun getKeysBackupService(): KeysBackupService {
|
|
return keysBackup
|
|
}
|
|
|
|
/**
|
|
* @return the SasVerificationService
|
|
*/
|
|
override fun getSasVerificationService(): SasVerificationService {
|
|
return sasVerificationService
|
|
}
|
|
|
|
/**
|
|
* A sync response has been received
|
|
*
|
|
* @param syncResponse the syncResponse
|
|
*/
|
|
fun onSyncCompleted(syncResponse: SyncResponse) {
|
|
GlobalScope.launch(coroutineDispatchers.crypto) {
|
|
if (syncResponse.deviceLists != null) {
|
|
deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left)
|
|
}
|
|
if (syncResponse.deviceOneTimeKeysCount != null) {
|
|
val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
|
|
oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
|
|
}
|
|
if (isStarted()) {
|
|
// Make sure we process to-device messages before generating new one-time-keys #2782
|
|
deviceListManager.refreshOutdatedDeviceLists()
|
|
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
|
incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find a device by curve25519 identity key
|
|
*
|
|
* @param senderKey the curve25519 key to match.
|
|
* @param algorithm the encryption algorithm.
|
|
* @return the device info, or null if not found / unsupported algorithm / crypto released
|
|
*/
|
|
override fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo? {
|
|
return if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM && algorithm != MXCRYPTO_ALGORITHM_OLM) {
|
|
// We only deal in olm keys
|
|
null
|
|
} else cryptoStore.deviceWithIdentityKey(senderKey)
|
|
}
|
|
|
|
/**
|
|
* Provides the device information for a device id and a user Id
|
|
*
|
|
* @param userId the user id
|
|
* @param deviceId the device id
|
|
*/
|
|
override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? {
|
|
return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) {
|
|
cryptoStore.getUserDevice(deviceId, userId)
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the devices as known
|
|
*
|
|
* @param devices the devices. Note that the verified member of the devices in this list will not be updated by this method.
|
|
* @param callback the asynchronous callback
|
|
*/
|
|
override fun setDevicesKnown(devices: List<MXDeviceInfo>, callback: MatrixCallback<Unit>?) {
|
|
// build a devices map
|
|
val devicesIdListByUserId = HashMap<String, List<String>>()
|
|
|
|
for (di in devices) {
|
|
var deviceIdsList: MutableList<String>? = devicesIdListByUserId[di.userId]?.toMutableList()
|
|
|
|
if (null == deviceIdsList) {
|
|
deviceIdsList = ArrayList()
|
|
devicesIdListByUserId[di.userId] = deviceIdsList
|
|
}
|
|
deviceIdsList.add(di.deviceId)
|
|
}
|
|
|
|
val userIds = devicesIdListByUserId.keys
|
|
|
|
for (userId in userIds) {
|
|
val storedDeviceIDs = cryptoStore.getUserDevices(userId)
|
|
|
|
// sanity checks
|
|
if (null != storedDeviceIDs) {
|
|
var isUpdated = false
|
|
val deviceIds = devicesIdListByUserId[userId]
|
|
|
|
deviceIds?.forEach { deviceId ->
|
|
val device = storedDeviceIDs[deviceId]
|
|
|
|
// assume if the device is either verified or blocked
|
|
// it means that the device is known
|
|
if (device?.isUnknown == true) {
|
|
device.verified = MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED
|
|
isUpdated = true
|
|
}
|
|
}
|
|
|
|
if (isUpdated) {
|
|
cryptoStore.storeUserDevices(userId, storedDeviceIDs)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
callback?.onSuccess(Unit)
|
|
}
|
|
|
|
/**
|
|
* Update the blocked/verified state of the given device.
|
|
*
|
|
* @param verificationStatus the new verification status
|
|
* @param deviceId the unique identifier for the device.
|
|
* @param userId the owner of the device
|
|
*/
|
|
override fun setDeviceVerification(verificationStatus: Int, deviceId: String, userId: String) {
|
|
setDeviceVerificationAction.handle(verificationStatus, deviceId, userId)
|
|
}
|
|
|
|
/**
|
|
* Configure a room to use encryption.
|
|
*
|
|
* @param roomId the room id to enable encryption in.
|
|
* @param algorithm the encryption config for the room.
|
|
* @param inhibitDeviceQuery true to suppress device list query for users in the room (for now)
|
|
* @param membersId list of members to start tracking their devices
|
|
* @return true if the operation succeeds.
|
|
*/
|
|
private suspend fun setEncryptionInRoom(roomId: String,
|
|
algorithm: String?,
|
|
inhibitDeviceQuery: Boolean,
|
|
membersId: List<String>): Boolean {
|
|
// If we already have encryption in this room, we should ignore this event
|
|
// (for now at least. Maybe we should alert the user somehow?)
|
|
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
|
|
|
|
if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) {
|
|
Timber.e("## setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId")
|
|
return false
|
|
}
|
|
|
|
val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm)
|
|
|
|
if (!encryptingClass) {
|
|
Timber.e("## setEncryptionInRoom() : Unable to encrypt room ${roomId} with $algorithm")
|
|
return false
|
|
}
|
|
|
|
cryptoStore.storeRoomAlgorithm(roomId, algorithm!!)
|
|
|
|
val alg: IMXEncrypting = when (algorithm) {
|
|
MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId)
|
|
else -> olmEncryptionFactory.create(roomId)
|
|
}
|
|
|
|
synchronized(roomEncryptors) {
|
|
roomEncryptors.put(roomId, alg)
|
|
}
|
|
|
|
// if encryption was not previously enabled in this room, we will have been
|
|
// ignoring new device events for these users so far. We may well have
|
|
// up-to-date lists for some users, for instance if we were sharing other
|
|
// e2e rooms with them, so there is room for optimisation here, but for now
|
|
// we just invalidate everyone in the room.
|
|
if (null == existingAlgorithm) {
|
|
Timber.v("Enabling encryption in $roomId for the first time; invalidating device lists for all users therein")
|
|
|
|
val userIds = ArrayList(membersId)
|
|
|
|
deviceListManager.startTrackingDeviceList(userIds)
|
|
|
|
if (!inhibitDeviceQuery) {
|
|
deviceListManager.refreshOutdatedDeviceLists()
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Tells if a room is encrypted
|
|
*
|
|
* @param roomId the room id
|
|
* @return true if the room is encrypted
|
|
*/
|
|
override fun isRoomEncrypted(roomId: String): Boolean {
|
|
val encryptionEvent = monarchy.fetchCopied {
|
|
EventEntity.where(it, roomId = roomId, type = EventType.ENCRYPTION).findFirst()
|
|
}
|
|
return encryptionEvent != null
|
|
}
|
|
|
|
/**
|
|
* @return the stored device keys for a user.
|
|
*/
|
|
override fun getUserDevices(userId: String): MutableList<MXDeviceInfo> {
|
|
val map = cryptoStore.getUserDevices(userId)
|
|
return if (null != map) ArrayList(map.values) else ArrayList()
|
|
}
|
|
|
|
fun isEncryptionEnabledForInvitedUser(): Boolean {
|
|
return cryptoConfig.enableEncryptionForInvitedMembers
|
|
}
|
|
|
|
override fun getEncryptionAlgorithm(roomId: String): String? {
|
|
return cryptoStore.getRoomAlgorithm(roomId)
|
|
}
|
|
|
|
/**
|
|
* Determine whether we should encrypt messages for invited users in this room.
|
|
* <p>
|
|
* Check here whether the invited members are allowed to read messages in the room history
|
|
* from the point they were invited onwards.
|
|
*
|
|
* @return true if we should encrypt messages for invited users.
|
|
*/
|
|
override fun shouldEncryptForInvitedMembers(roomId: String): Boolean {
|
|
return cryptoStore.shouldEncryptForInvitedMembers(roomId)
|
|
}
|
|
|
|
/**
|
|
* Encrypt an event content according to the configuration of the room.
|
|
*
|
|
* @param eventContent the content of the event.
|
|
* @param eventType the type of the event.
|
|
* @param roomId the room identifier the event will be sent.
|
|
* @param callback the asynchronous callback
|
|
*/
|
|
override fun encryptEventContent(eventContent: Content,
|
|
eventType: String,
|
|
roomId: String,
|
|
callback: MatrixCallback<MXEncryptEventContentResult>) {
|
|
GlobalScope.launch(coroutineDispatchers.crypto) {
|
|
if (!isStarted()) {
|
|
Timber.v("## encryptEventContent() : wait after e2e init")
|
|
internalStart(false)
|
|
}
|
|
val userIds = getRoomUserIds(roomId)
|
|
var alg = synchronized(roomEncryptors) {
|
|
roomEncryptors[roomId]
|
|
}
|
|
if (alg == null) {
|
|
val algorithm = getEncryptionAlgorithm(roomId)
|
|
if (algorithm != null) {
|
|
if (setEncryptionInRoom(roomId, algorithm, false, userIds)) {
|
|
synchronized(roomEncryptors) {
|
|
alg = roomEncryptors[roomId]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
val safeAlgorithm = alg
|
|
if (safeAlgorithm != null) {
|
|
val t0 = System.currentTimeMillis()
|
|
Timber.v("## encryptEventContent() starts")
|
|
runCatching {
|
|
safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
|
|
}
|
|
.fold(
|
|
{
|
|
Timber.v("## encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
|
|
callback.onSuccess(MXEncryptEventContentResult(it, EventType.ENCRYPTED))
|
|
},
|
|
{ callback.onFailure(it) }
|
|
|
|
)
|
|
} else {
|
|
val algorithm = getEncryptionAlgorithm(roomId)
|
|
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON,
|
|
algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON)
|
|
Timber.e("## encryptEventContent() : $reason")
|
|
callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason)))
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decrypt an event
|
|
*
|
|
* @param event the raw event.
|
|
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
|
* @return the MXEventDecryptionResult data, or throw in case of error
|
|
*/
|
|
@Throws(MXCryptoError::class)
|
|
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
|
return runBlocking {
|
|
internalDecryptEvent(event, timeline)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decrypt an event asynchronously
|
|
*
|
|
* @param event the raw event.
|
|
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
|
* @param callback the callback to return data or null
|
|
*/
|
|
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
|
|
GlobalScope.launch {
|
|
val result = runCatching {
|
|
withContext(coroutineDispatchers.crypto) {
|
|
internalDecryptEvent(event, timeline)
|
|
}
|
|
}
|
|
result.foldToCallback(callback)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decrypt an event
|
|
*
|
|
* @param event the raw event.
|
|
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
|
* @return the MXEventDecryptionResult data, or null in case of error
|
|
*/
|
|
private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
|
val eventContent = event.content
|
|
if (eventContent == null) {
|
|
Timber.e("## decryptEvent : empty event content")
|
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
|
|
} else {
|
|
val algorithm = eventContent["algorithm"]?.toString()
|
|
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
|
|
if (alg == null) {
|
|
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
|
|
Timber.e("## decryptEvent() : $reason")
|
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
|
|
} else {
|
|
return alg.decryptEvent(event, timeline)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset replay attack data for the given timeline.
|
|
*
|
|
* @param timelineId the timeline id
|
|
*/
|
|
fun resetReplayAttackCheckInTimeline(timelineId: String) {
|
|
olmDevice.resetReplayAttackCheckInTimeline(timelineId)
|
|
}
|
|
|
|
/**
|
|
* Handle the 'toDevice' event
|
|
*
|
|
* @param event the event
|
|
*/
|
|
fun onToDeviceEvent(event: Event) {
|
|
GlobalScope.launch(coroutineDispatchers.crypto) {
|
|
when (event.getClearType()) {
|
|
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
|
onRoomKeyEvent(event)
|
|
}
|
|
EventType.ROOM_KEY_REQUEST -> {
|
|
incomingRoomKeyRequestManager.onRoomKeyRequestEvent(event)
|
|
}
|
|
else -> {
|
|
//ignore
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle a key event.
|
|
*
|
|
* @param event the key event.
|
|
*/
|
|
private fun onRoomKeyEvent(event: Event) {
|
|
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
|
|
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
|
|
Timber.e("## onRoomKeyEvent() : missing fields")
|
|
return
|
|
}
|
|
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm)
|
|
if (alg == null) {
|
|
Timber.e("## onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
|
|
return
|
|
}
|
|
alg.onRoomKeyEvent(event, keysBackup)
|
|
}
|
|
|
|
/**
|
|
* Handle an m.room.encryption event.
|
|
*
|
|
* @param event the encryption event.
|
|
*/
|
|
private fun onRoomEncryptionEvent(roomId: String, event: Event) {
|
|
GlobalScope.launch(coroutineDispatchers.crypto) {
|
|
val params = LoadRoomMembersTask.Params(roomId)
|
|
try {
|
|
loadRoomMembersTask.execute(params)
|
|
val userIds = getRoomUserIds(roomId)
|
|
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
|
|
} catch (throwable: Throwable) {
|
|
Timber.e(throwable)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun getRoomUserIds(roomId: String): List<String> {
|
|
var userIds: List<String> = emptyList()
|
|
monarchy.doWithRealm { realm ->
|
|
// Check whether the event content must be encrypted for the invited members.
|
|
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
|
|
&& shouldEncryptForInvitedMembers(roomId)
|
|
|
|
userIds = if (encryptForInvitedMembers) {
|
|
RoomMembers(realm, roomId).getActiveRoomMemberIds()
|
|
} else {
|
|
RoomMembers(realm, roomId).getJoinedRoomMemberIds()
|
|
}
|
|
|
|
}
|
|
return userIds
|
|
}
|
|
|
|
/**
|
|
* Handle a change in the membership state of a member of a room.
|
|
*
|
|
* @param event the membership event causing the change
|
|
*/
|
|
private fun onRoomMembershipEvent(roomId: String, event: Event) {
|
|
val alg: IMXEncrypting?
|
|
|
|
synchronized(roomEncryptors) {
|
|
alg = roomEncryptors[roomId]
|
|
}
|
|
|
|
if (null == alg) {
|
|
// No encrypting in this room
|
|
return
|
|
}
|
|
event.stateKey?.let { userId ->
|
|
val roomMember: RoomMember? = event.content.toModel()
|
|
val membership = roomMember?.membership
|
|
if (membership == Membership.JOIN) {
|
|
// make sure we are tracking the deviceList for this user.
|
|
deviceListManager.startTrackingDeviceList(listOf(userId))
|
|
} else if (membership == Membership.INVITE
|
|
&& shouldEncryptForInvitedMembers(roomId)
|
|
&& cryptoConfig.enableEncryptionForInvitedMembers) {
|
|
// track the deviceList for this invited user.
|
|
// Caution: there's a big edge case here in that federated servers do not
|
|
// know what other servers are in the room at the time they've been invited.
|
|
// They therefore will not send device updates if a user logs in whilst
|
|
// their state is invite.
|
|
deviceListManager.startTrackingDeviceList(listOf(userId))
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) {
|
|
val eventContent = event.content.toModel<RoomHistoryVisibilityContent>()
|
|
eventContent?.historyVisibility?.let {
|
|
cryptoStore.setShouldEncryptForInvitedMembers(roomId, it != RoomHistoryVisibility.JOINED)
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Upload my user's device keys.
|
|
*/
|
|
private suspend fun uploadDeviceKeys(): KeysUploadResponse {
|
|
// Prepare the device keys data to send
|
|
// Sign it
|
|
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary())
|
|
getMyDevice().signatures = objectSigner.signObject(canonicalJson)
|
|
|
|
// For now, we set the device id explicitly, as we may not be using the
|
|
// same one as used in login.
|
|
val uploadDeviceKeysParams = UploadKeysTask.Params(getMyDevice().toDeviceKeys(), null, getMyDevice().deviceId)
|
|
return uploadKeysTask.execute(uploadDeviceKeysParams)
|
|
}
|
|
|
|
/**
|
|
* Export the crypto keys
|
|
*
|
|
* @param password the password
|
|
* @param callback the exported keys
|
|
*/
|
|
override fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>) {
|
|
GlobalScope.launch(coroutineDispatchers.main) {
|
|
runCatching {
|
|
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT)
|
|
}.fold(callback::onSuccess, callback::onFailure)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Export the crypto keys
|
|
*
|
|
* @param password the password
|
|
* @param anIterationCount the encryption iteration count (0 means no encryption)
|
|
* @param callback the exported keys
|
|
*/
|
|
private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray {
|
|
return withContext(coroutineDispatchers.crypto) {
|
|
val iterationCount = max(0, anIterationCount)
|
|
|
|
val exportedSessions = cryptoStore.getInboundGroupSessions().mapNotNull { it.exportKeys() }
|
|
|
|
val adapter = MoshiProvider.providesMoshi()
|
|
.adapter(List::class.java)
|
|
|
|
MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Import the room keys
|
|
*
|
|
* @param roomKeysAsArray the room keys as array.
|
|
* @param password the password
|
|
* @param progressListener the progress listener
|
|
* @param callback the asynchronous callback.
|
|
*/
|
|
override fun importRoomKeys(roomKeysAsArray: ByteArray,
|
|
password: String,
|
|
progressListener: ProgressListener?,
|
|
callback: MatrixCallback<ImportRoomKeysResult>) {
|
|
GlobalScope.launch(coroutineDispatchers.main) {
|
|
withContext(coroutineDispatchers.crypto) {
|
|
Try {
|
|
Timber.v("## importRoomKeys starts")
|
|
|
|
val t0 = System.currentTimeMillis()
|
|
val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password)
|
|
val t1 = System.currentTimeMillis()
|
|
|
|
Timber.v("## importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms")
|
|
|
|
val importedSessions = MoshiProvider.providesMoshi()
|
|
.adapter<List<MegolmSessionData>>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java))
|
|
.fromJson(roomKeys)
|
|
|
|
val t2 = System.currentTimeMillis()
|
|
|
|
Timber.v("## importRoomKeys : JSON parsing ${t2 - t1} ms")
|
|
|
|
if (importedSessions == null) {
|
|
throw Exception("Error")
|
|
}
|
|
|
|
megolmSessionDataImporter.handle(importedSessions, true, uiHandler, progressListener)
|
|
}
|
|
}.foldToCallback(callback)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the warn status when some unknown devices are detected.
|
|
*
|
|
* @param warn true to warn when some unknown devices are detected.
|
|
*/
|
|
override fun setWarnOnUnknownDevices(warn: Boolean) {
|
|
warnOnUnknownDevicesRepository.setWarnOnUnknownDevices(warn)
|
|
}
|
|
|
|
/**
|
|
* Check if the user ids list have some unknown devices.
|
|
* A success means there is no unknown devices.
|
|
* If there are some unknown devices, a MXCryptoError.UnknownDevice exception is triggered.
|
|
*
|
|
* @param userIds the user ids list
|
|
* @param callback the asynchronous callback.
|
|
*/
|
|
fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) {
|
|
// force the refresh to ensure that the devices list is up-to-date
|
|
GlobalScope.launch(coroutineDispatchers.crypto) {
|
|
runCatching { deviceListManager.downloadKeys(userIds, true) }
|
|
.fold(
|
|
{
|
|
val unknownDevices = getUnknownDevices(it)
|
|
if (unknownDevices.map.isEmpty()) {
|
|
callback.onSuccess(Unit)
|
|
} else {
|
|
// trigger an an unknown devices exception
|
|
callback.onFailure(Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices)))
|
|
}
|
|
},
|
|
{ callback.onFailure(it) }
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the global override for whether the client should ever send encrypted
|
|
* messages to unverified devices.
|
|
* If false, it can still be overridden per-room.
|
|
* If true, it overrides the per-room settings.
|
|
*
|
|
* @param block true to unilaterally blacklist all
|
|
*/
|
|
override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) {
|
|
cryptoStore.setGlobalBlacklistUnverifiedDevices(block)
|
|
}
|
|
|
|
/**
|
|
* Tells whether the client should ever send encrypted messages to unverified devices.
|
|
* The default value is false.
|
|
* This function must be called in the getEncryptingThreadHandler() thread.
|
|
*
|
|
* @return true to unilaterally blacklist all unverified devices.
|
|
*/
|
|
override fun getGlobalBlacklistUnverifiedDevices(): Boolean {
|
|
return cryptoStore.getGlobalBlacklistUnverifiedDevices()
|
|
}
|
|
|
|
/**
|
|
* Tells whether the client should encrypt messages only for the verified devices
|
|
* in this room.
|
|
* The default value is false.
|
|
*
|
|
* @param roomId the room id
|
|
* @return true if the client should encrypt messages only for the verified devices.
|
|
*/
|
|
// TODO add this info in CryptoRoomEntity?
|
|
override fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean {
|
|
return roomId?.let { cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(it) }
|
|
?: false
|
|
}
|
|
|
|
/**
|
|
* Manages the room black-listing for unverified devices.
|
|
*
|
|
* @param roomId the room id
|
|
* @param add true to add the room id to the list, false to remove it.
|
|
*/
|
|
private fun setRoomBlacklistUnverifiedDevices(roomId: String, add: Boolean) {
|
|
val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList()
|
|
|
|
if (add) {
|
|
if (roomId !in roomIds) {
|
|
roomIds.add(roomId)
|
|
}
|
|
} else {
|
|
roomIds.remove(roomId)
|
|
}
|
|
|
|
cryptoStore.setRoomsListBlacklistUnverifiedDevices(roomIds)
|
|
}
|
|
|
|
|
|
/**
|
|
* Add this room to the ones which don't encrypt messages to unverified devices.
|
|
*
|
|
* @param roomId the room id
|
|
*/
|
|
override fun setRoomBlacklistUnverifiedDevices(roomId: String) {
|
|
setRoomBlacklistUnverifiedDevices(roomId, true)
|
|
}
|
|
|
|
/**
|
|
* Remove this room to the ones which don't encrypt messages to unverified devices.
|
|
*
|
|
* @param roomId the room id
|
|
*/
|
|
override fun setRoomUnBlacklistUnverifiedDevices(roomId: String) {
|
|
setRoomBlacklistUnverifiedDevices(roomId, false)
|
|
}
|
|
|
|
// TODO Check if this method is still necessary
|
|
/**
|
|
* Cancel any earlier room key request
|
|
*
|
|
* @param requestBody requestBody
|
|
*/
|
|
override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
|
outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody)
|
|
}
|
|
|
|
/**
|
|
* Re request the encryption keys required to decrypt an event.
|
|
*
|
|
* @param event the event to decrypt again.
|
|
*/
|
|
override fun reRequestRoomKeyForEvent(event: Event) {
|
|
val wireContent = event.content
|
|
if (wireContent == null) {
|
|
Timber.e("## reRequestRoomKeyForEvent Failed to re-request key, null content")
|
|
return
|
|
}
|
|
|
|
val requestBody = RoomKeyRequestBody()
|
|
|
|
requestBody.roomId = event.roomId
|
|
requestBody.algorithm = wireContent["algorithm"]?.toString()
|
|
requestBody.senderKey = wireContent["sender_key"]?.toString()
|
|
requestBody.sessionId = wireContent["session_id"]?.toString()
|
|
|
|
outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody)
|
|
}
|
|
|
|
/**
|
|
* Add a RoomKeysRequestListener listener.
|
|
*
|
|
* @param listener listener
|
|
*/
|
|
override fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) {
|
|
incomingRoomKeyRequestManager.addRoomKeysRequestListener(listener)
|
|
}
|
|
|
|
/**
|
|
* Add a RoomKeysRequestListener listener.
|
|
*
|
|
* @param listener listener
|
|
*/
|
|
override fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) {
|
|
incomingRoomKeyRequestManager.removeRoomKeysRequestListener(listener)
|
|
}
|
|
|
|
/**
|
|
* Provides the list of unknown devices
|
|
*
|
|
* @param devicesInRoom the devices map
|
|
* @return the unknown devices map
|
|
*/
|
|
private fun getUnknownDevices(devicesInRoom: MXUsersDevicesMap<MXDeviceInfo>): MXUsersDevicesMap<MXDeviceInfo> {
|
|
val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>()
|
|
val userIds = devicesInRoom.userIds
|
|
for (userId in userIds) {
|
|
devicesInRoom.getUserDeviceIds(userId)?.forEach { deviceId ->
|
|
devicesInRoom.getObject(userId, deviceId)
|
|
?.takeIf { it.isUnknown }
|
|
?.let {
|
|
unknownDevices.setObject(userId, deviceId, it)
|
|
}
|
|
}
|
|
}
|
|
|
|
return unknownDevices
|
|
}
|
|
|
|
override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) {
|
|
GlobalScope.launch(coroutineDispatchers.crypto) {
|
|
runCatching {
|
|
deviceListManager.downloadKeys(userIds, forceDownload)
|
|
}.foldToCallback(callback)
|
|
}
|
|
}
|
|
|
|
override fun clearCryptoCache(callback: MatrixCallback<Unit>) {
|
|
clearCryptoDataTask
|
|
.configureWith {
|
|
this.callback = callback
|
|
}
|
|
.executeBy(taskExecutor)
|
|
}
|
|
|
|
override fun addNewSessionListener(newSessionListener: NewSessionListener) {
|
|
roomDecryptorProvider.addNewSessionListener(newSessionListener)
|
|
}
|
|
|
|
override fun removeSessionListener(listener: NewSessionListener) {
|
|
roomDecryptorProvider.removeSessionListener(listener)
|
|
}
|
|
/* ==========================================================================================
|
|
* DEBUG INFO
|
|
* ========================================================================================== */
|
|
|
|
override fun toString(): String {
|
|
return "DefaultCryptoService of " + credentials.userId + " (" + credentials.deviceId + ")"
|
|
}
|
|
}
|