mirror of
https://github.com/vector-im/element-android.git
synced 2024-12-02 16:06:40 +08:00
Try to clean up after merging upstream develop
This commit is contained in:
parent
725e56db08
commit
7e49bad411
@ -37,7 +37,6 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic
|
|||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
import org.matrix.android.sdk.api.session.events.model.Content
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||||
import org.matrix.android.sdk.internal.crypto.NewSessionListener
|
|
||||||
|
|
||||||
interface CryptoService {
|
interface CryptoService {
|
||||||
|
|
||||||
|
@ -23,8 +23,7 @@ interface NewSessionListener {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param roomId the room id where the new Megolm session has been created for, may be null when importing from external sessions
|
* @param roomId the room id where the new Megolm session has been created for, may be null when importing from external sessions
|
||||||
* @param senderKey the sender key of the device which the Megolm session is shared with
|
|
||||||
* @param sessionId the session id of the Megolm session
|
* @param sessionId the session id of the Megolm session
|
||||||
*/
|
*/
|
||||||
fun onNewSession(roomId: String?, senderKey: String, sessionId: String)
|
fun onNewSession(roomId: String?, sessionId: String)
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ package org.matrix.android.sdk.api.session.crypto.crosssigning
|
|||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
|
||||||
interface CrossSigningService {
|
interface CrossSigningService {
|
||||||
|
@ -104,6 +104,11 @@ class MXUsersDevicesMap<E> {
|
|||||||
map.clear()
|
map.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun join(other: Map<out String, HashMap<String, E>>) {
|
||||||
|
map.putAll(other)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add entries from another MXUsersDevicesMap
|
* Add entries from another MXUsersDevicesMap
|
||||||
*
|
*
|
||||||
|
@ -26,8 +26,6 @@ import org.matrix.android.sdk.api.session.crypto.CryptoService
|
|||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||||
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
|
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.ComputeTrustTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultComputeTrustTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi
|
import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
|
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultCreateKeysBackupVersionTask
|
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DefaultCreateKeysBackupVersionTask
|
||||||
@ -68,7 +66,6 @@ import org.matrix.android.sdk.internal.crypto.tasks.DefaultDownloadKeysForUsers
|
|||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultEncryptEventTask
|
import org.matrix.android.sdk.internal.crypto.tasks.DefaultEncryptEventTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDeviceInfoTask
|
import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDeviceInfoTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDevicesTask
|
import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDevicesTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultInitializeCrossSigningTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendEventTask
|
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendEventTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendToDeviceTask
|
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendToDeviceTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendVerificationMessageTask
|
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendVerificationMessageTask
|
||||||
@ -81,7 +78,6 @@ import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask
|
|||||||
import org.matrix.android.sdk.internal.crypto.tasks.EncryptEventTask
|
import org.matrix.android.sdk.internal.crypto.tasks.EncryptEventTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask
|
import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask
|
import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask
|
import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
|
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
|
||||||
@ -247,12 +243,6 @@ internal abstract class CryptoModule {
|
|||||||
@Binds
|
@Binds
|
||||||
abstract fun bindCryptoStore(store: RealmCryptoStore): IMXCryptoStore
|
abstract fun bindCryptoStore(store: RealmCryptoStore): IMXCryptoStore
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindComputeShieldTrustTask(task: DefaultComputeTrustTask): ComputeTrustTask
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindInitializeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask
|
abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
package org.matrix.android.sdk.internal.crypto
|
package org.matrix.android.sdk.internal.crypto
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -13,8 +13,18 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
package org.matrix.android.sdk.internal.crypto
|
||||||
|
|
||||||
interface NewSessionListener {
|
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
||||||
fun onNewSession(roomId: String?, sessionId: String)
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class DecryptEventUseCase @Inject constructor(olmMachineProvider: OlmMachineProvider) {
|
||||||
|
|
||||||
|
private val olmMachine = olmMachineProvider.olmMachine
|
||||||
|
|
||||||
|
suspend operator fun invoke(event: Event): MXEventDecryptionResult {
|
||||||
|
return olmMachine.decryptRoomEvent(event)
|
||||||
|
}
|
||||||
}
|
}
|
@ -138,7 +138,11 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
private val keysBackupService: RustKeyBackupService,
|
private val keysBackupService: RustKeyBackupService,
|
||||||
private val megolmSessionImportManager: MegolmSessionImportManager,
|
private val megolmSessionImportManager: MegolmSessionImportManager,
|
||||||
private val olmMachineProvider: OlmMachineProvider,
|
private val olmMachineProvider: OlmMachineProvider,
|
||||||
private val liveEventManager: dagger.Lazy<StreamEventsManager>
|
private val liveEventManager: dagger.Lazy<StreamEventsManager>,
|
||||||
|
private val prepareToEncrypt: PrepareToEncryptUseCase,
|
||||||
|
private val encryptEventContent: EncryptEventContentUseCase,
|
||||||
|
private val shouldEncryptForInvitedMembers: ShouldEncryptForInvitedMembersUseCase,
|
||||||
|
private val getRoomUserIds: GetRoomUserIdsUseCase,
|
||||||
) : CryptoService {
|
) : CryptoService {
|
||||||
|
|
||||||
private val isStarting = AtomicBoolean(false)
|
private val isStarting = AtomicBoolean(false)
|
||||||
@ -152,14 +156,12 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
// private val deviceObserver: DeviceUpdateObserver = DeviceUpdateObserver()
|
// private val deviceObserver: DeviceUpdateObserver = DeviceUpdateObserver()
|
||||||
|
|
||||||
// Locks for some of our operations
|
// Locks for some of our operations
|
||||||
private val keyClaimLock: Mutex = Mutex()
|
|
||||||
private val outgoingRequestsProcessor = OutgoingRequestsProcessor(
|
private val outgoingRequestsProcessor = OutgoingRequestsProcessor(
|
||||||
requestSender = requestSender,
|
requestSender = requestSender,
|
||||||
coroutineScope = cryptoCoroutineScope,
|
coroutineScope = cryptoCoroutineScope,
|
||||||
cryptoSessionInfoProvider = cryptoSessionInfoProvider,
|
cryptoSessionInfoProvider = cryptoSessionInfoProvider,
|
||||||
shieldComputer = crossSigningService::shieldForGroup
|
shieldComputer = crossSigningService::shieldForGroup
|
||||||
)
|
)
|
||||||
private val roomKeyShareLocks: ConcurrentHashMap<String, Mutex> = ConcurrentHashMap()
|
|
||||||
|
|
||||||
fun onStateEvent(roomId: String, event: Event) {
|
fun onStateEvent(roomId: String, event: Event) {
|
||||||
when (event.type) {
|
when (event.type) {
|
||||||
@ -450,27 +452,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
override suspend fun encryptEventContent(eventContent: Content,
|
override suspend fun encryptEventContent(eventContent: Content,
|
||||||
eventType: String,
|
eventType: String,
|
||||||
roomId: String): MXEncryptEventContentResult {
|
roomId: String): MXEncryptEventContentResult {
|
||||||
// moved to crypto scope to have up to date values
|
return encryptEventContent.invoke(eventContent, eventType, roomId)
|
||||||
return withContext(coroutineDispatchers.crypto) {
|
|
||||||
val algorithm = getEncryptionAlgorithm(roomId)
|
|
||||||
if (algorithm != null) {
|
|
||||||
val userIds = getRoomUserIds(roomId)
|
|
||||||
val t0 = System.currentTimeMillis()
|
|
||||||
Timber.tag(loggerTag.value).v("encryptEventContent() starts")
|
|
||||||
measureTimeMillis {
|
|
||||||
preshareRoomKey(roomId, userIds)
|
|
||||||
}.also {
|
|
||||||
Timber.d("Shared room key in room $roomId took $it ms")
|
|
||||||
}
|
|
||||||
val content = encrypt(roomId, eventType, eventContent)
|
|
||||||
Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
|
|
||||||
MXEncryptEventContentResult(content, EventType.ENCRYPTED)
|
|
||||||
} else {
|
|
||||||
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON)
|
|
||||||
Timber.tag(loggerTag.value).e("encryptEventContent() : failed $reason")
|
|
||||||
throw Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun discardOutboundSession(roomId: String) {
|
override fun discardOutboundSession(roomId: String) {
|
||||||
@ -523,12 +505,6 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getRoomUserIds(roomId: String): List<String> {
|
|
||||||
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser() &&
|
|
||||||
shouldEncryptForInvitedMembers(roomId)
|
|
||||||
return cryptoSessionInfoProvider.getRoomUserIds(roomId, encryptForInvitedMembers)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a change in the membership state of a member of a room.
|
* Handle a change in the membership state of a member of a room.
|
||||||
*
|
*
|
||||||
@ -593,7 +569,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
when (event.type) {
|
when (event.type) {
|
||||||
EventType.ROOM_KEY -> {
|
EventType.ROOM_KEY -> {
|
||||||
val content = event.getClearContent().toModel<RoomKeyContent>() ?: return@forEach
|
val content = event.getClearContent().toModel<RoomKeyContent>() ?: return@forEach
|
||||||
|
content.sessionKey
|
||||||
val roomId = content.sessionId ?: return@forEach
|
val roomId = content.sessionId ?: return@forEach
|
||||||
val sessionId = content.sessionId
|
val sessionId = content.sessionId
|
||||||
|
|
||||||
@ -622,80 +598,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun preshareRoomKey(roomId: String, roomMembers: List<String>) {
|
|
||||||
claimMissingKeys(roomMembers)
|
|
||||||
val keyShareLock = roomKeyShareLocks.getOrPut(roomId) { Mutex() }
|
|
||||||
var sharedKey = false
|
|
||||||
keyShareLock.withLock {
|
|
||||||
coroutineScope {
|
|
||||||
olmMachine.shareRoomKey(roomId, roomMembers).map {
|
|
||||||
when (it) {
|
|
||||||
is Request.ToDevice -> {
|
|
||||||
sharedKey = true
|
|
||||||
async {
|
|
||||||
sendToDevice(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// This request can only be a to-device request but
|
|
||||||
// we need to handle all our cases and put this
|
|
||||||
// async block for our joinAll to work.
|
|
||||||
async {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.joinAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we sent out a room key over to-device messages it's likely that we created a new one
|
|
||||||
// Try to back the key up
|
|
||||||
if (sharedKey) {
|
|
||||||
keysBackupService.maybeBackupKeys()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun claimMissingKeys(roomMembers: List<String>) = keyClaimLock.withLock {
|
|
||||||
val request = this.olmMachine.getMissingSessions(roomMembers)
|
|
||||||
// This request can only be a keys claim request.
|
|
||||||
when (request) {
|
|
||||||
is Request.KeysClaim -> {
|
|
||||||
claimKeys(request)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun encrypt(roomId: String, eventType: String, content: Content): Content {
|
|
||||||
return olmMachine.encrypt(roomId, eventType, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun uploadKeys(request: Request.KeysUpload) {
|
|
||||||
try {
|
|
||||||
val response = requestSender.uploadKeys(request)
|
|
||||||
olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_UPLOAD, response)
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).e(throwable, "## CRYPTO uploadKeys(): error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun queryKeys(request: Request.KeysQuery) {
|
|
||||||
try {
|
|
||||||
val response = requestSender.queryKeys(request)
|
|
||||||
olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_QUERY, response)
|
|
||||||
|
|
||||||
// Update the shields!
|
|
||||||
cryptoCoroutineScope.launch {
|
|
||||||
cryptoSessionInfoProvider.getRoomsWhereUsersAreParticipating(request.users).forEach { roomId ->
|
|
||||||
val userGroup = cryptoSessionInfoProvider.getUserListForShieldComputation(roomId)
|
|
||||||
val shield = crossSigningService.shieldForGroup(userGroup)
|
|
||||||
cryptoSessionInfoProvider.updateShieldForRoom(roomId, shield)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).e(throwable, "## CRYPTO doKeyDownloadForUsers(): error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun sendToDevice(request: Request.ToDevice) {
|
private suspend fun sendToDevice(request: Request.ToDevice) {
|
||||||
try {
|
try {
|
||||||
@ -706,34 +609,6 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun claimKeys(request: Request.KeysClaim) {
|
|
||||||
try {
|
|
||||||
val response = requestSender.claimKeys(request)
|
|
||||||
olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_CLAIM, response)
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).e(throwable, "## CRYPTO claimKeys(): error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun signatureUpload(request: Request.SignatureUpload) {
|
|
||||||
try {
|
|
||||||
val response = requestSender.sendSignatureUpload(request)
|
|
||||||
olmMachine.markRequestAsSent(request.requestId, RequestType.SIGNATURE_UPLOAD, response)
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).e(throwable, "## CRYPTO signatureUpload(): error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun sendRoomMessage(request: Request.RoomMessage) {
|
|
||||||
try {
|
|
||||||
Timber.v("SendRoomMessage: $request")
|
|
||||||
val response = requestSender.sendRoomMessage(request)
|
|
||||||
olmMachine.markRequestAsSent(request.requestId, RequestType.ROOM_MESSAGE, response)
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).e(throwable, "## CRYPTO sendRoomMessage(): error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export the crypto keys
|
* Export the crypto keys
|
||||||
*
|
*
|
||||||
@ -966,32 +841,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
cryptoStore.logDbUsageInfo()
|
cryptoStore.logDbUsageInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun prepareToEncrypt(roomId: String) {
|
override suspend fun prepareToEncrypt(roomId: String) = prepareToEncrypt.invoke(roomId, ensureAllMembersAreLoaded = true)
|
||||||
withContext(coroutineDispatchers.crypto) {
|
|
||||||
Timber.tag(loggerTag.value).d("prepareToEncrypt() roomId:$roomId Check room members up to date")
|
|
||||||
// Ensure to load all room members
|
|
||||||
try {
|
|
||||||
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members")
|
|
||||||
throw failure
|
|
||||||
}
|
|
||||||
val userIds = getRoomUserIds(roomId)
|
|
||||||
|
|
||||||
val algorithm = getEncryptionAlgorithm(roomId)
|
|
||||||
|
|
||||||
if (algorithm == null) {
|
|
||||||
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON)
|
|
||||||
Timber.tag(loggerTag.value).e("prepareToEncrypt() : $reason")
|
|
||||||
throw IllegalArgumentException("Missing algorithm")
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
preshareRoomKey(roomId, userIds)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to PreshareRoomKey")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
* For test only
|
* For test only
|
||||||
|
@ -18,11 +18,11 @@ package org.matrix.android.sdk.internal.crypto
|
|||||||
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UnsignedDeviceInfo
|
|
||||||
import org.matrix.android.sdk.internal.crypto.network.RequestSender
|
import org.matrix.android.sdk.internal.crypto.network.RequestSender
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.prepareMethods
|
import org.matrix.android.sdk.internal.crypto.verification.prepareMethods
|
||||||
import uniffi.olm.CryptoStoreException
|
import uniffi.olm.CryptoStoreException
|
||||||
|
@ -1,585 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.MatrixPatterns
|
|
||||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
|
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
|
||||||
import org.matrix.android.sdk.internal.util.logLimit
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
// Legacy name: MXDeviceList
|
|
||||||
@Deprecated("In favor of rust olmMachine")
|
|
||||||
@SessionScope
|
|
||||||
internal class DeviceListManager @Inject constructor(
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val syncTokenStore: SyncTokenStore,
|
|
||||||
private val credentials: Credentials,
|
|
||||||
private val downloadKeysForUsersTask: DownloadKeysForUsersTask,
|
|
||||||
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
|
||||||
coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val taskExecutor: TaskExecutor,
|
|
||||||
private val clock: Clock,
|
|
||||||
) {
|
|
||||||
|
|
||||||
interface UserDevicesUpdateListener {
|
|
||||||
fun onUsersDeviceUpdate(userIds: List<String>)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val deviceChangeListeners = mutableListOf<UserDevicesUpdateListener>()
|
|
||||||
|
|
||||||
fun addListener(listener: UserDevicesUpdateListener) {
|
|
||||||
synchronized(deviceChangeListeners) {
|
|
||||||
deviceChangeListeners.add(listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeListener(listener: UserDevicesUpdateListener) {
|
|
||||||
synchronized(deviceChangeListeners) {
|
|
||||||
deviceChangeListeners.remove(listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun dispatchDeviceChange(users: List<String>) {
|
|
||||||
synchronized(deviceChangeListeners) {
|
|
||||||
deviceChangeListeners.forEach {
|
|
||||||
try {
|
|
||||||
it.onUsersDeviceUpdate(users)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.e(failure, "Failed to dispatch device change")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HS not ready for retry
|
|
||||||
private val notReadyToRetryHS = mutableSetOf<String>()
|
|
||||||
|
|
||||||
private val cryptoCoroutineContext = coroutineDispatchers.crypto
|
|
||||||
|
|
||||||
init {
|
|
||||||
taskExecutor.executorScope.launch(cryptoCoroutineContext) {
|
|
||||||
var isUpdated = false
|
|
||||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
|
||||||
for ((userId, status) in deviceTrackingStatuses) {
|
|
||||||
if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) {
|
|
||||||
// if a download was in progress when we got shut down, it isn't any more.
|
|
||||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
|
||||||
isUpdated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isUpdated) {
|
|
||||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells if the key downloads should be tried
|
|
||||||
*
|
|
||||||
* @param userId the userId
|
|
||||||
* @return true if the keys download can be retrieved
|
|
||||||
*/
|
|
||||||
private fun canRetryKeysDownload(userId: String): Boolean {
|
|
||||||
var res = false
|
|
||||||
|
|
||||||
if (':' in userId) {
|
|
||||||
try {
|
|
||||||
synchronized(notReadyToRetryHS) {
|
|
||||||
res = !notReadyToRetryHS.contains(userId.substringAfter(':'))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the unavailable server lists
|
|
||||||
*/
|
|
||||||
private fun clearUnavailableServersList() {
|
|
||||||
synchronized(notReadyToRetryHS) {
|
|
||||||
notReadyToRetryHS.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onRoomMembersLoadedFor(roomId: String) {
|
|
||||||
taskExecutor.executorScope.launch(cryptoCoroutineContext) {
|
|
||||||
if (cryptoSessionInfoProvider.isRoomEncrypted(roomId)) {
|
|
||||||
// It's OK to track also device for invited users
|
|
||||||
val userIds = cryptoSessionInfoProvider.getRoomUserIds(roomId, true)
|
|
||||||
startTrackingDeviceList(userIds)
|
|
||||||
refreshOutdatedDeviceLists()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark the cached device list for the given user outdated
|
|
||||||
* flag the given user for device-list tracking, if they are not already.
|
|
||||||
*
|
|
||||||
* @param userIds the user ids list
|
|
||||||
*/
|
|
||||||
fun startTrackingDeviceList(userIds: List<String>) {
|
|
||||||
var isUpdated = false
|
|
||||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
|
||||||
|
|
||||||
for (userId in userIds) {
|
|
||||||
if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) {
|
|
||||||
Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId")
|
|
||||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
|
||||||
isUpdated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isUpdated) {
|
|
||||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the devices list statuses
|
|
||||||
*
|
|
||||||
* @param changed the user ids list which have new devices
|
|
||||||
* @param left the user ids list which left a room
|
|
||||||
*/
|
|
||||||
fun handleDeviceListsChanges(changed: Collection<String>, left: Collection<String>) {
|
|
||||||
Timber.v("## CRYPTO: handleDeviceListsChanges changed: ${changed.logLimit()} / left: ${left.logLimit()}")
|
|
||||||
var isUpdated = false
|
|
||||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
|
||||||
|
|
||||||
if (changed.isNotEmpty() || left.isNotEmpty()) {
|
|
||||||
clearUnavailableServersList()
|
|
||||||
}
|
|
||||||
|
|
||||||
for (userId in changed) {
|
|
||||||
if (deviceTrackingStatuses.containsKey(userId)) {
|
|
||||||
Timber.v("## CRYPTO | handleDeviceListsChanges() : Marking device list outdated for $userId")
|
|
||||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
|
||||||
isUpdated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (userId in left) {
|
|
||||||
if (deviceTrackingStatuses.containsKey(userId)) {
|
|
||||||
Timber.v("## CRYPTO | handleDeviceListsChanges() : No longer tracking device list for $userId")
|
|
||||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED
|
|
||||||
isUpdated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isUpdated) {
|
|
||||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will flag each user whose devices we are tracking as in need of an
|
|
||||||
* + update
|
|
||||||
*/
|
|
||||||
fun invalidateAllDeviceLists() {
|
|
||||||
handleDeviceListsChanges(cryptoStore.getDeviceTrackingStatuses().keys, emptyList())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The keys download failed
|
|
||||||
*
|
|
||||||
* @param userIds the user ids list
|
|
||||||
*/
|
|
||||||
private fun onKeysDownloadFailed(userIds: List<String>) {
|
|
||||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
|
||||||
userIds.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_PENDING_DOWNLOAD }
|
|
||||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The keys download succeeded.
|
|
||||||
*
|
|
||||||
* @param userIds the userIds list
|
|
||||||
* @param failures the failure map.
|
|
||||||
*/
|
|
||||||
private fun onKeysDownloadSucceed(userIds: List<String>, failures: Map<String, Map<String, Any>>?): MXUsersDevicesMap<CryptoDeviceInfo> {
|
|
||||||
if (failures != null) {
|
|
||||||
for ((k, value) in failures) {
|
|
||||||
val statusCode = when (val status = value["status"]) {
|
|
||||||
is Double -> status.toInt()
|
|
||||||
is Int -> status.toInt()
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
if (statusCode == 503) {
|
|
||||||
synchronized(notReadyToRetryHS) {
|
|
||||||
notReadyToRetryHS.add(k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
|
||||||
val usersDevicesInfoMap = MXUsersDevicesMap<CryptoDeviceInfo>()
|
|
||||||
for (userId in userIds) {
|
|
||||||
val devices = cryptoStore.getUserDevices(userId)
|
|
||||||
if (null == devices) {
|
|
||||||
if (canRetryKeysDownload(userId)) {
|
|
||||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
|
||||||
Timber.e("failed to retry the devices of $userId : retry later")
|
|
||||||
} else {
|
|
||||||
if (deviceTrackingStatuses.containsKey(userId) && TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses[userId]) {
|
|
||||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_UNREACHABLE_SERVER
|
|
||||||
Timber.e("failed to retry the devices of $userId : the HS is not available")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (deviceTrackingStatuses.containsKey(userId) && TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses[userId]) {
|
|
||||||
// we didn't get any new invalidations since this download started:
|
|
||||||
// this user's device list is now up to date.
|
|
||||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_UP_TO_DATE
|
|
||||||
Timber.v("Device list for $userId now up to date")
|
|
||||||
}
|
|
||||||
// And the response result
|
|
||||||
usersDevicesInfoMap.setObjects(userId, devices)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
|
||||||
|
|
||||||
dispatchDeviceChange(userIds)
|
|
||||||
return usersDevicesInfoMap
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download the device keys for a list of users and stores the keys in the MXStore.
|
|
||||||
* It must be called in getEncryptingThreadHandler() thread.
|
|
||||||
*
|
|
||||||
* @param userIds The users to fetch.
|
|
||||||
* @param forceDownload Always download the keys even if cached.
|
|
||||||
*/
|
|
||||||
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<CryptoDeviceInfo> {
|
|
||||||
Timber.v("## CRYPTO | downloadKeys() : forceDownload $forceDownload : $userIds")
|
|
||||||
// Map from userId -> deviceId -> MXDeviceInfo
|
|
||||||
val stored = MXUsersDevicesMap<CryptoDeviceInfo>()
|
|
||||||
|
|
||||||
// List of user ids we need to download keys for
|
|
||||||
val downloadUsers = ArrayList<String>()
|
|
||||||
if (null != userIds) {
|
|
||||||
if (forceDownload) {
|
|
||||||
downloadUsers.addAll(userIds)
|
|
||||||
} else {
|
|
||||||
for (userId in userIds) {
|
|
||||||
val status = cryptoStore.getDeviceTrackingStatus(userId, TRACKING_STATUS_NOT_TRACKED)
|
|
||||||
// downloading keys ->the keys download won't be triggered twice but the callback requires the dedicated keys
|
|
||||||
// not yet retrieved
|
|
||||||
if (TRACKING_STATUS_UP_TO_DATE != status && TRACKING_STATUS_UNREACHABLE_SERVER != status) {
|
|
||||||
downloadUsers.add(userId)
|
|
||||||
} else {
|
|
||||||
val devices = cryptoStore.getUserDevices(userId)
|
|
||||||
// should always be true
|
|
||||||
if (devices != null) {
|
|
||||||
stored.setObjects(userId, devices)
|
|
||||||
} else {
|
|
||||||
downloadUsers.add(userId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return if (downloadUsers.isEmpty()) {
|
|
||||||
Timber.v("## CRYPTO | downloadKeys() : no new user device")
|
|
||||||
stored
|
|
||||||
} else {
|
|
||||||
Timber.v("## CRYPTO | downloadKeys() : starts")
|
|
||||||
val t0 = clock.epochMillis()
|
|
||||||
try {
|
|
||||||
val result = doKeyDownloadForUsers(downloadUsers)
|
|
||||||
Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms")
|
|
||||||
result.also {
|
|
||||||
it.addEntriesFromMap(stored)
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.w(failure, "## CRYPTO | downloadKeys() : doKeyDownloadForUsers failed after ${clock.epochMillis() - t0} ms")
|
|
||||||
if (forceDownload) {
|
|
||||||
throw failure
|
|
||||||
} else {
|
|
||||||
stored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download the devices keys for a set of users.
|
|
||||||
*
|
|
||||||
* @param downloadUsers the user ids list
|
|
||||||
*/
|
|
||||||
private suspend fun doKeyDownloadForUsers(downloadUsers: List<String>): MXUsersDevicesMap<CryptoDeviceInfo> {
|
|
||||||
Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers ${downloadUsers.logLimit()}")
|
|
||||||
// get the user ids which did not already trigger a keys download
|
|
||||||
val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) }
|
|
||||||
if (filteredUsers.isEmpty()) {
|
|
||||||
// trigger nothing
|
|
||||||
return MXUsersDevicesMap()
|
|
||||||
}
|
|
||||||
val params = DownloadKeysForUsersTask.Params(filteredUsers, syncTokenStore.getLastToken())
|
|
||||||
val response = try {
|
|
||||||
downloadKeysForUsersTask.execute(params)
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
|
|
||||||
if (throwable is CancellationException) {
|
|
||||||
// the crypto module is getting closed, so we cannot access the DB anymore
|
|
||||||
Timber.w("The crypto module is closed, ignoring this error")
|
|
||||||
} else {
|
|
||||||
onKeysDownloadFailed(filteredUsers)
|
|
||||||
}
|
|
||||||
throw throwable
|
|
||||||
}
|
|
||||||
Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
|
|
||||||
for (userId in filteredUsers) {
|
|
||||||
// al devices =
|
|
||||||
val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }
|
|
||||||
|
|
||||||
Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for $userId : $models")
|
|
||||||
if (!models.isNullOrEmpty()) {
|
|
||||||
val workingCopy = models.toMutableMap()
|
|
||||||
for ((deviceId, deviceInfo) in models) {
|
|
||||||
// Get the potential previously store device keys for this device
|
|
||||||
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(userId, deviceId)
|
|
||||||
|
|
||||||
// in some race conditions (like unit tests)
|
|
||||||
// the self device must be seen as verified
|
|
||||||
if (deviceInfo.deviceId == credentials.deviceId && userId == credentials.userId) {
|
|
||||||
deviceInfo.trustLevel = DeviceTrustLevel(previouslyStoredDeviceKeys?.trustLevel?.crossSigningVerified ?: false, true)
|
|
||||||
}
|
|
||||||
// Validate received keys
|
|
||||||
if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) {
|
|
||||||
// New device keys are not valid. Do not store them
|
|
||||||
workingCopy.remove(deviceId)
|
|
||||||
if (null != previouslyStoredDeviceKeys) {
|
|
||||||
// But keep old validated ones if any
|
|
||||||
workingCopy[deviceId] = previouslyStoredDeviceKeys
|
|
||||||
}
|
|
||||||
} else if (null != previouslyStoredDeviceKeys) {
|
|
||||||
// The verified status is not sync'ed with hs.
|
|
||||||
// This is a client side information, valid only for this client.
|
|
||||||
// So, transfer its previous value
|
|
||||||
workingCopy[deviceId]!!.trustLevel = previouslyStoredDeviceKeys.trustLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update the store
|
|
||||||
// Note that devices which aren't in the response will be removed from the stores
|
|
||||||
cryptoStore.storeUserDevices(userId, workingCopy)
|
|
||||||
}
|
|
||||||
|
|
||||||
val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also {
|
|
||||||
Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}")
|
|
||||||
}
|
|
||||||
val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also {
|
|
||||||
Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
|
|
||||||
}
|
|
||||||
val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also {
|
|
||||||
Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
|
|
||||||
}
|
|
||||||
cryptoStore.storeUserCrossSigningKeys(
|
|
||||||
userId,
|
|
||||||
masterKey,
|
|
||||||
selfSigningKey,
|
|
||||||
userSigningKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update devices trust for these users
|
|
||||||
// dispatchDeviceChange(downloadUsers)
|
|
||||||
|
|
||||||
return onKeysDownloadSucceed(filteredUsers, response.failures)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate device keys.
|
|
||||||
* This method must called on getEncryptingThreadHandler() thread.
|
|
||||||
*
|
|
||||||
* @param deviceKeys the device keys to validate.
|
|
||||||
* @param userId the id of the user of the device.
|
|
||||||
* @param deviceId the id of the device.
|
|
||||||
* @param previouslyStoredDeviceKeys the device keys we received before for this device
|
|
||||||
* @return true if succeeds
|
|
||||||
*/
|
|
||||||
private fun validateDeviceKeys(deviceKeys: CryptoDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: CryptoDeviceInfo?): Boolean {
|
|
||||||
if (null == deviceKeys) {
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys is null from $userId:$deviceId")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null == deviceKeys.keys) {
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null == deviceKeys.signatures) {
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the user_id and device_id in the received deviceKeys are correct
|
|
||||||
if (deviceKeys.userId != userId) {
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deviceKeys.deviceId != deviceId) {
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val signKeyId = "ed25519:" + deviceKeys.deviceId
|
|
||||||
val signKey = deviceKeys.keys[signKeyId]
|
|
||||||
|
|
||||||
if (null == signKey) {
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val signatureMap = deviceKeys.signatures[userId]
|
|
||||||
|
|
||||||
if (null == signatureMap) {
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val signature = signatureMap[signKeyId]
|
|
||||||
|
|
||||||
if (null == signature) {
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var isVerified = false
|
|
||||||
var errorMessage: String? = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
olmDevice.verifySignature(signKey, deviceKeys.signalableJSONDictionary(), signature)
|
|
||||||
isVerified = true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
errorMessage = e.message
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isVerified) {
|
|
||||||
Timber.e(
|
|
||||||
"## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":" +
|
|
||||||
deviceKeys.deviceId + " with error " + errorMessage
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null != previouslyStoredDeviceKeys) {
|
|
||||||
if (previouslyStoredDeviceKeys.fingerprint() != signKey) {
|
|
||||||
// This should only happen if the list has been MITMed; we are
|
|
||||||
// best off sticking with the original keys.
|
|
||||||
//
|
|
||||||
// Should we warn the user about it somehow?
|
|
||||||
Timber.e(
|
|
||||||
"## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" +
|
|
||||||
deviceKeys.deviceId + " has changed : " +
|
|
||||||
previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey
|
|
||||||
)
|
|
||||||
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys")
|
|
||||||
Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}")
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start device queries for any users who sent us an m.new_device recently
|
|
||||||
* This method must be called on getEncryptingThreadHandler() thread.
|
|
||||||
*/
|
|
||||||
suspend fun refreshOutdatedDeviceLists() {
|
|
||||||
Timber.v("## CRYPTO | refreshOutdatedDeviceLists()")
|
|
||||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
|
||||||
|
|
||||||
val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId ->
|
|
||||||
TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (users.isEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the statuses
|
|
||||||
users.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_DOWNLOAD_IN_PROGRESS }
|
|
||||||
|
|
||||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
|
||||||
runCatching {
|
|
||||||
doKeyDownloadForUsers(users)
|
|
||||||
}.fold(
|
|
||||||
{
|
|
||||||
Timber.v("## CRYPTO | refreshOutdatedDeviceLists() : done")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Timber.e(it, "## CRYPTO | refreshOutdatedDeviceLists() : ERROR updating device keys for users $users")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* State transition diagram for DeviceList.deviceTrackingStatus
|
|
||||||
* <pre>
|
|
||||||
*
|
|
||||||
* |
|
|
||||||
* stopTrackingDeviceList V
|
|
||||||
* +---------------------> NOT_TRACKED
|
|
||||||
* | |
|
|
||||||
* +<--------------------+ | startTrackingDeviceList
|
|
||||||
* | | V
|
|
||||||
* | +-------------> PENDING_DOWNLOAD <--------------------+-+
|
|
||||||
* | | ^ | | |
|
|
||||||
* | | restart download | | start download | | invalidateUserDeviceList
|
|
||||||
* | | client failed | | | |
|
|
||||||
* | | | V | |
|
|
||||||
* | +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
|
|
||||||
* | | | |
|
|
||||||
* +<-------------------+ | download successful |
|
|
||||||
* ^ V |
|
|
||||||
* +----------------------- UP_TO_DATE ------------------------+
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
|
|
||||||
const val TRACKING_STATUS_NOT_TRACKED = -1
|
|
||||||
const val TRACKING_STATUS_PENDING_DOWNLOAD = 1
|
|
||||||
const val TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2
|
|
||||||
const val TRACKING_STATUS_UP_TO_DATE = 3
|
|
||||||
const val TRACKING_STATUS_UNREACHABLE_SERVER = 4
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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 org.matrix.android.sdk.internal.crypto
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Content
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
import org.matrix.android.sdk.internal.util.time.Clock
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("EncryptEventContentUseCase", LoggerTag.CRYPTO)
|
||||||
|
|
||||||
|
internal class EncryptEventContentUseCase @Inject constructor(olmMachineProvider: OlmMachineProvider,
|
||||||
|
private val prepareToEncrypt: PrepareToEncryptUseCase,
|
||||||
|
private val clock: Clock) {
|
||||||
|
|
||||||
|
private val olmMachine = olmMachineProvider.olmMachine
|
||||||
|
|
||||||
|
suspend operator fun invoke(eventContent: Content,
|
||||||
|
eventType: String,
|
||||||
|
roomId: String): MXEncryptEventContentResult {
|
||||||
|
val t0 = clock.epochMillis()
|
||||||
|
prepareToEncrypt(roomId, ensureAllMembersAreLoaded = false)
|
||||||
|
val content = olmMachine.encrypt(roomId, eventType, eventContent)
|
||||||
|
Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${clock.epochMillis() - t0} ms")
|
||||||
|
return MXEncryptEventContentResult(content, EventType.ENCRYPTED)
|
||||||
|
}
|
||||||
|
}
|
@ -1,230 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.extensions.foldToCallback
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
private const val SEND_TO_DEVICE_RETRY_COUNT = 3
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class EventDecryptor @Inject constructor(
|
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val clock: Clock,
|
|
||||||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
|
||||||
private val messageEncrypter: MessageEncrypter,
|
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
|
||||||
private val deviceListManager: DeviceListManager,
|
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
|
||||||
private val cryptoStore: IMXCryptoStore
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rate limit unwedge attempt, should we persist that?
|
|
||||||
*/
|
|
||||||
private val lastNewSessionForcedDates = mutableMapOf<WedgedDeviceInfo, Long>()
|
|
||||||
|
|
||||||
data class WedgedDeviceInfo(
|
|
||||||
val userId: String,
|
|
||||||
val senderKey: String?
|
|
||||||
)
|
|
||||||
|
|
||||||
private val wedgedDevices = mutableListOf<WedgedDeviceInfo>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)
|
|
||||||
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
|
||||||
return 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
|
|
||||||
*/
|
|
||||||
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
|
|
||||||
// is it needed to do that on the crypto scope??
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
|
||||||
runCatching {
|
|
||||||
internalDecryptEvent(event, timeline)
|
|
||||||
}.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
|
|
||||||
*/
|
|
||||||
@Throws(MXCryptoError::class)
|
|
||||||
private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
|
||||||
val eventContent = event.content
|
|
||||||
if (eventContent == null) {
|
|
||||||
Timber.tag(loggerTag.value).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.tag(loggerTag.value).e("decryptEvent() : $reason")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
return alg.decryptEvent(event, timeline)
|
|
||||||
} catch (mxCryptoError: MXCryptoError) {
|
|
||||||
Timber.tag(loggerTag.value).d("internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
|
|
||||||
if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
|
|
||||||
if (mxCryptoError is MXCryptoError.Base &&
|
|
||||||
mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
|
|
||||||
// need to find sending device
|
|
||||||
val olmContent = event.content.toModel<OlmEventContent>()
|
|
||||||
if (event.senderId != null && olmContent?.senderKey != null) {
|
|
||||||
markOlmSessionForUnwedging(event.senderId, olmContent.senderKey)
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).d("Can't mark as wedge malformed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw mxCryptoError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun markOlmSessionForUnwedging(senderId: String, senderKey: String) {
|
|
||||||
val info = WedgedDeviceInfo(senderId, senderKey)
|
|
||||||
if (!wedgedDevices.contains(info)) {
|
|
||||||
Timber.tag(loggerTag.value).d("Marking device from $senderId key:$senderKey as wedged")
|
|
||||||
wedgedDevices.add(info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// coroutineDispatchers.crypto scope
|
|
||||||
suspend fun unwedgeDevicesIfNeeded() {
|
|
||||||
// handle wedged devices
|
|
||||||
// Some olm decryption have failed and some device are wedged
|
|
||||||
// we should force start a new session for those
|
|
||||||
Timber.tag(loggerTag.value).v("Unwedging: ${wedgedDevices.size} are wedged")
|
|
||||||
// get the one that should be retried according to rate limit
|
|
||||||
val now = clock.epochMillis()
|
|
||||||
val toUnwedge = wedgedDevices.filter {
|
|
||||||
val lastForcedDate = lastNewSessionForcedDates[it] ?: 0
|
|
||||||
if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
|
|
||||||
Timber.tag(loggerTag.value).d("Unwedging, New session for $it already forced with device at $lastForcedDate")
|
|
||||||
return@filter false
|
|
||||||
}
|
|
||||||
// let's already mark that we tried now
|
|
||||||
lastNewSessionForcedDates[it] = now
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toUnwedge.isEmpty()) {
|
|
||||||
Timber.tag(loggerTag.value).v("Nothing to unwedge")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Timber.tag(loggerTag.value).d("Unwedging, trying to create new session for ${toUnwedge.size} devices")
|
|
||||||
|
|
||||||
toUnwedge
|
|
||||||
.chunked(100) // safer to chunk if we ever have lots of wedged devices
|
|
||||||
.forEach { wedgedList ->
|
|
||||||
val groupedByUserId = wedgedList.groupBy { it.userId }
|
|
||||||
// lets download keys if needed
|
|
||||||
withContext(coroutineDispatchers.io) {
|
|
||||||
deviceListManager.downloadKeys(groupedByUserId.keys.toList(), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// find the matching devices
|
|
||||||
groupedByUserId
|
|
||||||
.map { groupedByUser ->
|
|
||||||
val userId = groupedByUser.key
|
|
||||||
val wedgeSenderKeysForUser = groupedByUser.value.map { it.senderKey }
|
|
||||||
val knownDevices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
|
|
||||||
userId to wedgeSenderKeysForUser.mapNotNull { senderKey ->
|
|
||||||
knownDevices.firstOrNull { it.identityKey() == senderKey }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toMap()
|
|
||||||
.let { deviceList ->
|
|
||||||
try {
|
|
||||||
// force creating new outbound session and mark them as most recent to
|
|
||||||
// be used for next encryption (dummy)
|
|
||||||
val sessionToUse = ensureOlmSessionsForDevicesAction.handle(deviceList, true)
|
|
||||||
Timber.tag(loggerTag.value).d("Unwedging, found ${sessionToUse.map.size} to send dummy to")
|
|
||||||
|
|
||||||
// Now send a dummy message on that session so the other side knows about it.
|
|
||||||
val payloadJson = mapOf(
|
|
||||||
"type" to EventType.DUMMY
|
|
||||||
)
|
|
||||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
|
||||||
sessionToUse.map.values
|
|
||||||
.flatMap { it.values }
|
|
||||||
.map { it.deviceInfo }
|
|
||||||
.forEach { deviceInfo ->
|
|
||||||
Timber.tag(loggerTag.value).v("encrypting dummy to ${deviceInfo.deviceId}")
|
|
||||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
|
||||||
sendToDeviceMap.setObject(deviceInfo.userId, deviceInfo.deviceId, encodedPayload)
|
|
||||||
}
|
|
||||||
|
|
||||||
// now let's send that
|
|
||||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
|
||||||
withContext(coroutineDispatchers.io) {
|
|
||||||
sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT)
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
deviceList.flatMap { it.value }.joinToString { it.shortDebugString() }.let {
|
|
||||||
Timber.tag(loggerTag.value).e(failure, "## Failed to unwedge devices: $it}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -14,19 +14,14 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms.olm
|
package org.matrix.android.sdk.internal.crypto
|
||||||
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class MXOlmDecryptionFactory @Inject constructor(private val olmDevice: MXOlmDevice,
|
internal class GetRoomUserIdsUseCase @Inject constructor(private val shouldEncryptForInvitedMembers: ShouldEncryptForInvitedMembersUseCase,
|
||||||
@UserId private val userId: String) {
|
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider) {
|
||||||
|
|
||||||
fun create(): MXOlmDecryption {
|
operator fun invoke(roomId: String): List<String> {
|
||||||
return MXOlmDecryption(
|
return cryptoSessionInfoProvider.getRoomUserIds(roomId, shouldEncryptForInvitedMembers(roomId))
|
||||||
olmDevice,
|
|
||||||
userId
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,463 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.system.measureTimeMillis
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("IncomingKeyRequestManager", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class IncomingKeyRequestManager @Inject constructor(
|
|
||||||
private val credentials: Credentials,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val cryptoConfig: MXCryptoConfig,
|
|
||||||
private val messageEncrypter: MessageEncrypter,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
|
||||||
private val clock: Clock,
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
|
||||||
private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher)
|
|
||||||
val sequencer = SemaphoreCoroutineSequencer()
|
|
||||||
|
|
||||||
private val incomingRequestBuffer = mutableListOf<ValidMegolmRequestBody>()
|
|
||||||
|
|
||||||
// the listeners
|
|
||||||
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet()
|
|
||||||
|
|
||||||
enum class MegolmRequestAction {
|
|
||||||
Request, Cancel
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ValidMegolmRequestBody(
|
|
||||||
val requestId: String,
|
|
||||||
val requestingUserId: String,
|
|
||||||
val requestingDeviceId: String,
|
|
||||||
val roomId: String,
|
|
||||||
val senderKey: String,
|
|
||||||
val sessionId: String,
|
|
||||||
val action: MegolmRequestAction
|
|
||||||
) {
|
|
||||||
fun shortDbgString() = "Request from $requestingUserId|$requestingDeviceId for session $sessionId in room $roomId"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun RoomKeyShareRequest.toValidMegolmRequest(senderId: String): ValidMegolmRequestBody? {
|
|
||||||
val deviceId = requestingDeviceId ?: return null
|
|
||||||
val body = body ?: return null
|
|
||||||
val roomId = body.roomId ?: return null
|
|
||||||
val sessionId = body.sessionId ?: return null
|
|
||||||
val senderKey = body.senderKey ?: return null
|
|
||||||
val requestId = this.requestId ?: return null
|
|
||||||
if (body.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null
|
|
||||||
val action = when (this.action) {
|
|
||||||
"request" -> MegolmRequestAction.Request
|
|
||||||
"request_cancellation" -> MegolmRequestAction.Cancel
|
|
||||||
else -> null
|
|
||||||
} ?: return null
|
|
||||||
return ValidMegolmRequestBody(
|
|
||||||
requestId = requestId,
|
|
||||||
requestingUserId = senderId,
|
|
||||||
requestingDeviceId = deviceId,
|
|
||||||
roomId = roomId,
|
|
||||||
senderKey = senderKey,
|
|
||||||
sessionId = sessionId,
|
|
||||||
action = action
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addNewIncomingRequest(senderId: String, request: RoomKeyShareRequest) {
|
|
||||||
if (!cryptoStore.isKeyGossipingEnabled()) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.i("Ignore incoming key request as per crypto config in room ${request.body?.roomId}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
outgoingRequestScope.launch {
|
|
||||||
// It is important to handle requests in order
|
|
||||||
sequencer.post {
|
|
||||||
val validMegolmRequest = request.toValidMegolmRequest(senderId) ?: return@post Unit.also {
|
|
||||||
Timber.tag(loggerTag.value).w("Received key request for unknown algorithm ${request.body?.algorithm}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// is there already one like that?
|
|
||||||
val existing = incomingRequestBuffer.firstOrNull { it == validMegolmRequest }
|
|
||||||
if (existing == null) {
|
|
||||||
when (validMegolmRequest.action) {
|
|
||||||
MegolmRequestAction.Request -> {
|
|
||||||
// just add to the buffer
|
|
||||||
incomingRequestBuffer.add(validMegolmRequest)
|
|
||||||
}
|
|
||||||
MegolmRequestAction.Cancel -> {
|
|
||||||
// ignore, we can't cancel as it's not known (probably already processed)
|
|
||||||
// still notify app layer if it was passed up previously
|
|
||||||
IncomingRoomKeyRequest.fromRestRequest(senderId, request, clock)?.let { iReq ->
|
|
||||||
outgoingRequestScope.launch(coroutineDispatchers.computation) {
|
|
||||||
val listenersCopy = synchronized(gossipingRequestListeners) {
|
|
||||||
gossipingRequestListeners.toList()
|
|
||||||
}
|
|
||||||
listenersCopy.onEach {
|
|
||||||
tryOrNull {
|
|
||||||
withContext(coroutineDispatchers.main) {
|
|
||||||
it.onRequestCancelled(iReq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
when (validMegolmRequest.action) {
|
|
||||||
MegolmRequestAction.Request -> {
|
|
||||||
// it's already in buffer, nop keep existing
|
|
||||||
}
|
|
||||||
MegolmRequestAction.Cancel -> {
|
|
||||||
// discard the request in buffer
|
|
||||||
incomingRequestBuffer.remove(existing)
|
|
||||||
outgoingRequestScope.launch(coroutineDispatchers.computation) {
|
|
||||||
val listenersCopy = synchronized(gossipingRequestListeners) {
|
|
||||||
gossipingRequestListeners.toList()
|
|
||||||
}
|
|
||||||
listenersCopy.onEach {
|
|
||||||
IncomingRoomKeyRequest.fromRestRequest(senderId, request, clock)?.let { iReq ->
|
|
||||||
withContext(coroutineDispatchers.main) {
|
|
||||||
tryOrNull { it.onRequestCancelled(iReq) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun processIncomingRequests() {
|
|
||||||
outgoingRequestScope.launch {
|
|
||||||
sequencer.post {
|
|
||||||
measureTimeMillis {
|
|
||||||
Timber.tag(loggerTag.value).v("processIncomingKeyRequests : ${incomingRequestBuffer.size} request to process")
|
|
||||||
incomingRequestBuffer.forEach {
|
|
||||||
// should not happen, we only store requests
|
|
||||||
if (it.action != MegolmRequestAction.Request) return@forEach
|
|
||||||
try {
|
|
||||||
handleIncomingRequest(it)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
// ignore and continue, should not happen
|
|
||||||
Timber.tag(loggerTag.value).w(failure, "processIncomingKeyRequests : failed to process request $it")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
incomingRequestBuffer.clear()
|
|
||||||
}.let { duration ->
|
|
||||||
Timber.tag(loggerTag.value).v("Finish processing incoming key request in $duration ms")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun handleIncomingRequest(request: ValidMegolmRequestBody) {
|
|
||||||
// We don't want to download keys, if we don't know the device yet we won't share any how?
|
|
||||||
val requestingDevice =
|
|
||||||
cryptoStore.getUserDevice(request.requestingUserId, request.requestingDeviceId)
|
|
||||||
?: return Unit.also {
|
|
||||||
Timber.tag(loggerTag.value).d("Ignoring key request: ${request.shortDbgString()}")
|
|
||||||
}
|
|
||||||
|
|
||||||
cryptoStore.saveIncomingKeyRequestAuditTrail(
|
|
||||||
request.requestId,
|
|
||||||
request.roomId,
|
|
||||||
request.sessionId,
|
|
||||||
request.senderKey,
|
|
||||||
MXCRYPTO_ALGORITHM_MEGOLM,
|
|
||||||
request.requestingUserId,
|
|
||||||
request.requestingDeviceId
|
|
||||||
)
|
|
||||||
|
|
||||||
val roomAlgorithm = // withContext(coroutineDispatchers.crypto) {
|
|
||||||
cryptoStore.getRoomAlgorithm(request.roomId)
|
|
||||||
// }
|
|
||||||
if (roomAlgorithm != MXCRYPTO_ALGORITHM_MEGOLM) {
|
|
||||||
// strange we received a request for a room that is not encrypted
|
|
||||||
// maybe a broken state?
|
|
||||||
Timber.tag(loggerTag.value).w("Received a key request in a room with unsupported alg:$roomAlgorithm , req:${request.shortDbgString()}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is it for one of our sessions?
|
|
||||||
if (request.requestingUserId == credentials.userId) {
|
|
||||||
Timber.tag(loggerTag.value).v("handling request from own user: megolm session ${request.sessionId}")
|
|
||||||
|
|
||||||
if (request.requestingDeviceId == credentials.deviceId) {
|
|
||||||
// ignore it's a remote echo
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// If it's verified we share from the early index we know
|
|
||||||
// if not we check if it was originaly shared or not
|
|
||||||
if (requestingDevice.isVerified) {
|
|
||||||
// we share from the earliest known chain index
|
|
||||||
shareMegolmKey(request, requestingDevice, null)
|
|
||||||
} else {
|
|
||||||
shareIfItWasPreviouslyShared(request, requestingDevice)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (cryptoConfig.limitRoomKeyRequestsToMyDevices) {
|
|
||||||
Timber.tag(loggerTag.value).v("Ignore request from other user as per crypto config: ${request.shortDbgString()}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Timber.tag(loggerTag.value).v("handling request from other user: megolm session ${request.sessionId}")
|
|
||||||
if (requestingDevice.isBlocked) {
|
|
||||||
// it's blocked, so send a withheld code
|
|
||||||
sendWithheldForRequest(request, WithHeldCode.BLACKLISTED)
|
|
||||||
} else {
|
|
||||||
shareIfItWasPreviouslyShared(request, requestingDevice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun shareIfItWasPreviouslyShared(request: ValidMegolmRequestBody, requestingDevice: CryptoDeviceInfo) {
|
|
||||||
// we don't reshare unless it was previously shared with
|
|
||||||
val wasSessionSharedWithUser = withContext(coroutineDispatchers.crypto) {
|
|
||||||
cryptoStore.getSharedSessionInfo(request.roomId, request.sessionId, requestingDevice)
|
|
||||||
}
|
|
||||||
if (wasSessionSharedWithUser.found && wasSessionSharedWithUser.chainIndex != null) {
|
|
||||||
// we share from the index it was previously shared with
|
|
||||||
shareMegolmKey(request, requestingDevice, wasSessionSharedWithUser.chainIndex.toLong())
|
|
||||||
} else {
|
|
||||||
val isOwnDevice = requestingDevice.userId == credentials.userId
|
|
||||||
sendWithheldForRequest(request, if (isOwnDevice) WithHeldCode.UNVERIFIED else WithHeldCode.UNAUTHORISED)
|
|
||||||
// if it's our device we could delegate to the app layer to decide
|
|
||||||
if (isOwnDevice) {
|
|
||||||
outgoingRequestScope.launch(coroutineDispatchers.computation) {
|
|
||||||
val listenersCopy = synchronized(gossipingRequestListeners) {
|
|
||||||
gossipingRequestListeners.toList()
|
|
||||||
}
|
|
||||||
val iReq = IncomingRoomKeyRequest(
|
|
||||||
userId = requestingDevice.userId,
|
|
||||||
deviceId = requestingDevice.deviceId,
|
|
||||||
requestId = request.requestId,
|
|
||||||
requestBody = RoomKeyRequestBody(
|
|
||||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
|
||||||
senderKey = request.senderKey,
|
|
||||||
sessionId = request.sessionId,
|
|
||||||
roomId = request.roomId
|
|
||||||
),
|
|
||||||
localCreationTimestamp = clock.epochMillis()
|
|
||||||
)
|
|
||||||
listenersCopy.onEach {
|
|
||||||
withContext(coroutineDispatchers.main) {
|
|
||||||
tryOrNull { it.onRoomKeyRequest(iReq) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun sendWithheldForRequest(request: ValidMegolmRequestBody, code: WithHeldCode) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.w("Send withheld $code for req: ${request.shortDbgString()}")
|
|
||||||
val withHeldContent = RoomKeyWithHeldContent(
|
|
||||||
roomId = request.roomId,
|
|
||||||
senderKey = request.senderKey,
|
|
||||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
|
||||||
sessionId = request.sessionId,
|
|
||||||
codeString = code.value,
|
|
||||||
fromDevice = credentials.deviceId
|
|
||||||
)
|
|
||||||
|
|
||||||
val params = SendToDeviceTask.Params(
|
|
||||||
EventType.ROOM_KEY_WITHHELD,
|
|
||||||
MXUsersDevicesMap<Any>().apply {
|
|
||||||
setObject(request.requestingUserId, request.requestingDeviceId, withHeldContent)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
withContext(coroutineDispatchers.io) {
|
|
||||||
sendToDeviceTask.execute(params)
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.d("Send withheld $code req: ${request.shortDbgString()}")
|
|
||||||
}
|
|
||||||
|
|
||||||
cryptoStore.saveWithheldAuditTrail(
|
|
||||||
roomId = request.roomId,
|
|
||||||
sessionId = request.sessionId,
|
|
||||||
senderKey = request.senderKey,
|
|
||||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
|
||||||
code = code,
|
|
||||||
userId = request.requestingUserId,
|
|
||||||
deviceId = request.requestingDeviceId
|
|
||||||
)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
// Ignore it's not that important?
|
|
||||||
// do we want to fallback to a worker?
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.w("Failed to send withheld $code req: ${request.shortDbgString()} reason:${failure.localizedMessage}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest) {
|
|
||||||
request.requestId ?: return
|
|
||||||
request.deviceId ?: return
|
|
||||||
request.userId ?: return
|
|
||||||
request.requestBody?.roomId ?: return
|
|
||||||
request.requestBody.senderKey ?: return
|
|
||||||
request.requestBody.sessionId ?: return
|
|
||||||
val validReq = ValidMegolmRequestBody(
|
|
||||||
requestId = request.requestId,
|
|
||||||
requestingDeviceId = request.deviceId,
|
|
||||||
requestingUserId = request.userId,
|
|
||||||
roomId = request.requestBody.roomId,
|
|
||||||
senderKey = request.requestBody.senderKey,
|
|
||||||
sessionId = request.requestBody.sessionId,
|
|
||||||
action = MegolmRequestAction.Request
|
|
||||||
)
|
|
||||||
val requestingDevice =
|
|
||||||
cryptoStore.getUserDevice(request.userId, request.deviceId)
|
|
||||||
?: return Unit.also {
|
|
||||||
Timber.tag(loggerTag.value).d("Ignoring key request: ${validReq.shortDbgString()}")
|
|
||||||
}
|
|
||||||
|
|
||||||
shareMegolmKey(validReq, requestingDevice, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun shareMegolmKey(validRequest: ValidMegolmRequestBody,
|
|
||||||
requestingDevice: CryptoDeviceInfo,
|
|
||||||
chainIndex: Long?): Boolean {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.d("try to re-share Megolm Key at index $chainIndex for ${validRequest.shortDbgString()}")
|
|
||||||
|
|
||||||
val devicesByUser = mapOf(validRequest.requestingUserId to listOf(requestingDevice))
|
|
||||||
val usersDeviceMap = try {
|
|
||||||
ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.w("Failed to establish olm session")
|
|
||||||
sendWithheldForRequest(validRequest, WithHeldCode.NO_OLM)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val olmSessionResult = usersDeviceMap.getObject(requestingDevice.userId, requestingDevice.deviceId)
|
|
||||||
if (olmSessionResult?.sessionId == null) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.w("reshareKey: no session with this device, probably because there were no one-time keys")
|
|
||||||
sendWithheldForRequest(validRequest, WithHeldCode.NO_OLM)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val sessionHolder = try {
|
|
||||||
olmDevice.getInboundGroupSession(validRequest.sessionId, validRequest.senderKey, validRequest.roomId)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.e(failure, "shareKeysWithDevice: failed to get session ${validRequest.requestingUserId}")
|
|
||||||
// It's unavailable
|
|
||||||
sendWithheldForRequest(validRequest, WithHeldCode.UNAVAILABLE)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val export = sessionHolder.mutex.withLock {
|
|
||||||
sessionHolder.wrapper.exportKeys(chainIndex)
|
|
||||||
} ?: return false.also {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.e("shareKeysWithDevice: failed to export group session ${validRequest.sessionId}")
|
|
||||||
}
|
|
||||||
|
|
||||||
val payloadJson = mapOf(
|
|
||||||
"type" to EventType.FORWARDED_ROOM_KEY,
|
|
||||||
"content" to export
|
|
||||||
)
|
|
||||||
|
|
||||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(requestingDevice))
|
|
||||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
|
||||||
sendToDeviceMap.setObject(requestingDevice.userId, requestingDevice.deviceId, encodedPayload)
|
|
||||||
Timber.tag(loggerTag.value).d("reshareKey() : try sending session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}")
|
|
||||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
|
||||||
return try {
|
|
||||||
sendToDeviceTask.execute(sendToDeviceParams)
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.i("successfully re-shared session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}")
|
|
||||||
cryptoStore.saveForwardKeyAuditTrail(
|
|
||||||
validRequest.roomId,
|
|
||||||
validRequest.sessionId,
|
|
||||||
validRequest.senderKey,
|
|
||||||
MXCRYPTO_ALGORITHM_MEGOLM,
|
|
||||||
requestingDevice.userId,
|
|
||||||
requestingDevice.deviceId,
|
|
||||||
chainIndex
|
|
||||||
)
|
|
||||||
true
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.e(failure, "fail to re-share session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
|
|
||||||
synchronized(gossipingRequestListeners) {
|
|
||||||
gossipingRequestListeners.add(listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
|
|
||||||
synchronized(gossipingRequestListeners) {
|
|
||||||
gossipingRequestListeners.remove(listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun close() {
|
|
||||||
try {
|
|
||||||
outgoingRequestScope.cancel("User Terminate")
|
|
||||||
incomingRequestBuffer.clear()
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).w("Failed to shutDown request manager")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,922 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
|
||||||
import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
|
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXOutboundSessionInfo
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.SharedWithHelper
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
|
||||||
import org.matrix.android.sdk.internal.util.convertFromUTF8
|
|
||||||
import org.matrix.android.sdk.internal.util.convertToUTF8
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import org.matrix.olm.OlmAccount
|
|
||||||
import org.matrix.olm.OlmException
|
|
||||||
import org.matrix.olm.OlmMessage
|
|
||||||
import org.matrix.olm.OlmOutboundGroupSession
|
|
||||||
import org.matrix.olm.OlmSession
|
|
||||||
import org.matrix.olm.OlmUtility
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
// The libolm wrapper.
|
|
||||||
@Deprecated("rust")
|
|
||||||
@SessionScope
|
|
||||||
internal class MXOlmDevice @Inject constructor(
|
|
||||||
/**
|
|
||||||
* The store where crypto data is saved.
|
|
||||||
*/
|
|
||||||
private val store: IMXCryptoStore,
|
|
||||||
private val olmSessionStore: OlmSessionStore,
|
|
||||||
private val inboundGroupSessionStore: InboundGroupSessionStore,
|
|
||||||
private val clock: Clock,
|
|
||||||
) {
|
|
||||||
|
|
||||||
val mutex = Mutex()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the Curve25519 key for the account.
|
|
||||||
*/
|
|
||||||
var deviceCurve25519Key: String? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the Ed25519 key for the account.
|
|
||||||
*/
|
|
||||||
var deviceEd25519Key: String? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
// The OLM lib utility instance.
|
|
||||||
private var olmUtility: OlmUtility? = null
|
|
||||||
|
|
||||||
private data class GroupSessionCacheItem(
|
|
||||||
val groupId: String,
|
|
||||||
val groupSession: OlmOutboundGroupSession
|
|
||||||
)
|
|
||||||
|
|
||||||
// The outbound group session.
|
|
||||||
// Caches active outbound session to avoid to sync with DB before read
|
|
||||||
// The key is the session id, the value the <roomID,outbound group session>.
|
|
||||||
private val outboundGroupSessionCache: MutableMap<String, GroupSessionCacheItem> = HashMap()
|
|
||||||
|
|
||||||
// Store a set of decrypted message indexes for each group session.
|
|
||||||
// This partially mitigates a replay attack where a MITM resends a group
|
|
||||||
// message into the room.
|
|
||||||
//
|
|
||||||
// The Matrix SDK exposes events through MXEventTimelines. A developer can open several
|
|
||||||
// timelines from a same room so that a message can be decrypted several times but from
|
|
||||||
// a different timeline.
|
|
||||||
// So, store these message indexes per timeline id.
|
|
||||||
//
|
|
||||||
// The first level keys are timeline ids.
|
|
||||||
// The second level keys are strings of form "<senderKey>|<session_id>|<message_index>"
|
|
||||||
private val inboundGroupSessionMessageIndexes: MutableMap<String, MutableSet<String>> = HashMap()
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Retrieve the account from the store
|
|
||||||
try {
|
|
||||||
store.getOrCreateOlmAccount()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "MXOlmDevice : cannot initialize olmAccount")
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
olmUtility = OlmUtility()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : OlmUtility failed with error")
|
|
||||||
olmUtility = null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
deviceCurve25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error")
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
deviceEd25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The current (unused, unpublished) one-time keys for this account.
|
|
||||||
*/
|
|
||||||
fun getOneTimeKeys(): Map<String, Map<String, String>>? {
|
|
||||||
try {
|
|
||||||
return store.doWithOlmAccount { it.oneTimeKeys() }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## getOneTimeKeys() : failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The maximum number of one-time keys the olm account can store.
|
|
||||||
*/
|
|
||||||
fun getMaxNumberOfOneTimeKeys(): Long {
|
|
||||||
return store.doWithOlmAccount { it.maxOneTimeKeys() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an unpublished fallback key
|
|
||||||
* A call to markKeysAsPublished will mark it as published and this
|
|
||||||
* call will return null (until a call to generateFallbackKey is made)
|
|
||||||
*/
|
|
||||||
fun getFallbackKey(): MutableMap<String, MutableMap<String, String>>? {
|
|
||||||
try {
|
|
||||||
return store.doWithOlmAccount { it.fallbackKey() }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e("## getFallbackKey() : failed")
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a new fallback key if there is not already
|
|
||||||
* an unpublished one.
|
|
||||||
* @return true if a new key was generated
|
|
||||||
*/
|
|
||||||
fun generateFallbackKeyIfNeeded(): Boolean {
|
|
||||||
try {
|
|
||||||
if (!hasUnpublishedFallbackKey()) {
|
|
||||||
store.doWithOlmAccount {
|
|
||||||
it.generateFallbackKey()
|
|
||||||
store.saveOlmAccount()
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e("## generateFallbackKey() : failed")
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun hasUnpublishedFallbackKey(): Boolean {
|
|
||||||
return getFallbackKey()?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty().isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun forgetFallbackKey() {
|
|
||||||
try {
|
|
||||||
store.doWithOlmAccount {
|
|
||||||
it.forgetFallbackKey()
|
|
||||||
store.saveOlmAccount()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e("## forgetFallbackKey() : failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Release the instance
|
|
||||||
*/
|
|
||||||
fun release() {
|
|
||||||
olmUtility?.releaseUtility()
|
|
||||||
outboundGroupSessionCache.values.forEach {
|
|
||||||
it.groupSession.releaseSession()
|
|
||||||
}
|
|
||||||
outboundGroupSessionCache.clear()
|
|
||||||
inboundGroupSessionStore.clear()
|
|
||||||
olmSessionStore.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signs a message with the ed25519 key for this account.
|
|
||||||
*
|
|
||||||
* @param message the message to be signed.
|
|
||||||
* @return the base64-encoded signature.
|
|
||||||
*/
|
|
||||||
fun signMessage(message: String): String? {
|
|
||||||
try {
|
|
||||||
return store.doWithOlmAccount { it.signMessage(message) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## signMessage() : failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks all of the one-time keys as published.
|
|
||||||
*/
|
|
||||||
fun markKeysAsPublished() {
|
|
||||||
try {
|
|
||||||
store.doWithOlmAccount {
|
|
||||||
it.markOneTimeKeysAsPublished()
|
|
||||||
store.saveOlmAccount()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## markKeysAsPublished() : failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate some new one-time keys
|
|
||||||
*
|
|
||||||
* @param numKeys number of keys to generate
|
|
||||||
*/
|
|
||||||
fun generateOneTimeKeys(numKeys: Int) {
|
|
||||||
try {
|
|
||||||
store.doWithOlmAccount {
|
|
||||||
it.generateOneTimeKeys(numKeys)
|
|
||||||
store.saveOlmAccount()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## generateOneTimeKeys() : failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a new outbound session.
|
|
||||||
* The new session will be stored in the MXStore.
|
|
||||||
*
|
|
||||||
* @param theirIdentityKey the remote user's Curve25519 identity key
|
|
||||||
* @param theirOneTimeKey the remote user's one-time Curve25519 key
|
|
||||||
* @return the session id for the outbound session.
|
|
||||||
*/
|
|
||||||
fun createOutboundSession(theirIdentityKey: String, theirOneTimeKey: String): String? {
|
|
||||||
Timber.tag(loggerTag.value).d("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey")
|
|
||||||
var olmSession: OlmSession? = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
olmSession = OlmSession()
|
|
||||||
store.doWithOlmAccount { olmAccount ->
|
|
||||||
olmSession.initOutboundSession(olmAccount, theirIdentityKey, theirOneTimeKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
val olmSessionWrapper = OlmSessionWrapper(olmSession, 0)
|
|
||||||
|
|
||||||
// Pretend we've received a message at this point, otherwise
|
|
||||||
// if we try to send a message to the device, it won't use
|
|
||||||
// this session
|
|
||||||
olmSessionWrapper.onMessageReceived(clock.epochMillis())
|
|
||||||
|
|
||||||
olmSessionStore.storeSession(olmSessionWrapper, theirIdentityKey)
|
|
||||||
|
|
||||||
val sessionIdentifier = olmSession.sessionIdentifier()
|
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).v("## createOutboundSession() ; olmSession.sessionIdentifier: $sessionIdentifier")
|
|
||||||
return sessionIdentifier
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## createOutboundSession() failed")
|
|
||||||
|
|
||||||
olmSession?.releaseSession()
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a new inbound session, given an incoming message.
|
|
||||||
*
|
|
||||||
* @param theirDeviceIdentityKey the remote user's Curve25519 identity key.
|
|
||||||
* @param messageType the message_type field from the received message (must be 0).
|
|
||||||
* @param ciphertext base64-encoded body from the received message.
|
|
||||||
* @return {{payload: string, session_id: string}} decrypted payload, and session id of new session.
|
|
||||||
*/
|
|
||||||
fun createInboundSession(theirDeviceIdentityKey: String, messageType: Int, ciphertext: String): Map<String, String>? {
|
|
||||||
Timber.tag(loggerTag.value).d("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey")
|
|
||||||
|
|
||||||
var olmSession: OlmSession? = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
try {
|
|
||||||
olmSession = OlmSession()
|
|
||||||
store.doWithOlmAccount { olmAccount ->
|
|
||||||
olmSession.initInboundSessionFrom(olmAccount, theirDeviceIdentityKey, ciphertext)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## createInboundSession() : the session creation failed")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}")
|
|
||||||
|
|
||||||
try {
|
|
||||||
store.doWithOlmAccount { olmAccount ->
|
|
||||||
olmAccount.removeOneTimeKeys(olmSession)
|
|
||||||
store.saveOlmAccount()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## createInboundSession() : removeOneTimeKeys failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
val olmMessage = OlmMessage()
|
|
||||||
olmMessage.mCipherText = ciphertext
|
|
||||||
olmMessage.mType = messageType.toLong()
|
|
||||||
|
|
||||||
var payloadString: String? = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
payloadString = olmSession.decryptMessage(olmMessage)
|
|
||||||
|
|
||||||
val olmSessionWrapper = OlmSessionWrapper(olmSession, 0)
|
|
||||||
// This counts as a received message: set last received message time to now
|
|
||||||
olmSessionWrapper.onMessageReceived(clock.epochMillis())
|
|
||||||
|
|
||||||
olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## createInboundSession() : decryptMessage failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
val res = HashMap<String, String>()
|
|
||||||
|
|
||||||
if (!payloadString.isNullOrEmpty()) {
|
|
||||||
res["payload"] = payloadString
|
|
||||||
}
|
|
||||||
|
|
||||||
val sessionIdentifier = olmSession.sessionIdentifier()
|
|
||||||
|
|
||||||
if (!sessionIdentifier.isNullOrEmpty()) {
|
|
||||||
res["session_id"] = sessionIdentifier
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## createInboundSession() : OlmSession creation failed")
|
|
||||||
|
|
||||||
olmSession?.releaseSession()
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a list of known session IDs for the given device.
|
|
||||||
*
|
|
||||||
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
|
|
||||||
* @return a list of known session ids for the device.
|
|
||||||
*/
|
|
||||||
fun getSessionIds(theirDeviceIdentityKey: String): List<String> {
|
|
||||||
return olmSessionStore.getDeviceSessionIds(theirDeviceIdentityKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the right olm session id for encrypting messages to the given identity key.
|
|
||||||
*
|
|
||||||
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
|
|
||||||
* @return the session id, or null if no established session.
|
|
||||||
*/
|
|
||||||
fun getSessionId(theirDeviceIdentityKey: String): String? {
|
|
||||||
return olmSessionStore.getLastUsedSessionId(theirDeviceIdentityKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encrypt an outgoing message using an existing session.
|
|
||||||
*
|
|
||||||
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
|
|
||||||
* @param sessionId the id of the active session
|
|
||||||
* @param payloadString the payload to be encrypted and sent
|
|
||||||
* @return the cipher text
|
|
||||||
*/
|
|
||||||
suspend fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map<String, Any>? {
|
|
||||||
val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId)
|
|
||||||
|
|
||||||
if (olmSessionWrapper != null) {
|
|
||||||
try {
|
|
||||||
Timber.tag(loggerTag.value).v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId")
|
|
||||||
|
|
||||||
val olmMessage = olmSessionWrapper.mutex.withLock {
|
|
||||||
olmSessionWrapper.olmSession.encryptMessage(payloadString)
|
|
||||||
}
|
|
||||||
return mapOf(
|
|
||||||
"body" to olmMessage.mCipherText,
|
|
||||||
"type" to olmMessage.mType,
|
|
||||||
).also {
|
|
||||||
olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## encryptMessage() : failed to encrypt olm with device|session:$theirDeviceIdentityKey|$sessionId")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).e("## encryptMessage() : Failed to encrypt unknown session $sessionId")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrypt an incoming message using an existing session.
|
|
||||||
*
|
|
||||||
* @param ciphertext the base64-encoded body from the received message.
|
|
||||||
* @param messageType message_type field from the received message.
|
|
||||||
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
|
|
||||||
* @param sessionId the id of the active session.
|
|
||||||
* @return the decrypted payload.
|
|
||||||
*/
|
|
||||||
@kotlin.jvm.Throws
|
|
||||||
suspend fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? {
|
|
||||||
var payloadString: String? = null
|
|
||||||
|
|
||||||
val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId)
|
|
||||||
|
|
||||||
if (null != olmSessionWrapper) {
|
|
||||||
val olmMessage = OlmMessage()
|
|
||||||
olmMessage.mCipherText = ciphertext
|
|
||||||
olmMessage.mType = messageType.toLong()
|
|
||||||
|
|
||||||
payloadString =
|
|
||||||
olmSessionWrapper.mutex.withLock {
|
|
||||||
olmSessionWrapper.olmSession.decryptMessage(olmMessage).also {
|
|
||||||
olmSessionWrapper.onMessageReceived(clock.epochMillis())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
return payloadString
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if an incoming messages is a prekey message matching an existing session.
|
|
||||||
*
|
|
||||||
* @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
|
|
||||||
* @param sessionId the id of the active session.
|
|
||||||
* @param messageType message_type field from the received message.
|
|
||||||
* @param ciphertext the base64-encoded body from the received message.
|
|
||||||
* @return YES if the received message is a prekey message which matchesthe given session.
|
|
||||||
*/
|
|
||||||
fun matchesSession(theirDeviceIdentityKey: String, sessionId: String, messageType: Int, ciphertext: String): Boolean {
|
|
||||||
if (messageType != 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId)
|
|
||||||
return null != olmSessionWrapper && olmSessionWrapper.olmSession.matchesInboundSession(ciphertext)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Outbound group session
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a new outbound group session.
|
|
||||||
*
|
|
||||||
* @return the session id for the outbound session.
|
|
||||||
*/
|
|
||||||
fun createOutboundGroupSessionForRoom(roomId: String): String? {
|
|
||||||
var session: OlmOutboundGroupSession? = null
|
|
||||||
try {
|
|
||||||
session = OlmOutboundGroupSession()
|
|
||||||
outboundGroupSessionCache[session.sessionIdentifier()] = GroupSessionCacheItem(roomId, session)
|
|
||||||
store.storeCurrentOutboundGroupSessionForRoom(roomId, session)
|
|
||||||
return session.sessionIdentifier()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "createOutboundGroupSession")
|
|
||||||
|
|
||||||
session?.releaseSession()
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun storeOutboundGroupSessionForRoom(roomId: String, sessionId: String) {
|
|
||||||
outboundGroupSessionCache[sessionId]?.let {
|
|
||||||
store.storeCurrentOutboundGroupSessionForRoom(roomId, it.groupSession)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun restoreOutboundGroupSessionForRoom(roomId: String): MXOutboundSessionInfo? {
|
|
||||||
val restoredOutboundGroupSession = store.getCurrentOutboundGroupSessionForRoom(roomId)
|
|
||||||
if (restoredOutboundGroupSession != null) {
|
|
||||||
val sessionId = restoredOutboundGroupSession.outboundGroupSession.sessionIdentifier()
|
|
||||||
// cache it
|
|
||||||
outboundGroupSessionCache[sessionId] = GroupSessionCacheItem(roomId, restoredOutboundGroupSession.outboundGroupSession)
|
|
||||||
|
|
||||||
return MXOutboundSessionInfo(
|
|
||||||
sessionId = sessionId,
|
|
||||||
sharedWithHelper = SharedWithHelper(roomId, sessionId, store),
|
|
||||||
clock,
|
|
||||||
restoredOutboundGroupSession.creationTime
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun discardOutboundGroupSessionForRoom(roomId: String) {
|
|
||||||
val toDiscard = outboundGroupSessionCache.filter {
|
|
||||||
it.value.groupId == roomId
|
|
||||||
}
|
|
||||||
toDiscard.forEach { (sessionId, cacheItem) ->
|
|
||||||
cacheItem.groupSession.releaseSession()
|
|
||||||
outboundGroupSessionCache.remove(sessionId)
|
|
||||||
}
|
|
||||||
store.storeCurrentOutboundGroupSessionForRoom(roomId, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current session key of an outbound group session.
|
|
||||||
*
|
|
||||||
* @param sessionId the id of the outbound group session.
|
|
||||||
* @return the base64-encoded secret key.
|
|
||||||
*/
|
|
||||||
fun getSessionKey(sessionId: String): String? {
|
|
||||||
if (sessionId.isNotEmpty()) {
|
|
||||||
try {
|
|
||||||
return outboundGroupSessionCache[sessionId]!!.groupSession.sessionKey()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## getSessionKey() : failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current message index of an outbound group session.
|
|
||||||
*
|
|
||||||
* @param sessionId the id of the outbound group session.
|
|
||||||
* @return the current chain index.
|
|
||||||
*/
|
|
||||||
fun getMessageIndex(sessionId: String): Int {
|
|
||||||
return if (sessionId.isNotEmpty()) {
|
|
||||||
outboundGroupSessionCache[sessionId]!!.groupSession.messageIndex()
|
|
||||||
} else 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encrypt an outgoing message with an outbound group session.
|
|
||||||
*
|
|
||||||
* @param sessionId the id of the outbound group session.
|
|
||||||
* @param payloadString the payload to be encrypted and sent.
|
|
||||||
* @return ciphertext
|
|
||||||
*/
|
|
||||||
fun encryptGroupMessage(sessionId: String, payloadString: String): String? {
|
|
||||||
if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) {
|
|
||||||
try {
|
|
||||||
return outboundGroupSessionCache[sessionId]!!.groupSession.encryptMessage(payloadString)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## encryptGroupMessage() : failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inbound group session
|
|
||||||
|
|
||||||
sealed interface AddSessionResult {
|
|
||||||
data class Imported(val ratchetIndex: Int) : AddSessionResult
|
|
||||||
abstract class Failure : AddSessionResult
|
|
||||||
object NotImported : Failure()
|
|
||||||
data class NotImportedHigherIndex(val newIndex: Int) : Failure()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add an inbound group session to the session store.
|
|
||||||
*
|
|
||||||
* @param sessionId the session identifier.
|
|
||||||
* @param sessionKey base64-encoded secret key.
|
|
||||||
* @param roomId the id of the room in which this session will be used.
|
|
||||||
* @param senderKey the base64-encoded curve25519 key of the sender.
|
|
||||||
* @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us.
|
|
||||||
* @param keysClaimed Other keys the sender claims.
|
|
||||||
* @param exportFormat true if the megolm keys are in export format
|
|
||||||
* @return true if the operation succeeds.
|
|
||||||
*/
|
|
||||||
fun addInboundGroupSession(sessionId: String,
|
|
||||||
sessionKey: String,
|
|
||||||
roomId: String,
|
|
||||||
senderKey: String,
|
|
||||||
forwardingCurve25519KeyChain: List<String>,
|
|
||||||
keysClaimed: Map<String, String>,
|
|
||||||
exportFormat: Boolean): AddSessionResult {
|
|
||||||
val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
|
|
||||||
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
|
|
||||||
val existingSession = existingSessionHolder?.wrapper
|
|
||||||
// If we have an existing one we should check if the new one is not better
|
|
||||||
if (existingSession != null) {
|
|
||||||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session")
|
|
||||||
try {
|
|
||||||
val existingFirstKnown = existingSession.firstKnownIndex ?: return AddSessionResult.NotImported.also {
|
|
||||||
// This is quite unexpected, could throw if native was released?
|
|
||||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
|
|
||||||
candidateSession.olmInboundGroupSession?.releaseSession()
|
|
||||||
// Probably should discard it?
|
|
||||||
}
|
|
||||||
val newKnownFirstIndex = candidateSession.firstKnownIndex
|
|
||||||
// If our existing session is better we keep it
|
|
||||||
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
|
|
||||||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
|
|
||||||
candidateSession.olmInboundGroupSession?.releaseSession()
|
|
||||||
return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt())
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
|
|
||||||
candidateSession.olmInboundGroupSession?.releaseSession()
|
|
||||||
return AddSessionResult.NotImported
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId")
|
|
||||||
|
|
||||||
// sanity check on the new session
|
|
||||||
val candidateOlmInboundSession = candidateSession.olmInboundGroupSession
|
|
||||||
if (null == candidateOlmInboundSession) {
|
|
||||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>")
|
|
||||||
return AddSessionResult.NotImported
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (candidateOlmInboundSession.sessionIdentifier() != sessionId) {
|
|
||||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
|
||||||
candidateOlmInboundSession.releaseSession()
|
|
||||||
return AddSessionResult.NotImported
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
candidateOlmInboundSession.releaseSession()
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed")
|
|
||||||
return AddSessionResult.NotImported
|
|
||||||
}
|
|
||||||
|
|
||||||
candidateSession.senderKey = senderKey
|
|
||||||
candidateSession.roomId = roomId
|
|
||||||
candidateSession.keysClaimed = keysClaimed
|
|
||||||
candidateSession.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain
|
|
||||||
|
|
||||||
if (existingSession != null) {
|
|
||||||
inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
|
|
||||||
} else {
|
|
||||||
inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
return AddSessionResult.Imported(candidateSession.firstKnownIndex?.toInt() ?: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import an inbound group sessions to the session store.
|
|
||||||
*
|
|
||||||
* @param megolmSessionsData the megolm sessions data
|
|
||||||
* @return the successfully imported sessions.
|
|
||||||
*/
|
|
||||||
fun importInboundGroupSessions(megolmSessionsData: List<MegolmSessionData>): List<OlmInboundGroupSessionWrapper2> {
|
|
||||||
val sessions = ArrayList<OlmInboundGroupSessionWrapper2>(megolmSessionsData.size)
|
|
||||||
|
|
||||||
for (megolmSessionData in megolmSessionsData) {
|
|
||||||
val sessionId = megolmSessionData.sessionId ?: continue
|
|
||||||
val senderKey = megolmSessionData.senderKey ?: continue
|
|
||||||
val roomId = megolmSessionData.roomId
|
|
||||||
|
|
||||||
var candidateSessionToImport: OlmInboundGroupSessionWrapper2? = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
candidateSessionToImport = OlmInboundGroupSessionWrapper2(megolmSessionData)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanity check
|
|
||||||
if (candidateSessionToImport?.olmInboundGroupSession == null) {
|
|
||||||
Timber.tag(loggerTag.value).e("## importInboundGroupSession : invalid session")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val candidateOlmInboundGroupSession = candidateSessionToImport.olmInboundGroupSession
|
|
||||||
try {
|
|
||||||
if (candidateOlmInboundGroupSession?.sessionIdentifier() != sessionId) {
|
|
||||||
Timber.tag(loggerTag.value).e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
|
||||||
candidateOlmInboundGroupSession?.releaseSession()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession : sessionIdentifier() failed")
|
|
||||||
candidateOlmInboundGroupSession?.releaseSession()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
|
|
||||||
val existingSession = existingSessionHolder?.wrapper
|
|
||||||
|
|
||||||
if (existingSession == null) {
|
|
||||||
// Session does not already exist, add it
|
|
||||||
Timber.tag(loggerTag.value).d("## importInboundGroupSession() : importing new megolm session $senderKey/$sessionId")
|
|
||||||
sessions.add(candidateSessionToImport)
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
|
||||||
val existingFirstKnown = tryOrNull { existingSession.firstKnownIndex }
|
|
||||||
val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.firstKnownIndex }
|
|
||||||
|
|
||||||
if (existingFirstKnown == null || candidateFirstKnownIndex == null) {
|
|
||||||
// should not happen?
|
|
||||||
candidateSessionToImport.olmInboundGroupSession?.releaseSession()
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex")
|
|
||||||
} else {
|
|
||||||
if (existingFirstKnown <= candidateSessionToImport.firstKnownIndex!!) {
|
|
||||||
// Ignore this, keep existing
|
|
||||||
candidateOlmInboundGroupSession.releaseSession()
|
|
||||||
} else {
|
|
||||||
// update cache with better session
|
|
||||||
inboundGroupSessionStore.replaceGroupSession(
|
|
||||||
existingSessionHolder,
|
|
||||||
InboundGroupSessionHolder(candidateSessionToImport),
|
|
||||||
sessionId,
|
|
||||||
senderKey
|
|
||||||
)
|
|
||||||
sessions.add(candidateSessionToImport)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
store.storeInboundGroupSessions(sessions)
|
|
||||||
|
|
||||||
return sessions
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrypt a received message with an inbound group session.
|
|
||||||
*
|
|
||||||
* @param body the base64-encoded body of the encrypted message.
|
|
||||||
* @param roomId the room in which the message was received.
|
|
||||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
|
||||||
* @param sessionId the session identifier.
|
|
||||||
* @param senderKey the base64-encoded curve25519 key of the sender.
|
|
||||||
* @return the decrypting result. Nil if the sessionId is unknown.
|
|
||||||
*/
|
|
||||||
@Throws(MXCryptoError::class)
|
|
||||||
suspend fun decryptGroupMessage(body: String,
|
|
||||||
roomId: String,
|
|
||||||
timeline: String?,
|
|
||||||
sessionId: String,
|
|
||||||
senderKey: String): OlmDecryptionResult {
|
|
||||||
val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId)
|
|
||||||
val wrapper = sessionHolder.wrapper
|
|
||||||
val inboundGroupSession = wrapper.olmInboundGroupSession
|
|
||||||
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null")
|
|
||||||
// Check that the room id matches the original one for the session. This stops
|
|
||||||
// the HS pretending a message was targeting a different room.
|
|
||||||
if (roomId == wrapper.roomId) {
|
|
||||||
val decryptResult = try {
|
|
||||||
sessionHolder.mutex.withLock {
|
|
||||||
inboundGroupSession.decryptMessage(body)
|
|
||||||
}
|
|
||||||
} catch (e: OlmException) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed")
|
|
||||||
throw MXCryptoError.OlmError(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (timeline?.isNotBlank() == true) {
|
|
||||||
val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() }
|
|
||||||
|
|
||||||
val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex
|
|
||||||
|
|
||||||
if (timelineSet.contains(messageIndexKey)) {
|
|
||||||
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
timelineSet.add(messageIndexKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey)
|
|
||||||
val payload = try {
|
|
||||||
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
|
|
||||||
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
|
|
||||||
adapter.fromJson(payloadString)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptGroupMessage() : fails to parse the payload")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
|
|
||||||
}
|
|
||||||
|
|
||||||
return OlmDecryptionResult(
|
|
||||||
payload,
|
|
||||||
wrapper.keysClaimed,
|
|
||||||
senderKey,
|
|
||||||
wrapper.forwardingCurve25519KeyChain
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, wrapper.roomId)
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset replay attack data for the given timeline.
|
|
||||||
*
|
|
||||||
* @param timeline the id of the timeline.
|
|
||||||
*/
|
|
||||||
fun resetReplayAttackCheckInTimeline(timeline: String?) {
|
|
||||||
if (null != timeline) {
|
|
||||||
inboundGroupSessionMessageIndexes.remove(timeline)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utilities
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify an ed25519 signature on a JSON object.
|
|
||||||
*
|
|
||||||
* @param key the ed25519 key.
|
|
||||||
* @param jsonDictionary the JSON object which was signed.
|
|
||||||
* @param signature the base64-encoded signature to be checked.
|
|
||||||
* @throws Exception the exception
|
|
||||||
*/
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun verifySignature(key: String, jsonDictionary: Map<String, Any>, signature: String) {
|
|
||||||
// Check signature on the canonical version of the JSON
|
|
||||||
olmUtility!!.verifyEd25519Signature(signature, key, JsonCanonicalizer.getCanonicalJson(Map::class.java, jsonDictionary))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the SHA-256 hash of the input and encodes it as base64.
|
|
||||||
*
|
|
||||||
* @param message the message to hash.
|
|
||||||
* @return the base64-encoded hash value.
|
|
||||||
*/
|
|
||||||
fun sha256(message: String): String {
|
|
||||||
return olmUtility!!.sha256(convertToUTF8(message))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search an OlmSession
|
|
||||||
*
|
|
||||||
* @param theirDeviceIdentityKey the device key
|
|
||||||
* @param sessionId the session Id
|
|
||||||
* @return the olm session
|
|
||||||
*/
|
|
||||||
private fun getSessionForDevice(theirDeviceIdentityKey: String, sessionId: String): OlmSessionWrapper? {
|
|
||||||
// sanity check
|
|
||||||
return if (theirDeviceIdentityKey.isEmpty() || sessionId.isEmpty()) null else {
|
|
||||||
olmSessionStore.getDeviceSession(sessionId, theirDeviceIdentityKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract an InboundGroupSession from the session store and do some check.
|
|
||||||
* inboundGroupSessionWithIdError describes the failure reason.
|
|
||||||
*
|
|
||||||
* @param roomId the room where the session is used.
|
|
||||||
* @param sessionId the session identifier.
|
|
||||||
* @param senderKey the base64-encoded curve25519 key of the sender.
|
|
||||||
* @return the inbound group session.
|
|
||||||
*/
|
|
||||||
fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): InboundGroupSessionHolder {
|
|
||||||
if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) {
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON)
|
|
||||||
}
|
|
||||||
|
|
||||||
val holder = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
|
|
||||||
val session = holder?.wrapper
|
|
||||||
|
|
||||||
if (session != null) {
|
|
||||||
// Check that the room id matches the original one for the session. This stops
|
|
||||||
// the HS pretending a message was targeting a different room.
|
|
||||||
if (roomId != session.roomId) {
|
|
||||||
val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
|
|
||||||
Timber.tag(loggerTag.value).e("## getInboundGroupSession() : $errorDescription")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription)
|
|
||||||
} else {
|
|
||||||
return holder
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).w("## getInboundGroupSession() : UISI $sessionId")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if we have the keys for a given megolm session.
|
|
||||||
*
|
|
||||||
* @param roomId room in which the message was received
|
|
||||||
* @param senderKey base64-encoded curve25519 key of the sender
|
|
||||||
* @param sessionId session identifier
|
|
||||||
* @return true if the unbound session keys are known.
|
|
||||||
*/
|
|
||||||
fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean {
|
|
||||||
return runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }.isSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
fun clearOlmSessionCache() {
|
|
||||||
olmSessionStore.clear()
|
|
||||||
}
|
|
||||||
}
|
|
@ -20,6 +20,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class MyDeviceInfoHolder @Inject constructor(
|
|
||||||
// The credentials,
|
|
||||||
credentials: Credentials,
|
|
||||||
// the crypto store
|
|
||||||
cryptoStore: IMXCryptoStore,
|
|
||||||
// Olm device
|
|
||||||
olmDevice: MXOlmDevice
|
|
||||||
) {
|
|
||||||
// Our device keys
|
|
||||||
/**
|
|
||||||
* my device info
|
|
||||||
*/
|
|
||||||
val myDevice: CryptoDeviceInfo
|
|
||||||
|
|
||||||
init {
|
|
||||||
|
|
||||||
val keys = HashMap<String, String>()
|
|
||||||
|
|
||||||
// TODO it's a bit strange, why not load from DB?
|
|
||||||
if (!olmDevice.deviceEd25519Key.isNullOrEmpty()) {
|
|
||||||
keys["ed25519:" + credentials.deviceId] = olmDevice.deviceEd25519Key!!
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!olmDevice.deviceCurve25519Key.isNullOrEmpty()) {
|
|
||||||
keys["curve25519:" + credentials.deviceId] = olmDevice.deviceCurve25519Key!!
|
|
||||||
}
|
|
||||||
|
|
||||||
// myDevice.keys = keys
|
|
||||||
//
|
|
||||||
// myDevice.algorithms = MXCryptoAlgorithms.supportedAlgorithms()
|
|
||||||
|
|
||||||
// TODO hwo to really check cross signed status?
|
|
||||||
//
|
|
||||||
val crossSigned = cryptoStore.getMyCrossSigningInfo()?.masterKey()?.trustLevel?.locallyVerified ?: false
|
|
||||||
// myDevice.trustLevel = DeviceTrustLevel(crossSigned, true)
|
|
||||||
|
|
||||||
myDevice = CryptoDeviceInfo(
|
|
||||||
credentials.deviceId!!,
|
|
||||||
credentials.userId,
|
|
||||||
keys = keys,
|
|
||||||
algorithms = MXCryptoAlgorithms.supportedAlgorithms(),
|
|
||||||
trustLevel = DeviceTrustLevel(crossSigned, true)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Add our own deviceinfo to the store
|
|
||||||
val endToEndDevicesForUser = cryptoStore.getUserDevices(credentials.userId)
|
|
||||||
|
|
||||||
val myDevices = endToEndDevicesForUser.orEmpty().toMutableMap()
|
|
||||||
|
|
||||||
myDevices[myDevice.deviceId] = myDevice
|
|
||||||
|
|
||||||
cryptoStore.storeUserDevices(credentials.userId, myDevices)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class ObjectSigner @Inject constructor(private val credentials: Credentials,
|
|
||||||
private val olmDevice: MXOlmDevice) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign Object
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* <pre>
|
|
||||||
* {
|
|
||||||
* "[MY_USER_ID]": {
|
|
||||||
* "ed25519:[MY_DEVICE_ID]": "sign(str)"
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* @param strToSign the String to sign and to include in the Map
|
|
||||||
* @return a Map (see example)
|
|
||||||
*/
|
|
||||||
fun signObject(strToSign: String): Map<String, Map<String, String>> {
|
|
||||||
val result = HashMap<String, Map<String, String>>()
|
|
||||||
|
|
||||||
val content = HashMap<String, String>()
|
|
||||||
|
|
||||||
content["ed25519:" + credentials.deviceId] = olmDevice.signMessage(strToSign)
|
|
||||||
?: "" // null reported by rageshake if happens during logout
|
|
||||||
|
|
||||||
result[credentials.userId] = content
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,247 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXKey
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import org.matrix.olm.OlmAccount
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.math.floor
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
// The spec recommend a 5mn delay, but due to federation
|
|
||||||
// or server downtime we give it a bit more time (1 hour)
|
|
||||||
private const val FALLBACK_KEY_FORGET_DELAY = 60 * 60_000L
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class OneTimeKeysUploader @Inject constructor(
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val objectSigner: ObjectSigner,
|
|
||||||
private val uploadKeysTask: UploadKeysTask,
|
|
||||||
private val clock: Clock,
|
|
||||||
context: Context
|
|
||||||
) {
|
|
||||||
// tell if there is a OTK check in progress
|
|
||||||
private var oneTimeKeyCheckInProgress = false
|
|
||||||
|
|
||||||
// last OTK check timestamp
|
|
||||||
private var lastOneTimeKeyCheck: Long = 0
|
|
||||||
private var oneTimeKeyCount: Int? = null
|
|
||||||
|
|
||||||
// Simple storage to remember when was uploaded the last fallback key
|
|
||||||
private val storage = context.getSharedPreferences("OneTimeKeysUploader_${olmDevice.deviceEd25519Key.hashCode()}", Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores the current one_time_key count which will be handled later (in a call of
|
|
||||||
* _onSyncCompleted). The count is e.g. coming from a /sync response.
|
|
||||||
*
|
|
||||||
* @param currentCount the new count
|
|
||||||
*/
|
|
||||||
fun updateOneTimeKeyCount(currentCount: Int) {
|
|
||||||
oneTimeKeyCount = currentCount
|
|
||||||
}
|
|
||||||
|
|
||||||
fun needsNewFallback() {
|
|
||||||
if (olmDevice.generateFallbackKeyIfNeeded()) {
|
|
||||||
// As we generated a new one, it's already forgetting one
|
|
||||||
// so we can clear the last publish time
|
|
||||||
// (in case the network calls fails after to avoid calling forgetKey)
|
|
||||||
saveLastFallbackKeyPublishTime(0L)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the OTK must be uploaded.
|
|
||||||
*/
|
|
||||||
suspend fun maybeUploadOneTimeKeys() {
|
|
||||||
if (oneTimeKeyCheckInProgress) {
|
|
||||||
Timber.v("maybeUploadOneTimeKeys: already in progress")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (clock.epochMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) {
|
|
||||||
// we've done a key upload recently.
|
|
||||||
Timber.v("maybeUploadOneTimeKeys: executed too recently")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
oneTimeKeyCheckInProgress = true
|
|
||||||
|
|
||||||
val oneTimeKeyCountFromSync = oneTimeKeyCount
|
|
||||||
?: fetchOtkCount() // we don't have count from sync so get from server
|
|
||||||
?: return Unit.also {
|
|
||||||
oneTimeKeyCheckInProgress = false
|
|
||||||
Timber.w("maybeUploadOneTimeKeys: Failed to get otk count from server")
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.d("maybeUploadOneTimeKeys: otk count $oneTimeKeyCountFromSync , unpublished fallback key ${olmDevice.hasUnpublishedFallbackKey()}")
|
|
||||||
|
|
||||||
lastOneTimeKeyCheck = clock.epochMillis()
|
|
||||||
|
|
||||||
// We then check how many keys we can store in the Account object.
|
|
||||||
val maxOneTimeKeys = olmDevice.getMaxNumberOfOneTimeKeys()
|
|
||||||
|
|
||||||
// Try to keep at most half that number on the server. This leaves the
|
|
||||||
// rest of the slots free to hold keys that have been claimed from the
|
|
||||||
// server but we haven't received a message for.
|
|
||||||
// If we run out of slots when generating new keys then olm will
|
|
||||||
// discard the oldest private keys first. This will eventually clean
|
|
||||||
// out stale private keys that won't receive a message.
|
|
||||||
val keyLimit = floor(maxOneTimeKeys / 2.0).toInt()
|
|
||||||
|
|
||||||
// We need to keep a pool of one time public keys on the server so that
|
|
||||||
// other devices can start conversations with us. But we can only store
|
|
||||||
// a finite number of private keys in the olm Account object.
|
|
||||||
// To complicate things further then can be a delay between a device
|
|
||||||
// claiming a public one time key from the server and it sending us a
|
|
||||||
// message. We need to keep the corresponding private key locally until
|
|
||||||
// we receive the message.
|
|
||||||
// But that message might never arrive leaving us stuck with duff
|
|
||||||
// private keys clogging up our local storage.
|
|
||||||
// So we need some kind of engineering compromise to balance all of
|
|
||||||
// these factors.
|
|
||||||
tryOrNull("Unable to upload OTK") {
|
|
||||||
val uploadedKeys = uploadOTK(oneTimeKeyCountFromSync, keyLimit)
|
|
||||||
Timber.v("## uploadKeys() : success, $uploadedKeys key(s) sent")
|
|
||||||
}
|
|
||||||
oneTimeKeyCheckInProgress = false
|
|
||||||
|
|
||||||
// Check if we need to forget a fallback key
|
|
||||||
val latestPublishedTime = getLastFallbackKeyPublishTime()
|
|
||||||
if (latestPublishedTime != 0L && clock.epochMillis() - latestPublishedTime > FALLBACK_KEY_FORGET_DELAY) {
|
|
||||||
// This should be called once you are reasonably certain that you will not receive any more messages
|
|
||||||
// that use the old fallback key
|
|
||||||
Timber.d("## forgetFallbackKey()")
|
|
||||||
olmDevice.forgetFallbackKey()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun fetchOtkCount(): Int? {
|
|
||||||
return tryOrNull("Unable to get OTK count") {
|
|
||||||
val result = uploadKeysTask.execute(UploadKeysTask.Params(null, null, null))
|
|
||||||
result.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload some the OTKs.
|
|
||||||
*
|
|
||||||
* @param keyCount the key count
|
|
||||||
* @param keyLimit the limit
|
|
||||||
* @return the number of uploaded keys
|
|
||||||
*/
|
|
||||||
private suspend fun uploadOTK(keyCount: Int, keyLimit: Int): Int {
|
|
||||||
if (keyLimit <= keyCount && !olmDevice.hasUnpublishedFallbackKey()) {
|
|
||||||
// If we don't need to generate any more keys then we are done.
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
var keysThisLoop = 0
|
|
||||||
if (keyLimit > keyCount) {
|
|
||||||
// Creating keys can be an expensive operation so we limit the
|
|
||||||
// number we generate in one go to avoid blocking the application
|
|
||||||
// for too long.
|
|
||||||
keysThisLoop = min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER)
|
|
||||||
olmDevice.generateOneTimeKeys(keysThisLoop)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We check before sending if there is an unpublished key in order to saveLastFallbackKeyPublishTime if needed
|
|
||||||
val hadUnpublishedFallbackKey = olmDevice.hasUnpublishedFallbackKey()
|
|
||||||
val response = uploadOneTimeKeys(olmDevice.getOneTimeKeys())
|
|
||||||
olmDevice.markKeysAsPublished()
|
|
||||||
if (hadUnpublishedFallbackKey) {
|
|
||||||
// It had an unpublished fallback key that was published just now
|
|
||||||
saveLastFallbackKeyPublishTime(clock.epochMillis())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) {
|
|
||||||
// Maybe upload other keys
|
|
||||||
return keysThisLoop +
|
|
||||||
uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit) +
|
|
||||||
(if (hadUnpublishedFallbackKey) 1 else 0)
|
|
||||||
} else {
|
|
||||||
Timber.e("## uploadOTK() : response for uploading keys does not contain one_time_key_counts.signed_curve25519")
|
|
||||||
throw Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveLastFallbackKeyPublishTime(timeMillis: Long) {
|
|
||||||
storage.edit().putLong("last_fb_key_publish", timeMillis).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getLastFallbackKeyPublishTime(): Long {
|
|
||||||
return storage.getLong("last_fb_key_publish", 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload curve25519 one time keys.
|
|
||||||
*/
|
|
||||||
private suspend fun uploadOneTimeKeys(oneTimeKeys: Map<String, Map<String, String>>?): KeysUploadResponse {
|
|
||||||
val oneTimeJson = mutableMapOf<String, Any>()
|
|
||||||
|
|
||||||
val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty()
|
|
||||||
|
|
||||||
curve25519Map.forEach { (key_id, value) ->
|
|
||||||
val k = mutableMapOf<String, Any>()
|
|
||||||
k["key"] = value
|
|
||||||
|
|
||||||
// the key is also signed
|
|
||||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)
|
|
||||||
|
|
||||||
k["signatures"] = objectSigner.signObject(canonicalJson)
|
|
||||||
|
|
||||||
oneTimeJson["signed_curve25519:$key_id"] = k
|
|
||||||
}
|
|
||||||
|
|
||||||
val fallbackJson = mutableMapOf<String, Any>()
|
|
||||||
val fallbackCurve25519Map = olmDevice.getFallbackKey()?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty()
|
|
||||||
fallbackCurve25519Map.forEach { (key_id, key) ->
|
|
||||||
val k = mutableMapOf<String, Any>()
|
|
||||||
k["key"] = key
|
|
||||||
k["fallback"] = true
|
|
||||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)
|
|
||||||
k["signatures"] = objectSigner.signObject(canonicalJson)
|
|
||||||
|
|
||||||
fallbackJson["signed_curve25519:$key_id"] = k
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now, we set the device id explicitly, as we may not be using the
|
|
||||||
// same one as used in login.
|
|
||||||
val uploadParams = UploadKeysTask.Params(
|
|
||||||
deviceKeys = null,
|
|
||||||
oneTimeKeys = oneTimeJson,
|
|
||||||
fallbackKeys = fallbackJson.takeIf { fallbackJson.isNotEmpty() }
|
|
||||||
)
|
|
||||||
return uploadKeysTask.executeRetry(uploadParams, 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
// max number of keys to upload at once
|
|
||||||
// Creating keys can be an expensive operation so we limit the
|
|
||||||
// number we generate in one go to avoid blocking the application
|
|
||||||
// for too long.
|
|
||||||
private const val ONE_TIME_KEY_GENERATION_MAX_NUMBER = 5
|
|
||||||
|
|
||||||
// frequency with which to check & upload one-time keys
|
|
||||||
private const val ONE_TIME_KEY_UPLOAD_PERIOD = (60_000).toLong() // one minute
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,518 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
|
||||||
import org.matrix.android.sdk.api.util.fromBase64
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.di.SessionId
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.Stack
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlin.system.measureTimeMillis
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("OutgoingKeyRequestManager", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is responsible for sending key requests to other devices when a message failed to decrypt.
|
|
||||||
* It's lifecycle is based on the sync pulse:
|
|
||||||
* - You can post queries for session, or report when you got a session
|
|
||||||
* - At the end of the sync (onSyncComplete) it will then process all the posted request and send to devices
|
|
||||||
* If a request failed it will be retried at the end of the next sync
|
|
||||||
*/
|
|
||||||
@SessionScope
|
|
||||||
internal class OutgoingKeyRequestManager @Inject constructor(
|
|
||||||
@SessionId private val sessionId: String,
|
|
||||||
@UserId private val myUserId: String,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val cryptoConfig: MXCryptoConfig,
|
|
||||||
private val inboundGroupSessionStore: InboundGroupSessionStore,
|
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
|
||||||
private val deviceListManager: DeviceListManager,
|
|
||||||
private val perSessionBackupQueryRateLimiter: PerSessionBackupQueryRateLimiter) {
|
|
||||||
|
|
||||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
|
||||||
private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher)
|
|
||||||
private val sequencer = SemaphoreCoroutineSequencer()
|
|
||||||
|
|
||||||
// We only have one active key request per session, so we don't request if it's already requested
|
|
||||||
// But it could make sense to check more the backup, as it's evolving.
|
|
||||||
// We keep a stack as we consider that the key requested last is more likely to be on screen?
|
|
||||||
private val requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup = Stack<Pair<String, String>>()
|
|
||||||
|
|
||||||
fun requestKeyForEvent(event: Event, force: Boolean) {
|
|
||||||
val (targets, body) = getRoomKeyRequestTargetForEvent(event) ?: return
|
|
||||||
val index = ratchetIndexForMessage(event) ?: 0
|
|
||||||
postRoomKeyRequest(body, targets, index, force)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getRoomKeyRequestTargetForEvent(event: Event): Pair<Map<String, List<String>>, RoomKeyRequestBody>? {
|
|
||||||
val sender = event.senderId ?: return null
|
|
||||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return null.also {
|
|
||||||
Timber.tag(loggerTag.value).e("getRoomKeyRequestTargetForEvent Failed to re-request key, null content")
|
|
||||||
}
|
|
||||||
if (encryptedEventContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null
|
|
||||||
|
|
||||||
val senderDevice = encryptedEventContent.deviceId
|
|
||||||
val recipients = if (cryptoConfig.limitRoomKeyRequestsToMyDevices) {
|
|
||||||
mapOf(
|
|
||||||
myUserId to listOf("*")
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
if (event.senderId == myUserId) {
|
|
||||||
mapOf(
|
|
||||||
myUserId to listOf("*")
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// for the case where you share the key with a device that has a broken olm session
|
|
||||||
// The other user might Re-shares a megolm session key with devices if the key has already been
|
|
||||||
// sent to them.
|
|
||||||
mapOf(
|
|
||||||
myUserId to listOf("*"),
|
|
||||||
|
|
||||||
// We might not have deviceId in the future due to https://github.com/matrix-org/matrix-spec-proposals/pull/3700
|
|
||||||
// so in this case query to all
|
|
||||||
sender to listOf(senderDevice ?: "*")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val requestBody = RoomKeyRequestBody(
|
|
||||||
roomId = event.roomId,
|
|
||||||
algorithm = encryptedEventContent.algorithm,
|
|
||||||
senderKey = encryptedEventContent.senderKey,
|
|
||||||
sessionId = encryptedEventContent.sessionId
|
|
||||||
)
|
|
||||||
return recipients to requestBody
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ratchetIndexForMessage(event: Event): Int? {
|
|
||||||
val encryptedContent = event.content.toModel<EncryptedEventContent>() ?: return null
|
|
||||||
if (encryptedContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null
|
|
||||||
return encryptedContent.ciphertext?.fromBase64()?.inputStream()?.reader()?.let {
|
|
||||||
tryOrNull {
|
|
||||||
val megolmVersion = it.read()
|
|
||||||
if (megolmVersion != 3) return@tryOrNull null
|
|
||||||
/** Int tag */
|
|
||||||
if (it.read() != 8) return@tryOrNull null
|
|
||||||
it.read()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun postRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>, fromIndex: Int, force: Boolean = false) {
|
|
||||||
outgoingRequestScope.launch {
|
|
||||||
sequencer.post {
|
|
||||||
internalQueueRequest(requestBody, recipients, fromIndex, force)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Typically called when we the session as been imported or received meanwhile
|
|
||||||
*/
|
|
||||||
fun postCancelRequestForSessionIfNeeded(sessionId: String, roomId: String, senderKey: String, fromIndex: Int) {
|
|
||||||
outgoingRequestScope.launch {
|
|
||||||
sequencer.post {
|
|
||||||
internalQueueCancelRequest(sessionId, roomId, senderKey, fromIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSelfCrossSigningTrustChanged(newTrust: Boolean) {
|
|
||||||
if (newTrust) {
|
|
||||||
// we were previously not cross signed, but we are now
|
|
||||||
// so there is now more chances to get better replies for existing request
|
|
||||||
// Let's forget about sent request so that next time we try to decrypt we will resend requests
|
|
||||||
// We don't resend all because we don't want to generate a bulk of traffic
|
|
||||||
outgoingRequestScope.launch {
|
|
||||||
sequencer.post {
|
|
||||||
cryptoStore.deleteOutgoingRoomKeyRequestInState(OutgoingRoomKeyRequestState.SENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
sequencer.post {
|
|
||||||
delay(1000)
|
|
||||||
perSessionBackupQueryRateLimiter.refreshBackupInfoIfNeeded(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onRoomKeyForwarded(sessionId: String,
|
|
||||||
algorithm: String,
|
|
||||||
roomId: String,
|
|
||||||
senderKey: String,
|
|
||||||
fromDevice: String?,
|
|
||||||
fromIndex: Int,
|
|
||||||
event: Event) {
|
|
||||||
Timber.tag(loggerTag.value).d("Key forwarded for $sessionId from ${event.senderId}|$fromDevice at index $fromIndex")
|
|
||||||
outgoingRequestScope.launch {
|
|
||||||
sequencer.post {
|
|
||||||
cryptoStore.updateOutgoingRoomKeyReply(
|
|
||||||
roomId = roomId,
|
|
||||||
sessionId = sessionId,
|
|
||||||
algorithm = algorithm,
|
|
||||||
senderKey = senderKey,
|
|
||||||
fromDevice = fromDevice,
|
|
||||||
// strip out encrypted stuff as it's just a trail?
|
|
||||||
event = event.copy(
|
|
||||||
type = event.getClearType(),
|
|
||||||
content = mapOf(
|
|
||||||
"chain_index" to fromIndex
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onRoomKeyWithHeld(sessionId: String,
|
|
||||||
algorithm: String,
|
|
||||||
roomId: String,
|
|
||||||
senderKey: String,
|
|
||||||
fromDevice: String?,
|
|
||||||
event: Event) {
|
|
||||||
outgoingRequestScope.launch {
|
|
||||||
sequencer.post {
|
|
||||||
Timber.tag(loggerTag.value).d("Withheld received for $sessionId from ${event.senderId}|$fromDevice")
|
|
||||||
Timber.tag(loggerTag.value).v("Withheld content ${event.getClearContent()}")
|
|
||||||
|
|
||||||
// We want to store withheld code from the sender of the message (owner of the megolm session), not from
|
|
||||||
// other devices that might gossip the key. If not the initial reason might be overridden
|
|
||||||
// by a request to one of our session.
|
|
||||||
event.getClearContent().toModel<RoomKeyWithHeldContent>()?.let { withheld ->
|
|
||||||
withContext(coroutineDispatchers.crypto) {
|
|
||||||
tryOrNull {
|
|
||||||
deviceListManager.downloadKeys(listOf(event.senderId ?: ""), false)
|
|
||||||
}
|
|
||||||
cryptoStore.getUserDeviceList(event.senderId ?: "")
|
|
||||||
.also { devices ->
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.v("Withheld Devices for ${event.senderId} are ${devices.orEmpty().joinToString { it.identityKey() ?: "" }}")
|
|
||||||
}
|
|
||||||
?.firstOrNull {
|
|
||||||
it.identityKey() == senderKey
|
|
||||||
}
|
|
||||||
}.also {
|
|
||||||
Timber.tag(loggerTag.value).v("Withheld device for sender key $senderKey is from ${it?.shortDebugString()}")
|
|
||||||
}?.let {
|
|
||||||
if (it.userId == event.senderId) {
|
|
||||||
if (fromDevice != null) {
|
|
||||||
if (it.deviceId == fromDevice) {
|
|
||||||
Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}")
|
|
||||||
cryptoStore.addWithHeldMegolmSession(withheld)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}")
|
|
||||||
cryptoStore.addWithHeldMegolmSession(withheld)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Here we store the replies from a given request
|
|
||||||
cryptoStore.updateOutgoingRoomKeyReply(
|
|
||||||
roomId = roomId,
|
|
||||||
sessionId = sessionId,
|
|
||||||
algorithm = algorithm,
|
|
||||||
senderKey = senderKey,
|
|
||||||
fromDevice = fromDevice,
|
|
||||||
event = event
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should be called after a sync, ideally if no catchup sync needed (as keys might arrive in those)
|
|
||||||
*/
|
|
||||||
fun requireProcessAllPendingKeyRequests() {
|
|
||||||
outgoingRequestScope.launch {
|
|
||||||
sequencer.post {
|
|
||||||
internalProcessPendingKeyRequests()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun internalQueueCancelRequest(sessionId: String, roomId: String, senderKey: String, localKnownChainIndex: Int) {
|
|
||||||
// do we have known requests for that session??
|
|
||||||
Timber.tag(loggerTag.value).v("Cancel Key Request if needed for $sessionId")
|
|
||||||
val knownRequest = cryptoStore.getOutgoingRoomKeyRequest(
|
|
||||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
|
||||||
roomId = roomId,
|
|
||||||
sessionId = sessionId,
|
|
||||||
senderKey = senderKey
|
|
||||||
)
|
|
||||||
if (knownRequest.isEmpty()) return Unit.also {
|
|
||||||
Timber.tag(loggerTag.value).v("Handle Cancel Key Request for $sessionId -- Was not currently requested")
|
|
||||||
}
|
|
||||||
if (knownRequest.size > 1) {
|
|
||||||
// It's worth logging, there should be only one
|
|
||||||
Timber.tag(loggerTag.value).w("Found multiple requests for same sessionId $sessionId")
|
|
||||||
}
|
|
||||||
knownRequest.forEach { request ->
|
|
||||||
when (request.state) {
|
|
||||||
OutgoingRoomKeyRequestState.UNSENT -> {
|
|
||||||
if (request.fromIndex >= localKnownChainIndex) {
|
|
||||||
// we have a good index we can cancel
|
|
||||||
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OutgoingRoomKeyRequestState.SENT -> {
|
|
||||||
// It was already sent, and index satisfied we can cancel
|
|
||||||
if (request.fromIndex >= localKnownChainIndex) {
|
|
||||||
cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> {
|
|
||||||
// It is already marked to be cancelled
|
|
||||||
}
|
|
||||||
OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> {
|
|
||||||
if (request.fromIndex >= localKnownChainIndex) {
|
|
||||||
// we just want to cancel now
|
|
||||||
cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> {
|
|
||||||
// was already canceled
|
|
||||||
// if we need a better index, should we resend?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun close() {
|
|
||||||
try {
|
|
||||||
outgoingRequestScope.cancel("User Terminate")
|
|
||||||
requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.clear()
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).w("Failed to shutDown request manager")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun internalQueueRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>, fromIndex: Int, force: Boolean) {
|
|
||||||
if (!cryptoStore.isKeyGossipingEnabled()) {
|
|
||||||
// we might want to try backup?
|
|
||||||
if (requestBody.roomId != null && requestBody.sessionId != null) {
|
|
||||||
requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(requestBody.roomId to requestBody.sessionId)
|
|
||||||
}
|
|
||||||
Timber.tag(loggerTag.value).d("discarding request for ${requestBody.sessionId} as gossiping is disabled")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).d("Queueing key request for ${requestBody.sessionId} force:$force")
|
|
||||||
val existing = cryptoStore.getOutgoingRoomKeyRequest(requestBody)
|
|
||||||
Timber.tag(loggerTag.value).v("Queueing key request exiting is ${existing?.state}")
|
|
||||||
when (existing?.state) {
|
|
||||||
null -> {
|
|
||||||
// create a new one
|
|
||||||
cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex)
|
|
||||||
}
|
|
||||||
OutgoingRoomKeyRequestState.UNSENT -> {
|
|
||||||
// nothing it's new or not yet handled
|
|
||||||
}
|
|
||||||
OutgoingRoomKeyRequestState.SENT -> {
|
|
||||||
// it was already requested
|
|
||||||
Timber.tag(loggerTag.value).d("The session ${requestBody.sessionId} is already requested")
|
|
||||||
if (force) {
|
|
||||||
// update to UNSENT
|
|
||||||
Timber.tag(loggerTag.value).d(".. force to request ${requestBody.sessionId}")
|
|
||||||
cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND)
|
|
||||||
} else {
|
|
||||||
if (existing.roomId != null && existing.sessionId != null) {
|
|
||||||
requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(existing.roomId to existing.sessionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> {
|
|
||||||
// request is canceled only if I got the keys so what to do here...
|
|
||||||
if (force) {
|
|
||||||
cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> {
|
|
||||||
// It's already going to resend
|
|
||||||
}
|
|
||||||
OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> {
|
|
||||||
if (force) {
|
|
||||||
cryptoStore.deleteOutgoingRoomKeyRequest(existing.requestId)
|
|
||||||
cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existing != null && existing.fromIndex >= fromIndex) {
|
|
||||||
// update the required index
|
|
||||||
cryptoStore.updateOutgoingRoomKeyRequiredIndex(existing.requestId, fromIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun internalProcessPendingKeyRequests() {
|
|
||||||
val toProcess = cryptoStore.getOutgoingRoomKeyRequests(OutgoingRoomKeyRequestState.pendingStates())
|
|
||||||
Timber.tag(loggerTag.value).v("Processing all pending key requests (found ${toProcess.size} pending)")
|
|
||||||
|
|
||||||
measureTimeMillis {
|
|
||||||
toProcess.forEach {
|
|
||||||
when (it.state) {
|
|
||||||
OutgoingRoomKeyRequestState.UNSENT -> handleUnsentRequest(it)
|
|
||||||
OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> handleRequestToCancel(it)
|
|
||||||
OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> handleRequestToCancelWillResend(it)
|
|
||||||
OutgoingRoomKeyRequestState.SENT_THEN_CANCELED,
|
|
||||||
OutgoingRoomKeyRequestState.SENT -> {
|
|
||||||
// these are filtered out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.let {
|
|
||||||
Timber.tag(loggerTag.value).v("Finish processing pending key request in $it ms")
|
|
||||||
}
|
|
||||||
|
|
||||||
val maxBackupCallsBySync = 60
|
|
||||||
var currentCalls = 0
|
|
||||||
measureTimeMillis {
|
|
||||||
while (requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.isNotEmpty() && currentCalls < maxBackupCallsBySync) {
|
|
||||||
requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.pop().let { (roomId, sessionId) ->
|
|
||||||
// we want to rate limit that somehow :/
|
|
||||||
perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)
|
|
||||||
}
|
|
||||||
currentCalls++
|
|
||||||
}
|
|
||||||
}.let {
|
|
||||||
Timber.tag(loggerTag.value).v("Finish querying backup in $it ms")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun handleUnsentRequest(request: OutgoingKeyRequest) {
|
|
||||||
// In order to avoid generating to_device traffic, we can first check if the key is backed up
|
|
||||||
Timber.tag(loggerTag.value).v("Handling unsent request for megolm session ${request.sessionId} in ${request.roomId}")
|
|
||||||
val sessionId = request.sessionId ?: return
|
|
||||||
val roomId = request.roomId ?: return
|
|
||||||
if (perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)) {
|
|
||||||
// let's see what's the index
|
|
||||||
val knownIndex = tryOrNull {
|
|
||||||
inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "")?.wrapper?.firstKnownIndex
|
|
||||||
}
|
|
||||||
if (knownIndex != null && knownIndex <= request.fromIndex) {
|
|
||||||
// we found the key in backup with good enough index, so we can just mark as cancelled, no need to send request
|
|
||||||
Timber.tag(loggerTag.value).v("Megolm session $sessionId successfully restored from backup, do not send request")
|
|
||||||
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// we need to send the request
|
|
||||||
val toDeviceContent = RoomKeyShareRequest(
|
|
||||||
requestingDeviceId = cryptoStore.getDeviceId(),
|
|
||||||
requestId = request.requestId,
|
|
||||||
action = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
|
|
||||||
body = request.requestBody
|
|
||||||
)
|
|
||||||
val contentMap = MXUsersDevicesMap<Any>()
|
|
||||||
request.recipients.forEach { userToDeviceMap ->
|
|
||||||
userToDeviceMap.value.forEach { deviceId ->
|
|
||||||
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val params = SendToDeviceTask.Params(
|
|
||||||
eventType = EventType.ROOM_KEY_REQUEST,
|
|
||||||
contentMap = contentMap,
|
|
||||||
transactionId = request.requestId
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
withContext(coroutineDispatchers.io) {
|
|
||||||
sendToDeviceTask.executeRetry(params, 3)
|
|
||||||
}
|
|
||||||
Timber.tag(loggerTag.value).d("Key request sent for $sessionId in room $roomId to ${request.recipients}")
|
|
||||||
// The request was sent, so update state
|
|
||||||
cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT)
|
|
||||||
// TODO update the audit trail
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).v("Failed to request $sessionId targets:${request.recipients}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun handleRequestToCancel(request: OutgoingKeyRequest): Boolean {
|
|
||||||
Timber.tag(loggerTag.value).v("handleRequestToCancel for megolm session ${request.sessionId}")
|
|
||||||
// we have to cancel this
|
|
||||||
val toDeviceContent = RoomKeyShareRequest(
|
|
||||||
requestingDeviceId = cryptoStore.getDeviceId(),
|
|
||||||
requestId = request.requestId,
|
|
||||||
action = GossipingToDeviceObject.ACTION_SHARE_CANCELLATION
|
|
||||||
)
|
|
||||||
val contentMap = MXUsersDevicesMap<Any>()
|
|
||||||
request.recipients.forEach { userToDeviceMap ->
|
|
||||||
userToDeviceMap.value.forEach { deviceId ->
|
|
||||||
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val params = SendToDeviceTask.Params(
|
|
||||||
eventType = EventType.ROOM_KEY_REQUEST,
|
|
||||||
contentMap = contentMap,
|
|
||||||
transactionId = request.requestId
|
|
||||||
)
|
|
||||||
return try {
|
|
||||||
withContext(coroutineDispatchers.io) {
|
|
||||||
sendToDeviceTask.executeRetry(params, 3)
|
|
||||||
}
|
|
||||||
// The request cancellation was sent, we don't delete yet because we want
|
|
||||||
// to keep trace of the sent replies
|
|
||||||
cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT_THEN_CANCELED)
|
|
||||||
true
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).v("Failed to cancel request ${request.requestId} for session $sessionId targets:${request.recipients}")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun handleRequestToCancelWillResend(request: OutgoingKeyRequest) {
|
|
||||||
if (handleRequestToCancel(request)) {
|
|
||||||
// this will create a new unsent request with no replies that will be process in the following call
|
|
||||||
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
|
|
||||||
request.requestBody?.let { cryptoStore.getOrAddOutgoingRoomKeyRequest(it, request.recipients, request.fromIndex) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
|
|||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
||||||
import org.matrix.android.sdk.api.util.awaitCallback
|
import org.matrix.android.sdk.api.util.awaitCallback
|
||||||
|
import org.matrix.android.sdk.internal.crypto.keysbackup.RustKeyBackupService
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
import org.matrix.android.sdk.internal.util.time.Clock
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -39,7 +40,7 @@ private val loggerTag = LoggerTag("OutgoingGossipingRequestManager", LoggerTag.C
|
|||||||
*/
|
*/
|
||||||
internal class PerSessionBackupQueryRateLimiter @Inject constructor(
|
internal class PerSessionBackupQueryRateLimiter @Inject constructor(
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val keysBackupService: Lazy<DefaultKeysBackupService>,
|
private val keysBackupService: Lazy<RustKeyBackupService>,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
) {
|
) {
|
||||||
@ -103,16 +104,14 @@ internal class PerSessionBackupQueryRateLimiter @Inject constructor(
|
|||||||
|
|
||||||
val successfullyImported = withContext(coroutineDispatchers.io) {
|
val successfullyImported = withContext(coroutineDispatchers.io) {
|
||||||
try {
|
try {
|
||||||
awaitCallback<ImportRoomKeysResult> {
|
|
||||||
keysBackupService.get().restoreKeysWithRecoveryKey(
|
keysBackupService.get().restoreKeysWithRecoveryKey(
|
||||||
currentVersion,
|
currentVersion,
|
||||||
savedKeyBackupKeyInfo?.recoveryKey ?: "",
|
savedKeyBackupKeyInfo?.recoveryKey ?: "",
|
||||||
roomId,
|
roomId,
|
||||||
sessionId,
|
sessionId,
|
||||||
null,
|
null,
|
||||||
it
|
|
||||||
)
|
)
|
||||||
}.successfullyNumberOfImportedKeys
|
.successfullyNumberOfImportedKeys
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
// Fail silently
|
// Fail silently
|
||||||
Timber.tag(loggerTag.value).v("getFromBackup failed ${failure.localizedMessage}")
|
Timber.tag(loggerTag.value).v("getFromBackup failed ${failure.localizedMessage}")
|
||||||
|
@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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 org.matrix.android.sdk.internal.crypto
|
||||||
|
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.joinAll
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
|
import org.matrix.android.sdk.internal.crypto.keysbackup.RustKeyBackupService
|
||||||
|
import org.matrix.android.sdk.internal.crypto.network.RequestSender
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
|
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
|
||||||
|
import timber.log.Timber
|
||||||
|
import uniffi.olm.Request
|
||||||
|
import uniffi.olm.RequestType
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("PrepareToEncryptUseCase", LoggerTag.CRYPTO)
|
||||||
|
|
||||||
|
@SessionScope
|
||||||
|
internal class PrepareToEncryptUseCase @Inject constructor(olmMachineProvider: OlmMachineProvider,
|
||||||
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
|
private val cryptoStore: IMXCryptoStore,
|
||||||
|
private val getRoomUserIds: GetRoomUserIdsUseCase,
|
||||||
|
private val requestSender: RequestSender,
|
||||||
|
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||||
|
private val keysBackupService: RustKeyBackupService
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val olmMachine = olmMachineProvider.olmMachine
|
||||||
|
|
||||||
|
private val keyClaimLock: Mutex = Mutex()
|
||||||
|
private val roomKeyShareLocks: ConcurrentHashMap<String, Mutex> = ConcurrentHashMap()
|
||||||
|
|
||||||
|
suspend operator fun invoke(roomId: String, ensureAllMembersAreLoaded: Boolean) {
|
||||||
|
withContext(coroutineDispatchers.crypto) {
|
||||||
|
Timber.tag(loggerTag.value).d("prepareToEncrypt() roomId:$roomId Check room members up to date")
|
||||||
|
// Ensure to load all room members
|
||||||
|
if (ensureAllMembersAreLoaded) {
|
||||||
|
try {
|
||||||
|
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members")
|
||||||
|
throw failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val userIds = getRoomUserIds(roomId)
|
||||||
|
val algorithm = getEncryptionAlgorithm(roomId)
|
||||||
|
if (algorithm == null) {
|
||||||
|
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON)
|
||||||
|
Timber.tag(loggerTag.value).e("prepareToEncrypt() : $reason")
|
||||||
|
throw IllegalArgumentException("Missing algorithm")
|
||||||
|
}
|
||||||
|
preshareRoomKey(roomId, userIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getEncryptionAlgorithm(roomId: String): String? {
|
||||||
|
return cryptoStore.getRoomAlgorithm(roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun preshareRoomKey(roomId: String, roomMembers: List<String>) {
|
||||||
|
claimMissingKeys(roomMembers)
|
||||||
|
val keyShareLock = roomKeyShareLocks.getOrPut(roomId) { Mutex() }
|
||||||
|
var sharedKey = false
|
||||||
|
keyShareLock.withLock {
|
||||||
|
coroutineScope {
|
||||||
|
olmMachine.shareRoomKey(roomId, roomMembers).map {
|
||||||
|
when (it) {
|
||||||
|
is Request.ToDevice -> {
|
||||||
|
sharedKey = true
|
||||||
|
async {
|
||||||
|
sendToDevice(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// This request can only be a to-device request but
|
||||||
|
// we need to handle all our cases and put this
|
||||||
|
// async block for our joinAll to work.
|
||||||
|
async {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.joinAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we sent out a room key over to-device messages it's likely that we created a new one
|
||||||
|
// Try to back the key up
|
||||||
|
if (sharedKey) {
|
||||||
|
keysBackupService.maybeBackupKeys()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun claimMissingKeys(roomMembers: List<String>) = keyClaimLock.withLock {
|
||||||
|
val request = this.olmMachine.getMissingSessions(roomMembers)
|
||||||
|
// This request can only be a keys claim request.
|
||||||
|
when (request) {
|
||||||
|
is Request.KeysClaim -> {
|
||||||
|
claimKeys(request)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun sendToDevice(request: Request.ToDevice) {
|
||||||
|
try {
|
||||||
|
requestSender.sendToDevice(request)
|
||||||
|
olmMachine.markRequestAsSent(request.requestId, RequestType.TO_DEVICE, "{}")
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
Timber.tag(loggerTag.value).e(throwable, "## CRYPTO sendToDevice(): error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun claimKeys(request: Request.KeysClaim) {
|
||||||
|
try {
|
||||||
|
val response = requestSender.claimKeys(request)
|
||||||
|
olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_CLAIM, response)
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
Timber.tag(loggerTag.value).e(throwable, "## CRYPTO claimKeys(): error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -23,7 +23,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerification
|
|||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.safeValueOf
|
import org.matrix.android.sdk.api.session.crypto.verification.safeValueOf
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
|
import org.matrix.android.sdk.api.util.fromBase64
|
||||||
import org.matrix.android.sdk.internal.crypto.network.RequestSender
|
import org.matrix.android.sdk.internal.crypto.network.RequestSender
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.UpdateDispatcher
|
import org.matrix.android.sdk.internal.crypto.verification.UpdateDispatcher
|
||||||
import uniffi.olm.CryptoStoreException
|
import uniffi.olm.CryptoStoreException
|
||||||
|
@ -1,106 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryptionFactory
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmDecryptionFactory
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class RoomDecryptorProvider @Inject constructor(
|
|
||||||
private val olmDecryptionFactory: MXOlmDecryptionFactory,
|
|
||||||
private val megolmDecryptionFactory: MXMegolmDecryptionFactory
|
|
||||||
) {
|
|
||||||
|
|
||||||
// A map from algorithm to MXDecrypting instance, for each room
|
|
||||||
private val roomDecryptors: MutableMap<String /* room id */, MutableMap<String /* algorithm */, IMXDecrypting>> = HashMap()
|
|
||||||
|
|
||||||
private val newSessionListeners = ArrayList<NewSessionListener>()
|
|
||||||
|
|
||||||
fun addNewSessionListener(listener: NewSessionListener) {
|
|
||||||
if (!newSessionListeners.contains(listener)) newSessionListeners.add(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeSessionListener(listener: NewSessionListener) {
|
|
||||||
newSessionListeners.remove(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a decryptor for a given room and algorithm.
|
|
||||||
* If we already have a decryptor for the given room and algorithm, return
|
|
||||||
* it. Otherwise try to instantiate it.
|
|
||||||
*
|
|
||||||
* @param roomId the room id
|
|
||||||
* @param algorithm the crypto algorithm
|
|
||||||
* @return the decryptor
|
|
||||||
* // TODO Create another method for the case of roomId is null
|
|
||||||
*/
|
|
||||||
fun getOrCreateRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? {
|
|
||||||
// sanity check
|
|
||||||
if (algorithm.isNullOrEmpty()) {
|
|
||||||
Timber.e("## getRoomDecryptor() : null algorithm")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (roomId != null && roomId.isNotEmpty()) {
|
|
||||||
synchronized(roomDecryptors) {
|
|
||||||
val decryptors = roomDecryptors.getOrPut(roomId) { mutableMapOf() }
|
|
||||||
val alg = decryptors[algorithm]
|
|
||||||
if (alg != null) {
|
|
||||||
return alg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val decryptingClass = MXCryptoAlgorithms.hasDecryptorClassForAlgorithm(algorithm)
|
|
||||||
if (decryptingClass) {
|
|
||||||
val alg = when (algorithm) {
|
|
||||||
MXCRYPTO_ALGORITHM_MEGOLM -> megolmDecryptionFactory.create().apply {
|
|
||||||
this.newSessionListener = object : NewSessionListener {
|
|
||||||
override fun onNewSession(roomId: String?, sessionId: String) {
|
|
||||||
// PR reviewer: the parameter has been renamed so is now in conflict with the parameter of getOrCreateRoomDecryptor
|
|
||||||
newSessionListeners.toList().forEach {
|
|
||||||
try {
|
|
||||||
it.onNewSession(roomId, sessionId)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> olmDecryptionFactory.create()
|
|
||||||
}
|
|
||||||
if (!roomId.isNullOrEmpty()) {
|
|
||||||
synchronized(roomDecryptors) {
|
|
||||||
roomDecryptors[roomId]?.put(algorithm, alg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return alg
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? {
|
|
||||||
if (roomId == null || algorithm == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return roomDecryptors[roomId]?.get(algorithm)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class RoomEncryptorsStore @Inject constructor(
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val megolmEncryptionFactory: MXMegolmEncryptionFactory,
|
|
||||||
private val olmEncryptionFactory: MXOlmEncryptionFactory,
|
|
||||||
) {
|
|
||||||
|
|
||||||
// MXEncrypting instance for each room.
|
|
||||||
private val roomEncryptors = mutableMapOf<String, IMXEncrypting>()
|
|
||||||
|
|
||||||
fun put(roomId: String, alg: IMXEncrypting) {
|
|
||||||
synchronized(roomEncryptors) {
|
|
||||||
roomEncryptors.put(roomId, alg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun get(roomId: String): IMXEncrypting? {
|
|
||||||
return synchronized(roomEncryptors) {
|
|
||||||
val cache = roomEncryptors[roomId]
|
|
||||||
if (cache != null) {
|
|
||||||
return@synchronized cache
|
|
||||||
} else {
|
|
||||||
val alg: IMXEncrypting? = when (cryptoStore.getRoomAlgorithm(roomId)) {
|
|
||||||
MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId)
|
|
||||||
MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
alg?.let { roomEncryptors.put(roomId, it) }
|
|
||||||
return@synchronized alg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,15 +18,15 @@ package org.matrix.android.sdk.internal.crypto
|
|||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.UserTrustResult
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustResult
|
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.UserTrustResult
|
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -24,11 +24,6 @@ import kotlinx.coroutines.withContext
|
|||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
|
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
|
import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
|
||||||
@ -36,9 +31,6 @@ import org.matrix.android.sdk.api.session.events.model.Event
|
|||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
|
import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
|
import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
|
||||||
@ -54,8 +46,6 @@ internal class SecretShareManager @Inject constructor(
|
|||||||
private val credentials: Credentials,
|
private val credentials: Credentials,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
private val cryptoCoroutineScope: CoroutineScope,
|
||||||
private val messageEncrypter: MessageEncrypter,
|
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
private val sendToDeviceTask: SendToDeviceTask,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
@ -107,6 +97,7 @@ internal class SecretShareManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
suspend fun handleSecretRequest(toDevice: Event) {
|
suspend fun handleSecretRequest(toDevice: Event) {
|
||||||
val request = toDevice.getClearContent().toModel<SecretShareRequest>()
|
val request = toDevice.getClearContent().toModel<SecretShareRequest>()
|
||||||
?: return Unit.also {
|
?: return Unit.also {
|
||||||
@ -214,6 +205,8 @@ internal class SecretShareManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
private suspend fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean {
|
private suspend fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean {
|
||||||
val verifTimestamp = verifMutex.withLock {
|
val verifTimestamp = verifMutex.withLock {
|
||||||
recentlyVerifiedDevices[deviceId]
|
recentlyVerifiedDevices[deviceId]
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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 org.matrix.android.sdk.internal.crypto
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class ShouldEncryptForInvitedMembersUseCase @Inject constructor(private val cryptoConfig: MXCryptoConfig,
|
||||||
|
private val cryptoStore: IMXCryptoStore) {
|
||||||
|
|
||||||
|
operator fun invoke(roomId: String): Boolean {
|
||||||
|
return cryptoConfig.enableEncryptionForInvitedMembers && cryptoStore.shouldEncryptForInvitedMembers(roomId)
|
||||||
|
}
|
||||||
|
}
|
@ -18,11 +18,11 @@ package org.matrix.android.sdk.internal.crypto
|
|||||||
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
|
|
||||||
import org.matrix.android.sdk.internal.crypto.network.RequestSender
|
import org.matrix.android.sdk.internal.crypto.network.RequestSender
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.prepareMethods
|
import org.matrix.android.sdk.internal.crypto.verification.prepareMethods
|
||||||
import uniffi.olm.CryptoStoreException
|
import uniffi.olm.CryptoStoreException
|
||||||
|
@ -27,7 +27,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationI
|
|||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.safeValueOf
|
import org.matrix.android.sdk.api.session.crypto.verification.safeValueOf
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
|
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN
|
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN
|
||||||
import org.matrix.android.sdk.internal.crypto.network.RequestSender
|
import org.matrix.android.sdk.internal.crypto.network.RequestSender
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.prepareMethods
|
import org.matrix.android.sdk.internal.crypto.verification.prepareMethods
|
||||||
|
@ -1,170 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.actions
|
|
||||||
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXKey
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
private const val ONE_TIME_KEYS_RETRY_COUNT = 3
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("EnsureOlmSessionsForDevicesAction", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) {
|
|
||||||
|
|
||||||
private val ensureMutex = Mutex()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We want to synchronize a bit here, because we are iterating to check existing olm session and
|
|
||||||
* also adding some
|
|
||||||
*/
|
|
||||||
suspend fun handle(devicesByUser: Map<String, List<CryptoDeviceInfo>>, force: Boolean = false): MXUsersDevicesMap<MXOlmSessionResult> {
|
|
||||||
ensureMutex.withLock {
|
|
||||||
val results = MXUsersDevicesMap<MXOlmSessionResult>()
|
|
||||||
val deviceList = devicesByUser.flatMap { it.value }
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.d("ensure olm forced:$force for ${deviceList.joinToString { it.shortDebugString() }}")
|
|
||||||
val devicesToCreateSessionWith = mutableListOf<CryptoDeviceInfo>()
|
|
||||||
if (force) {
|
|
||||||
// we take all devices and will query otk for them
|
|
||||||
devicesToCreateSessionWith.addAll(deviceList)
|
|
||||||
} else {
|
|
||||||
// only peek devices without active session
|
|
||||||
deviceList.forEach { deviceInfo ->
|
|
||||||
val deviceId = deviceInfo.deviceId
|
|
||||||
val userId = deviceInfo.userId
|
|
||||||
val key = deviceInfo.identityKey() ?: return@forEach Unit.also {
|
|
||||||
Timber.tag(loggerTag.value).w("Ignoring device ${deviceInfo.shortDebugString()} without identity key")
|
|
||||||
}
|
|
||||||
|
|
||||||
// is there a session that as been already used?
|
|
||||||
val sessionId = olmDevice.getSessionId(key)
|
|
||||||
if (sessionId.isNullOrEmpty()) {
|
|
||||||
Timber.tag(loggerTag.value).d("Found no existing olm session ${deviceInfo.shortDebugString()} add to claim list")
|
|
||||||
devicesToCreateSessionWith.add(deviceInfo)
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).d("using olm session $sessionId for (${deviceInfo.userId}|$deviceId)")
|
|
||||||
val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId)
|
|
||||||
results.setObject(userId, deviceId, olmSessionResult)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (devicesToCreateSessionWith.isEmpty()) {
|
|
||||||
// no session to create
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
val usersDevicesToClaim = MXUsersDevicesMap<String>().apply {
|
|
||||||
devicesToCreateSessionWith.forEach {
|
|
||||||
setObject(it.userId, it.deviceId, MXKey.KEY_SIGNED_CURVE_25519_TYPE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's now claim one time keys
|
|
||||||
val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
|
|
||||||
val oneTimeKeys = withContext(coroutineDispatchers.io) {
|
|
||||||
oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, ONE_TIME_KEYS_RETRY_COUNT)
|
|
||||||
}
|
|
||||||
|
|
||||||
// let now start olm session using the new otks
|
|
||||||
devicesToCreateSessionWith.forEach { deviceInfo ->
|
|
||||||
val userId = deviceInfo.userId
|
|
||||||
val deviceId = deviceInfo.deviceId
|
|
||||||
// Did we get an OTK
|
|
||||||
val oneTimeKey = oneTimeKeys.getObject(userId, deviceId)
|
|
||||||
if (oneTimeKey == null) {
|
|
||||||
Timber.tag(loggerTag.value).d("No otk for ${deviceInfo.shortDebugString()}")
|
|
||||||
} else if (oneTimeKey.type != MXKey.KEY_SIGNED_CURVE_25519_TYPE) {
|
|
||||||
Timber.tag(loggerTag.value).d("Bad otk type (${oneTimeKey.type}) for ${deviceInfo.shortDebugString()}")
|
|
||||||
} else {
|
|
||||||
val olmSessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo)
|
|
||||||
if (olmSessionId != null) {
|
|
||||||
val olmSessionResult = MXOlmSessionResult(deviceInfo, olmSessionId)
|
|
||||||
results.setObject(userId, deviceId, olmSessionResult)
|
|
||||||
} else {
|
|
||||||
Timber
|
|
||||||
.tag(loggerTag.value)
|
|
||||||
.d("## CRYPTO | cant unwedge failed to create outbound ${deviceInfo.shortDebugString()}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: CryptoDeviceInfo): String? {
|
|
||||||
var sessionId: String? = null
|
|
||||||
|
|
||||||
val deviceId = deviceInfo.deviceId
|
|
||||||
val signKeyId = "ed25519:$deviceId"
|
|
||||||
val signature = oneTimeKey.signatureForUserId(userId, signKeyId)
|
|
||||||
|
|
||||||
val fingerprint = deviceInfo.fingerprint()
|
|
||||||
if (!signature.isNullOrEmpty() && !fingerprint.isNullOrEmpty()) {
|
|
||||||
var isVerified = false
|
|
||||||
var errorMessage: String? = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
olmDevice.verifySignature(fingerprint, oneTimeKey.signalableJSONDictionary(), signature)
|
|
||||||
isVerified = true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).d(
|
|
||||||
e, "verifyKeyAndStartSession() : Verify error for otk: ${oneTimeKey.signalableJSONDictionary()}," +
|
|
||||||
" signature:$signature fingerprint:$fingerprint"
|
|
||||||
)
|
|
||||||
Timber.tag(loggerTag.value).e(
|
|
||||||
"verifyKeyAndStartSession() : Verify error for ${deviceInfo.userId}|${deviceInfo.deviceId} " +
|
|
||||||
" - signable json ${oneTimeKey.signalableJSONDictionary()}"
|
|
||||||
)
|
|
||||||
errorMessage = e.message
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check one-time key signature
|
|
||||||
if (isVerified) {
|
|
||||||
sessionId = deviceInfo.identityKey()?.let { identityKey ->
|
|
||||||
olmDevice.createOutboundSession(identityKey, oneTimeKey.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sessionId.isNullOrEmpty()) {
|
|
||||||
// Possibly a bad key
|
|
||||||
Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId")
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).d("verifyKeyAndStartSession() : Started new sessionId $sessionId for device $userId:$deviceId")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Unable to verify otk signature for $userId:$deviceId: $errorMessage")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sessionId
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.actions
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val olmDevice: MXOlmDevice,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to make sure we have established olm sessions for the given users.
|
|
||||||
* @param users a list of user ids.
|
|
||||||
*/
|
|
||||||
suspend fun handle(users: List<String>): MXUsersDevicesMap<MXOlmSessionResult> {
|
|
||||||
Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users")
|
|
||||||
val devicesByUser = users.associateWith { userId ->
|
|
||||||
val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
|
|
||||||
|
|
||||||
devices.filter {
|
|
||||||
// Don't bother setting up session to ourself
|
|
||||||
it.identityKey() != olmDevice.deviceCurve25519Key &&
|
|
||||||
// Don't bother setting up sessions with blocked users
|
|
||||||
!(it.trustLevel?.isVerified() ?: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,122 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.actions
|
|
||||||
|
|
||||||
import androidx.annotation.WorkerThread
|
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
||||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
|
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.RoomDecryptorProvider
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MegolmSessionDataImporter", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
internal class MegolmSessionDataImporter @Inject constructor(private val olmDevice: MXOlmDevice,
|
|
||||||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
|
||||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val clock: Clock,
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import a list of megolm session keys.
|
|
||||||
* Must be call on the crypto coroutine thread
|
|
||||||
*
|
|
||||||
* @param megolmSessionsData megolm sessions.
|
|
||||||
* @param fromBackup true if the imported keys are already backed up on the server.
|
|
||||||
* @param progressListener the progress listener
|
|
||||||
* @return import room keys result
|
|
||||||
*/
|
|
||||||
@WorkerThread
|
|
||||||
fun handle(megolmSessionsData: List<MegolmSessionData>,
|
|
||||||
fromBackup: Boolean,
|
|
||||||
progressListener: ProgressListener?): ImportRoomKeysResult {
|
|
||||||
val t0 = clock.epochMillis()
|
|
||||||
|
|
||||||
val totalNumbersOfKeys = megolmSessionsData.size
|
|
||||||
var lastProgress = 0
|
|
||||||
var totalNumbersOfImportedKeys = 0
|
|
||||||
|
|
||||||
progressListener?.onProgress(0, 100)
|
|
||||||
val olmInboundGroupSessionWrappers = olmDevice.importInboundGroupSessions(megolmSessionsData)
|
|
||||||
|
|
||||||
megolmSessionsData.forEachIndexed { cpt, megolmSessionData ->
|
|
||||||
val decrypting = roomDecryptorProvider.getOrCreateRoomDecryptor(megolmSessionData.roomId, megolmSessionData.algorithm)
|
|
||||||
|
|
||||||
if (null != decrypting) {
|
|
||||||
try {
|
|
||||||
val sessionId = megolmSessionData.sessionId
|
|
||||||
Timber.tag(loggerTag.value).v("## importRoomKeys retrieve senderKey ${megolmSessionData.senderKey} sessionId $sessionId")
|
|
||||||
|
|
||||||
totalNumbersOfImportedKeys++
|
|
||||||
|
|
||||||
// cancel any outstanding room key requests for this session
|
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).d("Imported megolm session $sessionId from backup=$fromBackup in ${megolmSessionData.roomId}")
|
|
||||||
outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(
|
|
||||||
megolmSessionData.sessionId ?: "",
|
|
||||||
megolmSessionData.roomId ?: "",
|
|
||||||
megolmSessionData.senderKey ?: "",
|
|
||||||
tryOrNull {
|
|
||||||
olmInboundGroupSessionWrappers
|
|
||||||
.firstOrNull { it.olmInboundGroupSession?.sessionIdentifier() == megolmSessionData.sessionId }
|
|
||||||
?.firstKnownIndex?.toInt()
|
|
||||||
} ?: 0
|
|
||||||
)
|
|
||||||
|
|
||||||
// Have another go at decrypting events sent with this session
|
|
||||||
when (decrypting) {
|
|
||||||
is MXMegolmDecryption -> {
|
|
||||||
decrypting.onNewSession(megolmSessionData.roomId, megolmSessionData.senderKey!!, sessionId!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## importRoomKeys() : onNewSession failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressListener != null) {
|
|
||||||
val progress = 100 * (cpt + 1) / totalNumbersOfKeys
|
|
||||||
|
|
||||||
if (lastProgress != progress) {
|
|
||||||
lastProgress = progress
|
|
||||||
|
|
||||||
progressListener.onProgress(progress, 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not back up the key if it comes from a backup recovery
|
|
||||||
if (fromBackup) {
|
|
||||||
cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers)
|
|
||||||
}
|
|
||||||
|
|
||||||
val t1 = clock.epochMillis()
|
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)")
|
|
||||||
|
|
||||||
return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.actions
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedMessage
|
|
||||||
import org.matrix.android.sdk.internal.di.DeviceId
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
|
||||||
import org.matrix.android.sdk.internal.util.convertToUTF8
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MessageEncrypter", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
internal class MessageEncrypter @Inject constructor(
|
|
||||||
@UserId
|
|
||||||
private val userId: String,
|
|
||||||
@DeviceId
|
|
||||||
private val deviceId: String?,
|
|
||||||
private val olmDevice: MXOlmDevice) {
|
|
||||||
/**
|
|
||||||
* Encrypt an event payload for a list of devices.
|
|
||||||
* This method must be called from the getCryptoHandler() thread.
|
|
||||||
*
|
|
||||||
* @param payloadFields fields to include in the encrypted payload.
|
|
||||||
* @param deviceInfos list of device infos to encrypt for.
|
|
||||||
* @return the content for an m.room.encrypted event.
|
|
||||||
*/
|
|
||||||
suspend fun encryptMessage(payloadFields: Content, deviceInfos: List<CryptoDeviceInfo>): EncryptedMessage {
|
|
||||||
val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! }
|
|
||||||
|
|
||||||
val payloadJson = payloadFields.toMutableMap()
|
|
||||||
|
|
||||||
payloadJson["sender"] = userId
|
|
||||||
payloadJson["sender_device"] = deviceId!!
|
|
||||||
|
|
||||||
// Include the Ed25519 key so that the recipient knows what
|
|
||||||
// device this message came from.
|
|
||||||
// We don't need to include the curve25519 key since the
|
|
||||||
// recipient will already know this from the olm headers.
|
|
||||||
// When combined with the device keys retrieved from the
|
|
||||||
// homeserver signed by the ed25519 key this proves that
|
|
||||||
// the curve25519 key and the ed25519 key are owned by
|
|
||||||
// the same device.
|
|
||||||
payloadJson["keys"] = mapOf("ed25519" to olmDevice.deviceEd25519Key!!)
|
|
||||||
|
|
||||||
val ciphertext = mutableMapOf<String, Any>()
|
|
||||||
|
|
||||||
for ((deviceKey, deviceInfo) in deviceInfoParticipantKey) {
|
|
||||||
val sessionId = olmDevice.getSessionId(deviceKey)
|
|
||||||
|
|
||||||
if (!sessionId.isNullOrEmpty()) {
|
|
||||||
Timber.tag(loggerTag.value).d("Using sessionid $sessionId for device $deviceKey")
|
|
||||||
|
|
||||||
payloadJson["recipient"] = deviceInfo.userId
|
|
||||||
payloadJson["recipient_keys"] = mapOf("ed25519" to deviceInfo.fingerprint()!!)
|
|
||||||
|
|
||||||
val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson))
|
|
||||||
ciphertext[deviceKey] = olmDevice.encryptMessage(deviceKey, sessionId, payloadString)!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return EncryptedMessage(
|
|
||||||
algorithm = MXCRYPTO_ALGORITHM_OLM,
|
|
||||||
senderKey = olmDevice.deviceCurve25519Key,
|
|
||||||
cipherText = ciphertext
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.actions
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class SetDeviceVerificationAction @Inject constructor(
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
@UserId private val userId: String,
|
|
||||||
private val defaultKeysBackupService: DefaultKeysBackupService) {
|
|
||||||
|
|
||||||
fun handle(trustLevel: DeviceTrustLevel, userId: String, deviceId: String) {
|
|
||||||
val device = cryptoStore.getUserDevice(userId, deviceId)
|
|
||||||
|
|
||||||
// Sanity check
|
|
||||||
if (null == device) {
|
|
||||||
Timber.w("## setDeviceVerification() : Unknown device $userId:$deviceId")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device.isVerified != trustLevel.isVerified()) {
|
|
||||||
if (userId == this.userId) {
|
|
||||||
// If one of the user's own devices is being marked as verified / unverified,
|
|
||||||
// check the key backup status, since whether or not we use this depends on
|
|
||||||
// whether it has a signature from a verified device
|
|
||||||
defaultKeysBackupService.checkAndStartKeysBackup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device.trustLevel != trustLevel) {
|
|
||||||
device.trustLevel = trustLevel
|
|
||||||
cryptoStore.setDeviceTrust(userId, deviceId, trustLevel.crossSigningVerified, trustLevel.locallyVerified)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.algorithms
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface for decrypting data
|
|
||||||
*/
|
|
||||||
internal interface IMXDecrypting {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 decryption information, or an error
|
|
||||||
*/
|
|
||||||
@Throws(MXCryptoError::class)
|
|
||||||
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a key event.
|
|
||||||
*
|
|
||||||
* @param event the key event.
|
|
||||||
*/
|
|
||||||
fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.algorithms
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface for encrypting data
|
|
||||||
*/
|
|
||||||
@Deprecated("rust")
|
|
||||||
internal interface IMXEncrypting {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 userIds the room members the event will be sent to.
|
|
||||||
* @return the encrypted content
|
|
||||||
*/
|
|
||||||
suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.algorithms
|
|
||||||
|
|
||||||
@Deprecated("rust")
|
|
||||||
internal interface IMXGroupEncryption {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In Megolm, each recipient maintains a record of the ratchet value which allows
|
|
||||||
* them to decrypt any messages sent in the session after the corresponding point
|
|
||||||
* in the conversation. If this value is compromised, an attacker can similarly
|
|
||||||
* decrypt past messages which were encrypted by a key derived from the
|
|
||||||
* compromised or subsequent ratchet values. This gives 'partial' forward
|
|
||||||
* secrecy.
|
|
||||||
*
|
|
||||||
* To mitigate this issue, the application should offer the user the option to
|
|
||||||
* discard historical conversations, by winding forward any stored ratchet values,
|
|
||||||
* or discarding sessions altogether.
|
|
||||||
*/
|
|
||||||
fun discardSessionKey()
|
|
||||||
|
|
||||||
suspend fun preshareKey(userIds: List<String>)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-shares a session key with devices if the key has already been
|
|
||||||
* sent to them.
|
|
||||||
*
|
|
||||||
* @param sessionId The id of the outbound session to share.
|
|
||||||
* @param userId The id of the user who owns the target device.
|
|
||||||
* @param deviceId The id of the target device.
|
|
||||||
* @param senderKey The key of the originating device for the session.
|
|
||||||
*
|
|
||||||
* @return true in case of success
|
|
||||||
*/
|
|
||||||
suspend fun reshareKey(groupSessionId: String,
|
|
||||||
userId: String,
|
|
||||||
deviceId: String,
|
|
||||||
senderKey: String): Boolean
|
|
||||||
}
|
|
@ -1,304 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
|
||||||
|
|
||||||
import dagger.Lazy
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.session.StreamEventsManager
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
internal class MXMegolmDecryption(
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val liveEventManager: Lazy<StreamEventsManager>
|
|
||||||
) : IMXDecrypting {
|
|
||||||
|
|
||||||
var newSessionListener: NewSessionListener? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Events which we couldn't decrypt due to unknown sessions / indexes: map from
|
|
||||||
* senderKey|sessionId to timelines to list of MatrixEvents.
|
|
||||||
*/
|
|
||||||
// private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()
|
|
||||||
|
|
||||||
@Throws(MXCryptoError::class)
|
|
||||||
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
|
||||||
return decryptEvent(event, timeline, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(MXCryptoError::class)
|
|
||||||
private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
|
|
||||||
Timber.tag(loggerTag.value).v("decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail")
|
|
||||||
if (event.roomId.isNullOrBlank()) {
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
|
||||||
}
|
|
||||||
|
|
||||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
|
||||||
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
|
||||||
|
|
||||||
if (encryptedEventContent.senderKey.isNullOrBlank() ||
|
|
||||||
encryptedEventContent.sessionId.isNullOrBlank() ||
|
|
||||||
encryptedEventContent.ciphertext.isNullOrBlank()) {
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
|
||||||
}
|
|
||||||
|
|
||||||
return runCatching {
|
|
||||||
olmDevice.decryptGroupMessage(
|
|
||||||
encryptedEventContent.ciphertext,
|
|
||||||
event.roomId,
|
|
||||||
timeline,
|
|
||||||
encryptedEventContent.sessionId,
|
|
||||||
encryptedEventContent.senderKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.fold(
|
|
||||||
{ olmDecryptionResult ->
|
|
||||||
// the decryption succeeds
|
|
||||||
if (olmDecryptionResult.payload != null) {
|
|
||||||
MXEventDecryptionResult(
|
|
||||||
clearEvent = olmDecryptionResult.payload,
|
|
||||||
senderCurve25519Key = olmDecryptionResult.senderKey,
|
|
||||||
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
|
|
||||||
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
|
|
||||||
.orEmpty()
|
|
||||||
).also {
|
|
||||||
liveEventManager.get().dispatchLiveEventDecrypted(event, it)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ throwable ->
|
|
||||||
liveEventManager.get().dispatchLiveEventDecryptionFailed(event, throwable)
|
|
||||||
if (throwable is MXCryptoError.OlmError) {
|
|
||||||
// TODO Check the value of .message
|
|
||||||
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
|
|
||||||
// So we know that session, but it's ratcheted and we can't decrypt at that index
|
|
||||||
|
|
||||||
if (requestKeysOnFail) {
|
|
||||||
requestKeysForEvent(event)
|
|
||||||
}
|
|
||||||
// Check if partially withheld
|
|
||||||
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
|
||||||
if (withHeldInfo != null) {
|
|
||||||
// Encapsulate as withHeld exception
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.KEYS_WITHHELD,
|
|
||||||
withHeldInfo.code?.value ?: "",
|
|
||||||
withHeldInfo.reason
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
|
|
||||||
"UNKNOWN_MESSAGE_INDEX",
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message)
|
|
||||||
val detailedReason = String.format(MXCryptoError.DETAILED_OLM_REASON, encryptedEventContent.ciphertext, reason)
|
|
||||||
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.OLM,
|
|
||||||
reason,
|
|
||||||
detailedReason
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (throwable is MXCryptoError.Base) {
|
|
||||||
if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
|
||||||
// Check if it was withheld by sender to enrich error code
|
|
||||||
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
|
||||||
if (withHeldInfo != null) {
|
|
||||||
if (requestKeysOnFail) {
|
|
||||||
requestKeysForEvent(event)
|
|
||||||
}
|
|
||||||
// Encapsulate as withHeld exception
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.KEYS_WITHHELD,
|
|
||||||
withHeldInfo.code?.value ?: "",
|
|
||||||
withHeldInfo.reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestKeysOnFail) {
|
|
||||||
requestKeysForEvent(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw throwable
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper for the real decryptEvent and for _retryDecryption. If
|
|
||||||
* requestKeysOnFail is true, we'll send an m.room_key_request when we fail
|
|
||||||
* to decrypt the event due to missing megolm keys.
|
|
||||||
*
|
|
||||||
* @param event the event
|
|
||||||
*/
|
|
||||||
private fun requestKeysForEvent(event: Event) {
|
|
||||||
outgoingKeyRequestManager.requestKeyForEvent(event, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a key event.
|
|
||||||
*
|
|
||||||
* @param event the key event.
|
|
||||||
*/
|
|
||||||
override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {
|
|
||||||
Timber.tag(loggerTag.value).v("onRoomKeyEvent()")
|
|
||||||
var exportFormat = false
|
|
||||||
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
|
|
||||||
|
|
||||||
var senderKey: String? = event.getSenderKey()
|
|
||||||
var keysClaimed: MutableMap<String, String> = HashMap()
|
|
||||||
val forwardingCurve25519KeyChain: MutableList<String> = ArrayList()
|
|
||||||
|
|
||||||
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.sessionId.isNullOrEmpty() || roomKeyContent.sessionKey.isNullOrEmpty()) {
|
|
||||||
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : Key event is missing fields")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
|
||||||
if (!cryptoStore.isKeyGossipingEnabled()) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.i("onRoomKeyEvent(), ignore forward adding as per crypto config : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
|
|
||||||
val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>()
|
|
||||||
?: return
|
|
||||||
|
|
||||||
forwardedRoomKeyContent.forwardingCurve25519KeyChain?.let {
|
|
||||||
forwardingCurve25519KeyChain.addAll(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (senderKey == null) {
|
|
||||||
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : event is missing sender_key field")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
forwardingCurve25519KeyChain.add(senderKey)
|
|
||||||
|
|
||||||
exportFormat = true
|
|
||||||
senderKey = forwardedRoomKeyContent.senderKey
|
|
||||||
if (null == senderKey) {
|
|
||||||
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : forwarded_room_key event is missing sender_key field")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) {
|
|
||||||
Timber.tag(loggerTag.value).e("forwarded_room_key_event is missing sender_claimed_ed25519_key field")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
|
|
||||||
if (null == senderKey) {
|
|
||||||
Timber.tag(loggerTag.value).e("## onRoomKeyEvent() : key event has no sender key (not encrypted?)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// inherit the claimed ed25519 key from the setup message
|
|
||||||
keysClaimed = event.getKeysClaimed().toMutableMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
|
|
||||||
val addSessionResult = olmDevice.addInboundGroupSession(
|
|
||||||
roomKeyContent.sessionId,
|
|
||||||
roomKeyContent.sessionKey,
|
|
||||||
roomKeyContent.roomId,
|
|
||||||
senderKey,
|
|
||||||
forwardingCurve25519KeyChain,
|
|
||||||
keysClaimed,
|
|
||||||
exportFormat
|
|
||||||
)
|
|
||||||
|
|
||||||
when (addSessionResult) {
|
|
||||||
is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex
|
|
||||||
is MXOlmDevice.AddSessionResult.NotImportedHigherIndex -> addSessionResult.newIndex
|
|
||||||
else -> null
|
|
||||||
}?.let { index ->
|
|
||||||
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
|
||||||
val fromDevice = (event.content?.get("sender_key") as? String)?.let { senderDeviceIdentityKey ->
|
|
||||||
cryptoStore.getUserDeviceList(event.senderId ?: "")
|
|
||||||
?.firstOrNull {
|
|
||||||
it.identityKey() == senderDeviceIdentityKey
|
|
||||||
}
|
|
||||||
}?.deviceId
|
|
||||||
|
|
||||||
outgoingKeyRequestManager.onRoomKeyForwarded(
|
|
||||||
sessionId = roomKeyContent.sessionId,
|
|
||||||
algorithm = roomKeyContent.algorithm ?: "",
|
|
||||||
roomId = roomKeyContent.roomId,
|
|
||||||
senderKey = senderKey,
|
|
||||||
fromIndex = index,
|
|
||||||
fromDevice = fromDevice,
|
|
||||||
event = event)
|
|
||||||
|
|
||||||
cryptoStore.saveIncomingForwardKeyAuditTrail(
|
|
||||||
roomId = roomKeyContent.roomId,
|
|
||||||
sessionId = roomKeyContent.sessionId,
|
|
||||||
senderKey = senderKey,
|
|
||||||
algorithm = roomKeyContent.algorithm ?: "",
|
|
||||||
userId = event.senderId ?: "",
|
|
||||||
deviceId = fromDevice ?: "",
|
|
||||||
chainIndex = index.toLong())
|
|
||||||
|
|
||||||
// The index is used to decide if we cancel sent request or if we wait for a better key
|
|
||||||
outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, senderKey, index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addSessionResult is MXOlmDevice.AddSessionResult.Imported) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}")
|
|
||||||
defaultKeysBackupService.maybeBackupKeys()
|
|
||||||
|
|
||||||
onNewSession(roomKeyContent.roomId, senderKey, roomKeyContent.sessionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the some messages can be decrypted with a new session
|
|
||||||
*
|
|
||||||
* @param roomId the room id where the new Megolm session has been created for, may be null when importing from external sessions
|
|
||||||
* @param senderKey the session sender key
|
|
||||||
* @param sessionId the session id
|
|
||||||
*/
|
|
||||||
fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
|
|
||||||
Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey")
|
|
||||||
newSessionListener?.onNewSession(roomId, senderKey, sessionId)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
|
||||||
|
|
||||||
import dagger.Lazy
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.session.StreamEventsManager
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class MXMegolmDecryptionFactory @Inject constructor(
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val eventsManager: Lazy<StreamEventsManager>
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun create(): MXMegolmDecryption {
|
|
||||||
return MXMegolmDecryption(
|
|
||||||
olmDevice,
|
|
||||||
outgoingKeyRequestManager,
|
|
||||||
cryptoStore,
|
|
||||||
eventsManager)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,515 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.forEach
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.toDebugCount
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.toDebugString
|
|
||||||
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
|
||||||
import org.matrix.android.sdk.internal.util.convertToUTF8
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MXMegolmEncryption", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
internal class MXMegolmEncryption(
|
|
||||||
// The id of the room we will be sending to.
|
|
||||||
private val roomId: String,
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val defaultKeysBackupService: DefaultKeysBackupService,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val deviceListManager: DeviceListManager,
|
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
|
||||||
private val myUserId: String,
|
|
||||||
private val myDeviceId: String,
|
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
|
||||||
private val messageEncrypter: MessageEncrypter,
|
|
||||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
|
||||||
private val clock: Clock,
|
|
||||||
) : IMXEncrypting, IMXGroupEncryption {
|
|
||||||
|
|
||||||
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
|
|
||||||
// that even if this is non-null, it may not be ready for use (in which
|
|
||||||
// case outboundSession.shareOperation will be non-null.)
|
|
||||||
private var outboundSession: MXOutboundSessionInfo? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
// restore existing outbound session if any
|
|
||||||
outboundSession = olmDevice.restoreOutboundGroupSessionForRoom(roomId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default rotation periods
|
|
||||||
// TODO: Make it configurable via parameters
|
|
||||||
// Session rotation periods
|
|
||||||
private var sessionRotationPeriodMsgs: Int = 100
|
|
||||||
private var sessionRotationPeriodMs: Int = 7 * 24 * 3600 * 1000
|
|
||||||
|
|
||||||
override suspend fun encryptEventContent(eventContent: Content,
|
|
||||||
eventType: String,
|
|
||||||
userIds: List<String>): Content {
|
|
||||||
val ts = clock.epochMillis()
|
|
||||||
Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom")
|
|
||||||
val devices = getDevicesInRoom(userIds)
|
|
||||||
Timber.tag(loggerTag.value).d("encrypt event in room=$roomId - devices count in room ${devices.allowedDevices.toDebugCount()}")
|
|
||||||
Timber.tag(loggerTag.value).v("encryptEventContent ${clock.epochMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.toDebugString()}")
|
|
||||||
val outboundSession = ensureOutboundSession(devices.allowedDevices)
|
|
||||||
|
|
||||||
return encryptContent(outboundSession, eventType, eventContent)
|
|
||||||
.also {
|
|
||||||
notifyWithheldForSession(devices.withHeldDevices, outboundSession)
|
|
||||||
// annoyingly we have to serialize again the saved outbound session to store message index :/
|
|
||||||
// if not we would see duplicate message index errors
|
|
||||||
olmDevice.storeOutboundGroupSessionForRoom(roomId, outboundSession.sessionId)
|
|
||||||
Timber.tag(loggerTag.value).d("encrypt event in room=$roomId Finished in ${clock.epochMillis() - ts} millis")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun notifyWithheldForSession(devices: MXUsersDevicesMap<WithHeldCode>, outboundSession: MXOutboundSessionInfo) {
|
|
||||||
// offload to computation thread
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
|
|
||||||
mutableListOf<Pair<UserDevice, WithHeldCode>>().apply {
|
|
||||||
devices.forEach { userId, deviceId, withheldCode ->
|
|
||||||
this.add(UserDevice(userId, deviceId) to withheldCode)
|
|
||||||
}
|
|
||||||
}.groupBy(
|
|
||||||
{ it.second },
|
|
||||||
{ it.first }
|
|
||||||
).forEach { (code, targets) ->
|
|
||||||
notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun discardSessionKey() {
|
|
||||||
outboundSession = null
|
|
||||||
olmDevice.discardOutboundGroupSessionForRoom(roomId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun preshareKey(userIds: List<String>) {
|
|
||||||
val ts = clock.epochMillis()
|
|
||||||
Timber.tag(loggerTag.value).d("preshareKey started in $roomId ...")
|
|
||||||
val devices = getDevicesInRoom(userIds)
|
|
||||||
val outboundSession = ensureOutboundSession(devices.allowedDevices)
|
|
||||||
|
|
||||||
notifyWithheldForSession(devices.withHeldDevices, outboundSession)
|
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).d("preshareKey in $roomId done in ${clock.epochMillis() - ts} millis")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepare a new session.
|
|
||||||
*
|
|
||||||
* @return the session description
|
|
||||||
*/
|
|
||||||
private fun prepareNewSessionInRoom(): MXOutboundSessionInfo {
|
|
||||||
Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() ")
|
|
||||||
val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId)
|
|
||||||
|
|
||||||
val keysClaimedMap = mapOf(
|
|
||||||
"ed25519" to olmDevice.deviceEd25519Key!!
|
|
||||||
)
|
|
||||||
|
|
||||||
olmDevice.addInboundGroupSession(
|
|
||||||
sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!,
|
|
||||||
emptyList(), keysClaimedMap, false
|
|
||||||
)
|
|
||||||
|
|
||||||
defaultKeysBackupService.maybeBackupKeys()
|
|
||||||
|
|
||||||
return MXOutboundSessionInfo(sessionId, SharedWithHelper(roomId, sessionId, cryptoStore), clock)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure the outbound session
|
|
||||||
*
|
|
||||||
* @param devicesInRoom the devices list
|
|
||||||
*/
|
|
||||||
private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): MXOutboundSessionInfo {
|
|
||||||
Timber.tag(loggerTag.value).v("ensureOutboundSession roomId:$roomId")
|
|
||||||
var session = outboundSession
|
|
||||||
if (session == null ||
|
|
||||||
// Need to make a brand new session?
|
|
||||||
session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) ||
|
|
||||||
// Determine if we have shared with anyone we shouldn't have
|
|
||||||
session.sharedWithTooManyDevices(devicesInRoom)) {
|
|
||||||
Timber.tag(loggerTag.value).d("roomId:$roomId Starting new megolm session because we need to rotate.")
|
|
||||||
session = prepareNewSessionInRoom()
|
|
||||||
outboundSession = session
|
|
||||||
}
|
|
||||||
val safeSession = session
|
|
||||||
val shareMap = HashMap<String, MutableList<CryptoDeviceInfo>>()/* userId */
|
|
||||||
val userIds = devicesInRoom.userIds
|
|
||||||
for (userId in userIds) {
|
|
||||||
val deviceIds = devicesInRoom.getUserDeviceIds(userId)
|
|
||||||
for (deviceId in deviceIds!!) {
|
|
||||||
val deviceInfo = devicesInRoom.getObject(userId, deviceId)
|
|
||||||
if (deviceInfo != null && !cryptoStore.getSharedSessionInfo(roomId, safeSession.sessionId, deviceInfo).found) {
|
|
||||||
val devices = shareMap.getOrPut(userId) { ArrayList() }
|
|
||||||
devices.add(deviceInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val devicesCount = shareMap.entries.fold(0) { acc, new -> acc + new.value.size }
|
|
||||||
Timber.tag(loggerTag.value).d("roomId:$roomId found $devicesCount devices without megolm session(${session.sessionId})")
|
|
||||||
shareKey(safeSession, shareMap)
|
|
||||||
return safeSession
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Share the device key to a list of users
|
|
||||||
*
|
|
||||||
* @param session the session info
|
|
||||||
* @param devicesByUsers the devices map
|
|
||||||
*/
|
|
||||||
private suspend fun shareKey(session: MXOutboundSessionInfo,
|
|
||||||
devicesByUsers: Map<String, List<CryptoDeviceInfo>>) {
|
|
||||||
// nothing to send, the task is done
|
|
||||||
if (devicesByUsers.isEmpty()) {
|
|
||||||
Timber.tag(loggerTag.value).v("shareKey() : nothing more to do")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user)
|
|
||||||
val subMap = HashMap<String, List<CryptoDeviceInfo>>()
|
|
||||||
var devicesCount = 0
|
|
||||||
for ((userId, devices) in devicesByUsers) {
|
|
||||||
subMap[userId] = devices
|
|
||||||
devicesCount += devices.size
|
|
||||||
if (devicesCount > 100) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Timber.tag(loggerTag.value).v("shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}")
|
|
||||||
shareUserDevicesKey(session, subMap)
|
|
||||||
val remainingDevices = devicesByUsers - subMap.keys
|
|
||||||
shareKey(session, remainingDevices)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Share the device keys of a an user
|
|
||||||
*
|
|
||||||
* @param session the session info
|
|
||||||
* @param devicesByUser the devices map
|
|
||||||
*/
|
|
||||||
private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo,
|
|
||||||
devicesByUser: Map<String, List<CryptoDeviceInfo>>) {
|
|
||||||
val sessionKey = olmDevice.getSessionKey(session.sessionId)
|
|
||||||
val chainIndex = olmDevice.getMessageIndex(session.sessionId)
|
|
||||||
|
|
||||||
val submap = HashMap<String, Any>()
|
|
||||||
submap["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM
|
|
||||||
submap["room_id"] = roomId
|
|
||||||
submap["session_id"] = session.sessionId
|
|
||||||
submap["session_key"] = sessionKey!!
|
|
||||||
submap["chain_index"] = chainIndex
|
|
||||||
|
|
||||||
val payload = HashMap<String, Any>()
|
|
||||||
payload["type"] = EventType.ROOM_KEY
|
|
||||||
payload["content"] = submap
|
|
||||||
|
|
||||||
var t0 = clock.epochMillis()
|
|
||||||
Timber.tag(loggerTag.value).v("shareUserDevicesKey() : starts")
|
|
||||||
|
|
||||||
val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
|
||||||
Timber.tag(loggerTag.value).v(
|
|
||||||
"""shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${clock.epochMillis() - t0} ms"""
|
|
||||||
.trimMargin()
|
|
||||||
)
|
|
||||||
val contentMap = MXUsersDevicesMap<Any>()
|
|
||||||
var haveTargets = false
|
|
||||||
val userIds = results.userIds
|
|
||||||
val noOlmToNotify = mutableListOf<UserDevice>()
|
|
||||||
for (userId in userIds) {
|
|
||||||
val devicesToShareWith = devicesByUser[userId]
|
|
||||||
for ((deviceID) in devicesToShareWith!!) {
|
|
||||||
val sessionResult = results.getObject(userId, deviceID)
|
|
||||||
if (sessionResult?.sessionId == null) {
|
|
||||||
// no session with this device, probably because there
|
|
||||||
// were no one-time keys.
|
|
||||||
|
|
||||||
// MSC 2399
|
|
||||||
// send withheld m.no_olm: an olm session could not be established.
|
|
||||||
// This may happen, for example, if the sender was unable to obtain a one-time key from the recipient.
|
|
||||||
Timber.tag(loggerTag.value).v("shareUserDevicesKey() : No Olm Session for $userId:$deviceID mark for withheld")
|
|
||||||
noOlmToNotify.add(UserDevice(userId, deviceID))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
Timber.tag(loggerTag.value).v("shareUserDevicesKey() : Add to share keys contentMap for $userId:$deviceID")
|
|
||||||
contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo)))
|
|
||||||
haveTargets = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the devices we have shared with to session.sharedWithDevices.
|
|
||||||
// we deliberately iterate over devicesByUser (ie, the devices we
|
|
||||||
// attempted to share with) rather than the contentMap (those we did
|
|
||||||
// share with), because we don't want to try to claim a one-time-key
|
|
||||||
// for dead devices on every message.
|
|
||||||
for ((_, devicesToShareWith) in devicesByUser) {
|
|
||||||
for (deviceInfo in devicesToShareWith) {
|
|
||||||
session.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex)
|
|
||||||
// XXX is it needed to add it to the audit trail?
|
|
||||||
// For now decided that no, we are more interested by forward trail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (haveTargets) {
|
|
||||||
t0 = clock.epochMillis()
|
|
||||||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target")
|
|
||||||
Timber.tag(loggerTag.value).d("sending to device room key for ${session.sessionId} to ${contentMap.toDebugString()}")
|
|
||||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
|
|
||||||
try {
|
|
||||||
withContext(coroutineDispatchers.io) {
|
|
||||||
sendToDeviceTask.execute(sendToDeviceParams)
|
|
||||||
}
|
|
||||||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${clock.epochMillis() - t0} ms")
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
// What to do here...
|
|
||||||
Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${session.sessionId}>")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (noOlmToNotify.isNotEmpty()) {
|
|
||||||
// XXX offload?, as they won't read the message anyhow?
|
|
||||||
notifyKeyWithHeld(
|
|
||||||
noOlmToNotify,
|
|
||||||
session.sessionId,
|
|
||||||
olmDevice.deviceCurve25519Key,
|
|
||||||
WithHeldCode.NO_OLM
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun notifyKeyWithHeld(targets: List<UserDevice>,
|
|
||||||
sessionId: String,
|
|
||||||
senderKey: String?,
|
|
||||||
code: WithHeldCode) {
|
|
||||||
Timber.tag(loggerTag.value).d(
|
|
||||||
"notifyKeyWithHeld() :sending withheld for session:$sessionId and code $code to" +
|
|
||||||
" ${targets.joinToString { "${it.userId}|${it.deviceId}" }}"
|
|
||||||
)
|
|
||||||
val withHeldContent = RoomKeyWithHeldContent(
|
|
||||||
roomId = roomId,
|
|
||||||
senderKey = senderKey,
|
|
||||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
|
||||||
sessionId = sessionId,
|
|
||||||
codeString = code.value,
|
|
||||||
fromDevice = myDeviceId
|
|
||||||
)
|
|
||||||
val params = SendToDeviceTask.Params(
|
|
||||||
EventType.ROOM_KEY_WITHHELD,
|
|
||||||
MXUsersDevicesMap<Any>().apply {
|
|
||||||
targets.forEach {
|
|
||||||
setObject(it.userId, it.deviceId, withHeldContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
withContext(coroutineDispatchers.io) {
|
|
||||||
sendToDeviceTask.execute(params)
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.e("notifyKeyWithHeld() :$sessionId Failed to send withheld ${targets.map { "${it.userId}|${it.deviceId}" }}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* process the pending encryptions
|
|
||||||
*/
|
|
||||||
private fun encryptContent(session: MXOutboundSessionInfo, eventType: String, eventContent: Content): Content {
|
|
||||||
// Everything is in place, encrypt all pending events
|
|
||||||
val payloadJson = HashMap<String, Any>()
|
|
||||||
payloadJson["room_id"] = roomId
|
|
||||||
payloadJson["type"] = eventType
|
|
||||||
payloadJson["content"] = eventContent
|
|
||||||
|
|
||||||
// Get canonical Json from
|
|
||||||
|
|
||||||
val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson))
|
|
||||||
val ciphertext = olmDevice.encryptGroupMessage(session.sessionId, payloadString)
|
|
||||||
|
|
||||||
val map = HashMap<String, Any>()
|
|
||||||
map["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM
|
|
||||||
map["sender_key"] = olmDevice.deviceCurve25519Key!!
|
|
||||||
map["ciphertext"] = ciphertext!!
|
|
||||||
map["session_id"] = session.sessionId
|
|
||||||
|
|
||||||
// Include our device ID so that recipients can send us a
|
|
||||||
// m.new_device message if they don't have our session key.
|
|
||||||
map["device_id"] = myDeviceId
|
|
||||||
session.useCount++
|
|
||||||
return map
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the list of devices which can encrypt data to.
|
|
||||||
* This method must be called in getDecryptingThreadHandler() thread.
|
|
||||||
*
|
|
||||||
* @param userIds the user ids whose devices must be checked.
|
|
||||||
*/
|
|
||||||
private suspend fun getDevicesInRoom(userIds: List<String>): DeviceInRoomInfo {
|
|
||||||
// We are happy to use a cached version here: we assume that if we already
|
|
||||||
// have a list of the user's devices, then we already share an e2e room
|
|
||||||
// with them, which means that they will have announced any new devices via
|
|
||||||
// an m.new_device.
|
|
||||||
val keys = deviceListManager.downloadKeys(userIds, false)
|
|
||||||
val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() ||
|
|
||||||
cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId)
|
|
||||||
|
|
||||||
val devicesInRoom = DeviceInRoomInfo()
|
|
||||||
val unknownDevices = MXUsersDevicesMap<CryptoDeviceInfo>()
|
|
||||||
|
|
||||||
for (userId in keys.userIds) {
|
|
||||||
val deviceIds = keys.getUserDeviceIds(userId) ?: continue
|
|
||||||
for (deviceId in deviceIds) {
|
|
||||||
val deviceInfo = keys.getObject(userId, deviceId) ?: continue
|
|
||||||
if (warnOnUnknownDevicesRepository.warnOnUnknownDevices() && deviceInfo.isUnknown) {
|
|
||||||
// The device is not yet known by the user
|
|
||||||
unknownDevices.setObject(userId, deviceId, deviceInfo)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (deviceInfo.isBlocked) {
|
|
||||||
// Remove any blocked devices
|
|
||||||
devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.BLACKLISTED)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) {
|
|
||||||
devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.UNVERIFIED)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deviceInfo.identityKey() == olmDevice.deviceCurve25519Key) {
|
|
||||||
// Don't bother sending to ourself
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
devicesInRoom.allowedDevices.setObject(userId, deviceId, deviceInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (unknownDevices.isEmpty) {
|
|
||||||
return devicesInRoom
|
|
||||||
} else {
|
|
||||||
throw MXCryptoError.UnknownDevice(unknownDevices)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun reshareKey(groupSessionId: String,
|
|
||||||
userId: String,
|
|
||||||
deviceId: String,
|
|
||||||
senderKey: String): Boolean {
|
|
||||||
Timber.tag(loggerTag.value).i("process reshareKey for $groupSessionId to $userId:$deviceId")
|
|
||||||
val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false
|
|
||||||
.also { Timber.tag(loggerTag.value).w("reshareKey: Device not found") }
|
|
||||||
|
|
||||||
// Get the chain index of the key we previously sent this device
|
|
||||||
val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, groupSessionId, deviceInfo)
|
|
||||||
if (!wasSessionSharedWithUser.found) {
|
|
||||||
// This session was never shared with this user
|
|
||||||
// Send a room key with held
|
|
||||||
notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), groupSessionId, senderKey, WithHeldCode.UNAUTHORISED)
|
|
||||||
Timber.tag(loggerTag.value).w("reshareKey: ERROR : Never shared megolm with this device")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// if found chain index should not be null
|
|
||||||
val chainIndex = wasSessionSharedWithUser.chainIndex ?: return false
|
|
||||||
.also {
|
|
||||||
Timber.tag(loggerTag.value).w("reshareKey: Null chain index")
|
|
||||||
}
|
|
||||||
|
|
||||||
val devicesByUser = mapOf(userId to listOf(deviceInfo))
|
|
||||||
val usersDeviceMap = try {
|
|
||||||
ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId)
|
|
||||||
if (olmSessionResult?.sessionId == null) {
|
|
||||||
Timber.tag(loggerTag.value).w("reshareKey: no session with this device, probably because there were no one-time keys")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
Timber.tag(loggerTag.value).i(" reshareKey: $groupSessionId:$chainIndex with device $userId:$deviceId using session ${olmSessionResult.sessionId}")
|
|
||||||
|
|
||||||
val sessionHolder = try {
|
|
||||||
olmDevice.getInboundGroupSession(groupSessionId, senderKey, roomId)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session $groupSessionId")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val export = sessionHolder.mutex.withLock {
|
|
||||||
sessionHolder.wrapper.exportKeys()
|
|
||||||
} ?: return false.also {
|
|
||||||
Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session $groupSessionId")
|
|
||||||
}
|
|
||||||
|
|
||||||
val payloadJson = mapOf(
|
|
||||||
"type" to EventType.FORWARDED_ROOM_KEY,
|
|
||||||
"content" to export
|
|
||||||
)
|
|
||||||
|
|
||||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
|
||||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
|
||||||
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
|
||||||
Timber.tag(loggerTag.value).i("reshareKey() : sending session $groupSessionId to $userId:$deviceId")
|
|
||||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
|
||||||
return try {
|
|
||||||
sendToDeviceTask.execute(sendToDeviceParams)
|
|
||||||
Timber.tag(loggerTag.value).i("reshareKey() : successfully send <$groupSessionId> to $userId:$deviceId")
|
|
||||||
true
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).e(failure, "reshareKey() : fail to send <$groupSessionId> to $userId:$deviceId")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DeviceInRoomInfo(
|
|
||||||
val allowedDevices: MXUsersDevicesMap<CryptoDeviceInfo> = MXUsersDevicesMap(),
|
|
||||||
val withHeldDevices: MXUsersDevicesMap<WithHeldCode> = MXUsersDevicesMap()
|
|
||||||
)
|
|
||||||
|
|
||||||
data class UserDevice(
|
|
||||||
val userId: String,
|
|
||||||
val deviceId: String
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
|
||||||
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.di.DeviceId
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class MXMegolmEncryptionFactory @Inject constructor(
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
// private val defaultKeysBackupService: DefaultKeysBackupService,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val deviceListManager: DeviceListManager,
|
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
|
||||||
@UserId private val userId: String,
|
|
||||||
@DeviceId private val deviceId: String?,
|
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
|
||||||
private val messageEncrypter: MessageEncrypter,
|
|
||||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
|
||||||
private val clock: Clock,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun create(roomId: String): MXMegolmEncryption {
|
|
||||||
return MXMegolmEncryption(
|
|
||||||
roomId = roomId,
|
|
||||||
olmDevice = olmDevice,
|
|
||||||
// defaultKeysBackupService = defaultKeysBackupService,
|
|
||||||
cryptoStore = cryptoStore,
|
|
||||||
deviceListManager = deviceListManager,
|
|
||||||
ensureOlmSessionsForDevicesAction = ensureOlmSessionsForDevicesAction,
|
|
||||||
myUserId = userId,
|
|
||||||
myDeviceId = deviceId!!,
|
|
||||||
sendToDeviceTask = sendToDeviceTask,
|
|
||||||
messageEncrypter = messageEncrypter,
|
|
||||||
warnOnUnknownDevicesRepository = warnOnUnknownDevicesRepository,
|
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
|
||||||
cryptoCoroutineScope = cryptoCoroutineScope,
|
|
||||||
clock = clock,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
internal class MXOutboundSessionInfo(
|
|
||||||
// The id of the session
|
|
||||||
val sessionId: String,
|
|
||||||
val sharedWithHelper: SharedWithHelper,
|
|
||||||
private val clock: Clock,
|
|
||||||
// When the session was created
|
|
||||||
private val creationTime: Long = clock.epochMillis(),
|
|
||||||
) {
|
|
||||||
|
|
||||||
// Number of times this session has been used
|
|
||||||
var useCount: Int = 0
|
|
||||||
|
|
||||||
fun needsRotation(rotationPeriodMsgs: Int, rotationPeriodMs: Int): Boolean {
|
|
||||||
var needsRotation = false
|
|
||||||
val sessionLifetime = clock.epochMillis() - creationTime
|
|
||||||
|
|
||||||
if (useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) {
|
|
||||||
Timber.v("## needsRotation() : Rotating megolm session after $useCount, ${sessionLifetime}ms")
|
|
||||||
needsRotation = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return needsRotation
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if this session has been shared with devices which it shouldn't have been.
|
|
||||||
*
|
|
||||||
* @param devicesInRoom the devices map
|
|
||||||
* @return true if we have shared the session with devices which aren't in devicesInRoom.
|
|
||||||
*/
|
|
||||||
fun sharedWithTooManyDevices(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): Boolean {
|
|
||||||
val sharedWithDevices = sharedWithHelper.sharedWithDevices()
|
|
||||||
val userIds = sharedWithDevices.userIds
|
|
||||||
|
|
||||||
for (userId in userIds) {
|
|
||||||
if (null == devicesInRoom.getUserDeviceIds(userId)) {
|
|
||||||
Timber.v("## sharedWithTooManyDevices() : Starting new session because we shared with $userId")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
val deviceIds = sharedWithDevices.getUserDeviceIds(userId)
|
|
||||||
|
|
||||||
for (deviceId in deviceIds!!) {
|
|
||||||
if (null == devicesInRoom.getObject(userId, deviceId)) {
|
|
||||||
Timber.v("## sharedWithTooManyDevices() : Starting new session because we shared with $userId:$deviceId")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
|
|
||||||
internal class SharedWithHelper(
|
|
||||||
private val roomId: String,
|
|
||||||
private val sessionId: String,
|
|
||||||
private val cryptoStore: IMXCryptoStore) {
|
|
||||||
|
|
||||||
fun sharedWithDevices(): MXUsersDevicesMap<Int> {
|
|
||||||
return cryptoStore.getSharedWithInfo(roomId, sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun markedSessionAsShared(deviceInfo: CryptoDeviceInfo, chainIndex: Int) {
|
|
||||||
cryptoStore.markedSessionAsShared(
|
|
||||||
roomId = roomId,
|
|
||||||
sessionId = sessionId,
|
|
||||||
userId = deviceInfo.userId,
|
|
||||||
deviceId = deviceInfo.deviceId,
|
|
||||||
deviceIdentityKey = deviceInfo.identityKey() ?: "",
|
|
||||||
chainIndex = chainIndex
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,268 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.algorithms.olm
|
|
||||||
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.OlmPayloadContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
|
||||||
import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
|
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
|
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
|
||||||
import org.matrix.android.sdk.internal.util.convertFromUTF8
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MXOlmDecryption", LoggerTag.CRYPTO)
|
|
||||||
|
|
||||||
internal class MXOlmDecryption(
|
|
||||||
// The olm device interface
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
// the matrix userId
|
|
||||||
private val userId: String) :
|
|
||||||
IMXDecrypting {
|
|
||||||
|
|
||||||
@Throws(MXCryptoError::class)
|
|
||||||
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
|
||||||
val olmEventContent = event.content.toModel<OlmEventContent>() ?: run {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent() : bad event format")
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.BAD_EVENT_FORMAT,
|
|
||||||
MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val cipherText = olmEventContent.ciphertext ?: run {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent() : missing cipher text")
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.MISSING_CIPHER_TEXT,
|
|
||||||
MXCryptoError.MISSING_CIPHER_TEXT_REASON
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val senderKey = olmEventContent.senderKey ?: run {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent() : missing sender key")
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.MISSING_SENDER_KEY,
|
|
||||||
MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The message for myUser
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val message = messageAny as JsonDict
|
|
||||||
|
|
||||||
val decryptedPayload = decryptMessage(message, senderKey)
|
|
||||||
|
|
||||||
if (decryptedPayload == null) {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
|
|
||||||
}
|
|
||||||
val payloadString = convertFromUTF8(decryptedPayload)
|
|
||||||
|
|
||||||
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
|
|
||||||
val payload = adapter.fromJson(payloadString)
|
|
||||||
|
|
||||||
if (payload == null) {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent failed : null payload")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON)
|
|
||||||
}
|
|
||||||
|
|
||||||
val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent() : bad olmPayloadContent format")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (olmPayloadContent.recipient.isNullOrBlank()) {
|
|
||||||
val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient")
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent() : $reason")
|
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (olmPayloadContent.recipient != userId) {
|
|
||||||
Timber.tag(loggerTag.value).e(
|
|
||||||
"## decryptEvent() : Event ${event.eventId}:" +
|
|
||||||
" Intended recipient ${olmPayloadContent.recipient} does not match our id $userId"
|
|
||||||
)
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.BAD_RECIPIENT,
|
|
||||||
String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val recipientKeys = olmPayloadContent.recipientKeys ?: run {
|
|
||||||
Timber.tag(loggerTag.value).e(
|
|
||||||
"## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" +
|
|
||||||
" property; cannot prevent unknown-key attack"
|
|
||||||
)
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.MISSING_PROPERTY,
|
|
||||||
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val ed25519 = recipientKeys["ed25519"]
|
|
||||||
|
|
||||||
if (ed25519 != olmDevice.deviceEd25519Key) {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours")
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.BAD_RECIPIENT_KEY,
|
|
||||||
MXCryptoError.BAD_RECIPIENT_KEY_REASON
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (olmPayloadContent.sender.isNullOrBlank()) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack")
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.MISSING_PROPERTY,
|
|
||||||
String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (olmPayloadContent.sender != event.senderId) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.e("Event ${event.eventId}: sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}")
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.FORWARDED_MESSAGE,
|
|
||||||
String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (olmPayloadContent.roomId != event.roomId) {
|
|
||||||
Timber.tag(loggerTag.value)
|
|
||||||
.e("## decryptEvent() : Event ${event.eventId}: room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}")
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.BAD_ROOM,
|
|
||||||
String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val keys = olmPayloadContent.keys ?: run {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptEvent failed : null keys")
|
|
||||||
throw MXCryptoError.Base(
|
|
||||||
MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
|
|
||||||
MXCryptoError.MISSING_CIPHER_TEXT_REASON
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return MXEventDecryptionResult(
|
|
||||||
clearEvent = payload,
|
|
||||||
senderCurve25519Key = senderKey,
|
|
||||||
claimedEd25519Key = keys["ed25519"]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to decrypt an Olm message.
|
|
||||||
*
|
|
||||||
* @param theirDeviceIdentityKey the Curve25519 identity key of the sender.
|
|
||||||
* @param message message object, with 'type' and 'body' fields.
|
|
||||||
* @return payload, if decrypted successfully.
|
|
||||||
*/
|
|
||||||
private suspend fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
|
|
||||||
val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey)
|
|
||||||
|
|
||||||
val messageBody = message["body"] as? String ?: return null
|
|
||||||
val messageType = when (val typeAsVoid = message["type"]) {
|
|
||||||
is Double -> typeAsVoid.toInt()
|
|
||||||
is Int -> typeAsVoid
|
|
||||||
is Long -> typeAsVoid.toInt()
|
|
||||||
else -> return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try each session in turn
|
|
||||||
// decryptionErrors = {};
|
|
||||||
|
|
||||||
val isPreKey = messageType == 0
|
|
||||||
// we want to synchronize on prekey if not we could end up create two olm sessions
|
|
||||||
// Not very clear but it looks like the js-sdk for consistency
|
|
||||||
return if (isPreKey) {
|
|
||||||
olmDevice.mutex.withLock {
|
|
||||||
reallyDecryptMessage(sessionIds, messageBody, messageType, theirDeviceIdentityKey)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reallyDecryptMessage(sessionIds, messageBody, messageType, theirDeviceIdentityKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun reallyDecryptMessage(sessionIds: List<String>, messageBody: String, messageType: Int, theirDeviceIdentityKey: String): String? {
|
|
||||||
Timber.tag(loggerTag.value).d("decryptMessage() try to decrypt olm message type:$messageType from ${sessionIds.size} known sessions")
|
|
||||||
for (sessionId in sessionIds) {
|
|
||||||
val payload = try {
|
|
||||||
olmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey)
|
|
||||||
} catch (throwable: Exception) {
|
|
||||||
// As we are trying one by one, we don't really care of the error here
|
|
||||||
Timber.tag(loggerTag.value).d("decryptMessage() failed with session $sessionId")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null != payload) {
|
|
||||||
Timber.tag(loggerTag.value).v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId")
|
|
||||||
return payload
|
|
||||||
} else {
|
|
||||||
val foundSession = olmDevice.matchesSession(theirDeviceIdentityKey, sessionId, messageType, messageBody)
|
|
||||||
|
|
||||||
if (foundSession) {
|
|
||||||
// Decryption failed, but it was a prekey message matching this
|
|
||||||
// session, so it should have worked.
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting prekey message with existing session id $sessionId:TODO")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messageType != 0) {
|
|
||||||
// not a prekey message, so it should have matched an existing session, but it
|
|
||||||
// didn't work.
|
|
||||||
|
|
||||||
if (sessionIds.isEmpty()) {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptMessage() : No existing sessions")
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// prekey message which doesn't match any existing sessions: make a new
|
|
||||||
// session.
|
|
||||||
// XXXX Possible races here? if concurrent access for same prekey message, we might create 2 sessions?
|
|
||||||
Timber.tag(loggerTag.value).d("## decryptMessage() : Create inbound group session from prekey sender:$theirDeviceIdentityKey")
|
|
||||||
|
|
||||||
val res = olmDevice.createInboundSession(theirDeviceIdentityKey, messageType, messageBody)
|
|
||||||
|
|
||||||
if (null == res) {
|
|
||||||
Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).v("## decryptMessage() : Created new inbound Olm session get id ${res["session_id"]} with $theirDeviceIdentityKey")
|
|
||||||
|
|
||||||
return res["payload"]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.algorithms.olm
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForUsersAction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
|
|
||||||
internal class MXOlmEncryption(
|
|
||||||
private val roomId: String,
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val messageEncrypter: MessageEncrypter,
|
|
||||||
private val deviceListManager: DeviceListManager,
|
|
||||||
private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction) :
|
|
||||||
IMXEncrypting {
|
|
||||||
|
|
||||||
override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content {
|
|
||||||
// pick the list of recipients based on the membership list.
|
|
||||||
//
|
|
||||||
// TODO: there is a race condition here! What if a new user turns up
|
|
||||||
ensureSession(userIds)
|
|
||||||
val deviceInfos = ArrayList<CryptoDeviceInfo>()
|
|
||||||
for (userId in userIds) {
|
|
||||||
val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
|
|
||||||
for (device in devices) {
|
|
||||||
val key = device.identityKey()
|
|
||||||
if (key == olmDevice.deviceCurve25519Key) {
|
|
||||||
// Don't bother setting up session to ourself
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (device.isBlocked) {
|
|
||||||
// Don't bother setting up sessions with blocked users
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
deviceInfos.add(device)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val messageMap = mapOf(
|
|
||||||
"room_id" to roomId,
|
|
||||||
"type" to eventType,
|
|
||||||
"content" to eventContent
|
|
||||||
)
|
|
||||||
|
|
||||||
messageEncrypter.encryptMessage(messageMap, deviceInfos)
|
|
||||||
return messageMap.toContent()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure that the session
|
|
||||||
*
|
|
||||||
* @param users the user ids list
|
|
||||||
*/
|
|
||||||
private suspend fun ensureSession(users: List<String>) {
|
|
||||||
deviceListManager.downloadKeys(users, false)
|
|
||||||
ensureOlmSessionsForUsersAction.handle(users)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.algorithms.olm
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForUsersAction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class MXOlmEncryptionFactory @Inject constructor(private val olmDevice: MXOlmDevice,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val messageEncrypter: MessageEncrypter,
|
|
||||||
private val deviceListManager: DeviceListManager,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction) {
|
|
||||||
|
|
||||||
fun create(roomId: String): MXOlmEncryption {
|
|
||||||
return MXOlmEncryption(
|
|
||||||
roomId,
|
|
||||||
olmDevice,
|
|
||||||
cryptoStore,
|
|
||||||
messageEncrypter,
|
|
||||||
deviceListManager,
|
|
||||||
ensureOlmSessionsForUsersAction
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,93 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.crosssigning
|
|
||||||
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import org.matrix.android.sdk.internal.task.Task
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal interface ComputeTrustTask : Task<ComputeTrustTask.Params, RoomEncryptionTrustLevel> {
|
|
||||||
data class Params(
|
|
||||||
val activeMemberUserIds: List<String>,
|
|
||||||
val isDirectRoom: Boolean
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DefaultComputeTrustTask @Inject constructor(
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
@UserId private val userId: String,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
|
||||||
) : ComputeTrustTask {
|
|
||||||
|
|
||||||
override suspend fun execute(params: ComputeTrustTask.Params): RoomEncryptionTrustLevel = withContext(coroutineDispatchers.crypto) {
|
|
||||||
// The set of “all users” depends on the type of room:
|
|
||||||
// For regular / topic rooms, all users including yourself, are considered when decorating a room
|
|
||||||
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
|
|
||||||
val listToCheck = if (params.isDirectRoom) {
|
|
||||||
params.activeMemberUserIds.filter { it != userId }
|
|
||||||
} else {
|
|
||||||
params.activeMemberUserIds
|
|
||||||
}
|
|
||||||
|
|
||||||
val allTrustedUserIds = listToCheck
|
|
||||||
.filter { userId -> getUserCrossSigningKeys(userId)?.isTrusted() == true }
|
|
||||||
|
|
||||||
if (allTrustedUserIds.isEmpty()) {
|
|
||||||
RoomEncryptionTrustLevel.Default
|
|
||||||
} else {
|
|
||||||
// If one of the verified user as an untrusted device -> warning
|
|
||||||
// If all devices of all verified users are trusted -> green
|
|
||||||
// else -> black
|
|
||||||
allTrustedUserIds
|
|
||||||
.mapNotNull { cryptoStore.getUserDeviceList(it) }
|
|
||||||
.flatten()
|
|
||||||
.let { allDevices ->
|
|
||||||
if (getMyCrossSigningKeys() != null) {
|
|
||||||
allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() }
|
|
||||||
} else {
|
|
||||||
// Legacy method
|
|
||||||
allDevices.any { !it.isVerified }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.let { hasWarning ->
|
|
||||||
if (hasWarning) {
|
|
||||||
RoomEncryptionTrustLevel.Warning
|
|
||||||
} else {
|
|
||||||
if (listToCheck.size == allTrustedUserIds.size) {
|
|
||||||
// all users are trusted and all devices are verified
|
|
||||||
RoomEncryptionTrustLevel.Trusted
|
|
||||||
} else {
|
|
||||||
RoomEncryptionTrustLevel.Default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? {
|
|
||||||
return cryptoStore.getCrossSigningInfo(otherUserId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMyCrossSigningKeys(): MXCrossSigningInfo? {
|
|
||||||
return cryptoStore.getMyCrossSigningInfo()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,806 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.crosssigning
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.work.BackoffPolicy
|
|
||||||
import androidx.work.ExistingWorkPolicy
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustResult
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.UserTrustResult
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerified
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.isLocallyVerified
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
|
||||||
import org.matrix.android.sdk.api.util.fromBase64
|
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask
|
|
||||||
import org.matrix.android.sdk.internal.di.SessionId
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
|
||||||
import org.matrix.android.sdk.internal.task.TaskThread
|
|
||||||
import org.matrix.android.sdk.internal.task.configureWith
|
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
|
||||||
import org.matrix.android.sdk.internal.util.logLimit
|
|
||||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
|
||||||
import org.matrix.olm.OlmPkSigning
|
|
||||||
import org.matrix.olm.OlmUtility
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@SessionScope
|
|
||||||
internal class DefaultCrossSigningService @Inject constructor(
|
|
||||||
@UserId private val userId: String,
|
|
||||||
@SessionId private val sessionId: String,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val deviceListManager: DeviceListManager,
|
|
||||||
private val initializeCrossSigningTask: InitializeCrossSigningTask,
|
|
||||||
private val uploadSignaturesTask: UploadSignaturesTask,
|
|
||||||
private val taskExecutor: TaskExecutor,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
|
||||||
private val workManagerProvider: WorkManagerProvider,
|
|
||||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
|
||||||
private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
|
|
||||||
) : CrossSigningService,
|
|
||||||
DeviceListManager.UserDevicesUpdateListener {
|
|
||||||
|
|
||||||
private var olmUtility: OlmUtility? = null
|
|
||||||
|
|
||||||
private var masterPkSigning: OlmPkSigning? = null
|
|
||||||
private var userPkSigning: OlmPkSigning? = null
|
|
||||||
private var selfSigningPkSigning: OlmPkSigning? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
try {
|
|
||||||
olmUtility = OlmUtility()
|
|
||||||
|
|
||||||
// Try to get stored keys if they exist
|
|
||||||
cryptoStore.getMyCrossSigningInfo()?.let { mxCrossSigningInfo ->
|
|
||||||
Timber.i("## CrossSigning - Found Existing self signed keys")
|
|
||||||
Timber.i("## CrossSigning - Checking if private keys are known")
|
|
||||||
|
|
||||||
cryptoStore.getCrossSigningPrivateKeys()?.let { privateKeysInfo ->
|
|
||||||
privateKeysInfo.master
|
|
||||||
?.fromBase64()
|
|
||||||
?.let { privateKeySeed ->
|
|
||||||
val pkSigning = OlmPkSigning()
|
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
|
|
||||||
masterPkSigning = pkSigning
|
|
||||||
Timber.i("## CrossSigning - Loading master key success")
|
|
||||||
} else {
|
|
||||||
Timber.w("## CrossSigning - Public master key does not match the private key")
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
// TODO untrust?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
privateKeysInfo.user
|
|
||||||
?.fromBase64()
|
|
||||||
?.let { privateKeySeed ->
|
|
||||||
val pkSigning = OlmPkSigning()
|
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
|
|
||||||
userPkSigning = pkSigning
|
|
||||||
Timber.i("## CrossSigning - Loading User Signing key success")
|
|
||||||
} else {
|
|
||||||
Timber.w("## CrossSigning - Public User key does not match the private key")
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
// TODO untrust?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
privateKeysInfo.selfSigned
|
|
||||||
?.fromBase64()
|
|
||||||
?.let { privateKeySeed ->
|
|
||||||
val pkSigning = OlmPkSigning()
|
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
|
|
||||||
selfSigningPkSigning = pkSigning
|
|
||||||
Timber.i("## CrossSigning - Loading Self Signing key success")
|
|
||||||
} else {
|
|
||||||
Timber.w("## CrossSigning - Public Self Signing key does not match the private key")
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
// TODO untrust?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recover local trust in case private key are there?
|
|
||||||
setUserKeysAsTrusted(userId, checkUserTrust(userId).isVerified())
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
// Mmm this kind of a big issue
|
|
||||||
Timber.e(e, "Failed to initialize Cross Signing")
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceListManager.addListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun release() {
|
|
||||||
olmUtility?.releaseUtility()
|
|
||||||
listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() }
|
|
||||||
deviceListManager.removeListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun finalize() {
|
|
||||||
release()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* - Make 3 key pairs (MSK, USK, SSK)
|
|
||||||
* - Save the private keys with proper security
|
|
||||||
* - Sign the keys and upload them
|
|
||||||
* - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures
|
|
||||||
*/
|
|
||||||
override fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?, callback: MatrixCallback<Unit>) {
|
|
||||||
Timber.d("## CrossSigning initializeCrossSigning")
|
|
||||||
|
|
||||||
val params = InitializeCrossSigningTask.Params(
|
|
||||||
interactiveAuthInterceptor = uiaInterceptor
|
|
||||||
)
|
|
||||||
initializeCrossSigningTask.configureWith(params) {
|
|
||||||
this.callbackThread = TaskThread.CRYPTO
|
|
||||||
this.callback = object : MatrixCallback<InitializeCrossSigningTask.Result> {
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
Timber.e(failure, "Error in initializeCrossSigning()")
|
|
||||||
callback.onFailure(failure)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(data: InitializeCrossSigningTask.Result) {
|
|
||||||
val crossSigningInfo = MXCrossSigningInfo(userId, listOf(data.masterKeyInfo, data.userKeyInfo, data.selfSignedKeyInfo))
|
|
||||||
cryptoStore.setMyCrossSigningInfo(crossSigningInfo)
|
|
||||||
setUserKeysAsTrusted(userId, true)
|
|
||||||
cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK)
|
|
||||||
masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) }
|
|
||||||
userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) }
|
|
||||||
selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) }
|
|
||||||
|
|
||||||
callback.onSuccess(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.executeBy(taskExecutor)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSecretMSKGossip(mskPrivateKey: String) {
|
|
||||||
Timber.i("## CrossSigning - onSecretSSKGossip")
|
|
||||||
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also {
|
|
||||||
Timber.e("## CrossSigning - onSecretMSKGossip() received secret but public key is not known")
|
|
||||||
}
|
|
||||||
|
|
||||||
mskPrivateKey.fromBase64()
|
|
||||||
.let { privateKeySeed ->
|
|
||||||
val pkSigning = OlmPkSigning()
|
|
||||||
try {
|
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
|
|
||||||
masterPkSigning?.releaseSigning()
|
|
||||||
masterPkSigning = pkSigning
|
|
||||||
Timber.i("## CrossSigning - Loading MSK success")
|
|
||||||
cryptoStore.storeMSKPrivateKey(mskPrivateKey)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
Timber.e("## CrossSigning - onSecretMSKGossip() private key do not match public key")
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.e("## CrossSigning - onSecretMSKGossip() ${failure.localizedMessage}")
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSecretSSKGossip(sskPrivateKey: String) {
|
|
||||||
Timber.i("## CrossSigning - onSecretSSKGossip")
|
|
||||||
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also {
|
|
||||||
Timber.e("## CrossSigning - onSecretSSKGossip() received secret but public key is not known")
|
|
||||||
}
|
|
||||||
|
|
||||||
sskPrivateKey.fromBase64()
|
|
||||||
.let { privateKeySeed ->
|
|
||||||
val pkSigning = OlmPkSigning()
|
|
||||||
try {
|
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
|
|
||||||
selfSigningPkSigning?.releaseSigning()
|
|
||||||
selfSigningPkSigning = pkSigning
|
|
||||||
Timber.i("## CrossSigning - Loading SSK success")
|
|
||||||
cryptoStore.storeSSKPrivateKey(sskPrivateKey)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
Timber.e("## CrossSigning - onSecretSSKGossip() private key do not match public key")
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.e("## CrossSigning - onSecretSSKGossip() ${failure.localizedMessage}")
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSecretUSKGossip(uskPrivateKey: String) {
|
|
||||||
Timber.i("## CrossSigning - onSecretUSKGossip")
|
|
||||||
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also {
|
|
||||||
Timber.e("## CrossSigning - onSecretUSKGossip() received secret but public key is not knwow ")
|
|
||||||
}
|
|
||||||
|
|
||||||
uskPrivateKey.fromBase64()
|
|
||||||
.let { privateKeySeed ->
|
|
||||||
val pkSigning = OlmPkSigning()
|
|
||||||
try {
|
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
|
|
||||||
userPkSigning?.releaseSigning()
|
|
||||||
userPkSigning = pkSigning
|
|
||||||
Timber.i("## CrossSigning - Loading USK success")
|
|
||||||
cryptoStore.storeUSKPrivateKey(uskPrivateKey)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
Timber.e("## CrossSigning - onSecretUSKGossip() private key do not match public key")
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?,
|
|
||||||
uskKeyPrivateKey: String?,
|
|
||||||
sskPrivateKey: String?
|
|
||||||
): UserTrustResult {
|
|
||||||
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
|
||||||
|
|
||||||
var masterKeyIsTrusted = false
|
|
||||||
var userKeyIsTrusted = false
|
|
||||||
var selfSignedKeyIsTrusted = false
|
|
||||||
|
|
||||||
masterKeyPrivateKey?.fromBase64()
|
|
||||||
?.let { privateKeySeed ->
|
|
||||||
val pkSigning = OlmPkSigning()
|
|
||||||
try {
|
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
|
|
||||||
masterPkSigning?.releaseSigning()
|
|
||||||
masterPkSigning = pkSigning
|
|
||||||
masterKeyIsTrusted = true
|
|
||||||
Timber.i("## CrossSigning - Loading master key success")
|
|
||||||
} else {
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uskKeyPrivateKey?.fromBase64()
|
|
||||||
?.let { privateKeySeed ->
|
|
||||||
val pkSigning = OlmPkSigning()
|
|
||||||
try {
|
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
|
|
||||||
userPkSigning?.releaseSigning()
|
|
||||||
userPkSigning = pkSigning
|
|
||||||
userKeyIsTrusted = true
|
|
||||||
Timber.i("## CrossSigning - Loading master key success")
|
|
||||||
} else {
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sskPrivateKey?.fromBase64()
|
|
||||||
?.let { privateKeySeed ->
|
|
||||||
val pkSigning = OlmPkSigning()
|
|
||||||
try {
|
|
||||||
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
|
|
||||||
selfSigningPkSigning?.releaseSigning()
|
|
||||||
selfSigningPkSigning = pkSigning
|
|
||||||
selfSignedKeyIsTrusted = true
|
|
||||||
Timber.i("## CrossSigning - Loading master key success")
|
|
||||||
} else {
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
pkSigning.releaseSigning()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!masterKeyIsTrusted || !userKeyIsTrusted || !selfSignedKeyIsTrusted) {
|
|
||||||
return UserTrustResult.KeysNotTrusted(mxCrossSigningInfo)
|
|
||||||
} else {
|
|
||||||
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
|
|
||||||
val checkSelfTrust = checkSelfTrust()
|
|
||||||
if (checkSelfTrust.isVerified()) {
|
|
||||||
cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey, uskKeyPrivateKey, sskPrivateKey)
|
|
||||||
setUserKeysAsTrusted(userId, true)
|
|
||||||
}
|
|
||||||
return checkSelfTrust
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* ┏━━━━━━━━┓ ┏━━━━━━━━┓
|
|
||||||
* ┃ ALICE ┃ ┃ BOB ┃
|
|
||||||
* ┗━━━━━━━━┛ ┗━━━━━━━━┛
|
|
||||||
* MSK ┌────────────▶ MSK
|
|
||||||
* │
|
|
||||||
* │ │
|
|
||||||
* │ SSK │
|
|
||||||
* │ │
|
|
||||||
* │ │
|
|
||||||
* └──▶ USK ────────────┘
|
|
||||||
*/
|
|
||||||
override fun isUserTrusted(otherUserId: String): Boolean {
|
|
||||||
return cryptoStore.getCrossSigningInfo(userId)?.isTrusted() == true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isCrossSigningVerified(): Boolean {
|
|
||||||
return checkSelfTrust().isVerified()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will not force a download of the key, but will verify signatures trust chain
|
|
||||||
*/
|
|
||||||
override fun checkUserTrust(otherUserId: String): UserTrustResult {
|
|
||||||
Timber.v("## CrossSigning checkUserTrust for $otherUserId")
|
|
||||||
if (otherUserId == userId) {
|
|
||||||
return checkSelfTrust()
|
|
||||||
}
|
|
||||||
// I trust a user if I trust his master key
|
|
||||||
// I can trust the master key if it is signed by my user key
|
|
||||||
// TODO what if the master key is signed by a device key that i have verified
|
|
||||||
|
|
||||||
// First let's get my user key
|
|
||||||
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
|
|
||||||
|
|
||||||
checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId))
|
|
||||||
|
|
||||||
return UserTrustResult.Success
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult {
|
|
||||||
val myUserKey = myCrossSigningInfo?.userKey()
|
|
||||||
?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
|
||||||
|
|
||||||
if (!myCrossSigningInfo.isTrusted()) {
|
|
||||||
return UserTrustResult.KeysNotTrusted(myCrossSigningInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's get the other user master key
|
|
||||||
val otherMasterKey = otherInfo?.masterKey()
|
|
||||||
?: return UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "")
|
|
||||||
|
|
||||||
val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures
|
|
||||||
?.get(userId) // Signatures made by me
|
|
||||||
?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}")
|
|
||||||
|
|
||||||
if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) {
|
|
||||||
Timber.d("## CrossSigning checkUserTrust false for ${otherInfo.userId}, not signed by my UserSigningKey")
|
|
||||||
return UserTrustResult.KeyNotSigned(otherMasterKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that Alice USK signature of Bob MSK is valid
|
|
||||||
try {
|
|
||||||
olmUtility!!.verifyEd25519Signature(masterKeySignaturesMadeByMyUserKey, myUserKey.unpaddedBase64PublicKey, otherMasterKey.canonicalSignable())
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
return UserTrustResult.InvalidSignature(myUserKey, masterKeySignaturesMadeByMyUserKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
return UserTrustResult.Success
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkSelfTrust(): UserTrustResult {
|
|
||||||
// Special case when it's me,
|
|
||||||
// I have to check that MSK -> USK -> SSK
|
|
||||||
// and that MSK is trusted (i know the private key, or is signed by a trusted device)
|
|
||||||
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
|
|
||||||
|
|
||||||
return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(userId))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List<CryptoDeviceInfo>?): UserTrustResult {
|
|
||||||
// Special case when it's me,
|
|
||||||
// I have to check that MSK -> USK -> SSK
|
|
||||||
// and that MSK is trusted (i know the private key, or is signed by a trusted device)
|
|
||||||
// val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
|
|
||||||
|
|
||||||
val myMasterKey = myCrossSigningInfo?.masterKey()
|
|
||||||
?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
|
||||||
|
|
||||||
// Is the master key trusted
|
|
||||||
// 1) check if I know the private key
|
|
||||||
val masterPrivateKey = cryptoStore.getCrossSigningPrivateKeys()
|
|
||||||
?.master
|
|
||||||
?.fromBase64()
|
|
||||||
|
|
||||||
var isMaterKeyTrusted = false
|
|
||||||
if (myMasterKey.trustLevel?.locallyVerified == true) {
|
|
||||||
isMaterKeyTrusted = true
|
|
||||||
} else if (masterPrivateKey != null) {
|
|
||||||
// Check if private match public
|
|
||||||
var olmPkSigning: OlmPkSigning? = null
|
|
||||||
try {
|
|
||||||
olmPkSigning = OlmPkSigning()
|
|
||||||
val expectedPK = olmPkSigning.initWithSeed(masterPrivateKey)
|
|
||||||
isMaterKeyTrusted = myMasterKey.unpaddedBase64PublicKey == expectedPK
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.e(failure)
|
|
||||||
}
|
|
||||||
olmPkSigning?.releaseSigning()
|
|
||||||
} else {
|
|
||||||
// Maybe it's signed by a locally trusted device?
|
|
||||||
myMasterKey.signatures?.get(userId)?.forEach { (key, value) ->
|
|
||||||
val potentialDeviceId = key.removePrefix("ed25519:")
|
|
||||||
val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId)
|
|
||||||
if (potentialDevice != null && potentialDevice.isVerified) {
|
|
||||||
// Check signature validity?
|
|
||||||
try {
|
|
||||||
olmUtility?.verifyEd25519Signature(value, potentialDevice.fingerprint(), myMasterKey.canonicalSignable())
|
|
||||||
isMaterKeyTrusted = true
|
|
||||||
return@forEach
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
// log
|
|
||||||
Timber.w(failure, "Signature not valid?")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isMaterKeyTrusted) {
|
|
||||||
return UserTrustResult.KeysNotTrusted(myCrossSigningInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
val myUserKey = myCrossSigningInfo.userKey()
|
|
||||||
?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
|
||||||
|
|
||||||
val userKeySignaturesMadeByMyMasterKey = myUserKey.signatures
|
|
||||||
?.get(userId) // Signatures made by me
|
|
||||||
?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}")
|
|
||||||
|
|
||||||
if (userKeySignaturesMadeByMyMasterKey.isNullOrBlank()) {
|
|
||||||
Timber.d("## CrossSigning checkUserTrust false for $userId, USK not signed by MSK")
|
|
||||||
return UserTrustResult.KeyNotSigned(myUserKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that Alice USK signature of Alice MSK is valid
|
|
||||||
try {
|
|
||||||
olmUtility!!.verifyEd25519Signature(userKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, myUserKey.canonicalSignable())
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
return UserTrustResult.InvalidSignature(myUserKey, userKeySignaturesMadeByMyMasterKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
val mySSKey = myCrossSigningInfo.selfSigningKey()
|
|
||||||
?: return UserTrustResult.CrossSigningNotConfigured(userId)
|
|
||||||
|
|
||||||
val ssKeySignaturesMadeByMyMasterKey = mySSKey.signatures
|
|
||||||
?.get(userId) // Signatures made by me
|
|
||||||
?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}")
|
|
||||||
|
|
||||||
if (ssKeySignaturesMadeByMyMasterKey.isNullOrBlank()) {
|
|
||||||
Timber.d("## CrossSigning checkUserTrust false for $userId, SSK not signed by MSK")
|
|
||||||
return UserTrustResult.KeyNotSigned(mySSKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that Alice USK signature of Alice MSK is valid
|
|
||||||
try {
|
|
||||||
olmUtility!!.verifyEd25519Signature(ssKeySignaturesMadeByMyMasterKey, myMasterKey.unpaddedBase64PublicKey, mySSKey.canonicalSignable())
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
return UserTrustResult.InvalidSignature(mySSKey, ssKeySignaturesMadeByMyMasterKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
return UserTrustResult.Success
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? {
|
|
||||||
return cryptoStore.getCrossSigningInfo(otherUserId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLiveCrossSigningKeys(userId: String): LiveData<Optional<MXCrossSigningInfo>> {
|
|
||||||
return cryptoStore.getLiveCrossSigningInfo(userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getMyCrossSigningKeys(): MXCrossSigningInfo? {
|
|
||||||
return cryptoStore.getMyCrossSigningInfo()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? {
|
|
||||||
return cryptoStore.getCrossSigningPrivateKeys()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>> {
|
|
||||||
return cryptoStore.getLiveCrossSigningPrivateKeys()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canCrossSign(): Boolean {
|
|
||||||
return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null &&
|
|
||||||
cryptoStore.getCrossSigningPrivateKeys()?.user != null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun allPrivateKeysKnown(): Boolean {
|
|
||||||
return checkSelfTrust().isVerified() &&
|
|
||||||
cryptoStore.getCrossSigningPrivateKeys()?.allKnown().orFalse()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
|
||||||
Timber.d("## CrossSigning - Mark user $userId as trusted ")
|
|
||||||
// We should have this user keys
|
|
||||||
val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey()
|
|
||||||
if (otherMasterKeys == null) {
|
|
||||||
callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known"))
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
val myKeys = getUserCrossSigningKeys(userId)
|
|
||||||
if (myKeys == null) {
|
|
||||||
callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account"))
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey
|
|
||||||
if (userPubKey == null || userPkSigning == null) {
|
|
||||||
callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey"))
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign the other MasterKey with our UserSigning key
|
|
||||||
val newSignature = JsonCanonicalizer.getCanonicalJson(
|
|
||||||
Map::class.java,
|
|
||||||
otherMasterKeys.signalableJSONDictionary()
|
|
||||||
).let { userPkSigning?.sign(it) }
|
|
||||||
|
|
||||||
if (newSignature == null) {
|
|
||||||
// race??
|
|
||||||
callback.onFailure(Throwable("## CrossSigning - Failed to sign"))
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
cryptoStore.setUserKeysAsTrusted(otherUserId, true)
|
|
||||||
// TODO update local copy with new signature directly here? kind of local echo of trust?
|
|
||||||
|
|
||||||
Timber.d("## CrossSigning - Upload signature of $userId MSK signed by USK")
|
|
||||||
val uploadQuery = UploadSignatureQueryBuilder()
|
|
||||||
.withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature))
|
|
||||||
.build()
|
|
||||||
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
|
|
||||||
this.executionThread = TaskThread.CRYPTO
|
|
||||||
this.callback = callback
|
|
||||||
}.executeBy(taskExecutor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun markMyMasterKeyAsTrusted() {
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
|
||||||
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
|
|
||||||
checkSelfTrust()
|
|
||||||
// re-verify all trusts
|
|
||||||
onUsersDeviceUpdate(listOf(userId))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun trustDevice(deviceId: String, callback: MatrixCallback<Unit>) {
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
|
||||||
// This device should be yours
|
|
||||||
val device = cryptoStore.getUserDevice(userId, deviceId)
|
|
||||||
if (device == null) {
|
|
||||||
callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours"))
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
val myKeys = getUserCrossSigningKeys(userId)
|
|
||||||
if (myKeys == null) {
|
|
||||||
callback.onFailure(Throwable("CrossSigning is not setup for this account"))
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey
|
|
||||||
if (ssPubKey == null || selfSigningPkSigning == null) {
|
|
||||||
callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey"))
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign with self signing
|
|
||||||
val newSignature = selfSigningPkSigning?.sign(device.canonicalSignable())
|
|
||||||
|
|
||||||
if (newSignature == null) {
|
|
||||||
// race??
|
|
||||||
callback.onFailure(Throwable("Failed to sign"))
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
val toUpload = device.copy(
|
|
||||||
signatures = mapOf(
|
|
||||||
userId
|
|
||||||
to
|
|
||||||
mapOf(
|
|
||||||
"ed25519:$ssPubKey" to newSignature
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val uploadQuery = UploadSignatureQueryBuilder()
|
|
||||||
.withDeviceInfo(toUpload)
|
|
||||||
.build()
|
|
||||||
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
|
|
||||||
this.executionThread = TaskThread.CRYPTO
|
|
||||||
this.callback = callback
|
|
||||||
}.executeBy(taskExecutor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult {
|
|
||||||
val otherDevice = cryptoStore.getUserDevice(otherUserId, otherDeviceId)
|
|
||||||
?: return DeviceTrustResult.UnknownDevice(otherDeviceId)
|
|
||||||
|
|
||||||
val myKeys = getUserCrossSigningKeys(userId)
|
|
||||||
?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId))
|
|
||||||
|
|
||||||
if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
|
|
||||||
|
|
||||||
val otherKeys = getUserCrossSigningKeys(otherUserId)
|
|
||||||
?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherUserId))
|
|
||||||
|
|
||||||
// TODO should we force verification ?
|
|
||||||
if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys))
|
|
||||||
|
|
||||||
// Check if the trust chain is valid
|
|
||||||
/*
|
|
||||||
* ┏━━━━━━━━┓ ┏━━━━━━━━┓
|
|
||||||
* ┃ ALICE ┃ ┃ BOB ┃
|
|
||||||
* ┗━━━━━━━━┛ ┗━━━━━━━━┛
|
|
||||||
* MSK ┌────────────▶MSK
|
|
||||||
* │
|
|
||||||
* │ │ │
|
|
||||||
* │ SSK │ └──▶ SSK ──────────────────┐
|
|
||||||
* │ │ │
|
|
||||||
* │ │ USK │
|
|
||||||
* └──▶ USK ────────────┘ (not visible by │
|
|
||||||
* Alice) │
|
|
||||||
* ▼
|
|
||||||
* ┌──────────────┐
|
|
||||||
* │ BOB's Device │
|
|
||||||
* └──────────────┘
|
|
||||||
*/
|
|
||||||
|
|
||||||
val otherSSKSignature = otherDevice.signatures?.get(otherUserId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}")
|
|
||||||
?: return legacyFallbackTrust(
|
|
||||||
locallyTrusted,
|
|
||||||
DeviceTrustResult.MissingDeviceSignature(
|
|
||||||
otherDeviceId, otherKeys.selfSigningKey()
|
|
||||||
?.unpaddedBase64PublicKey
|
|
||||||
?: ""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check bob's device is signed by bob's SSK
|
|
||||||
try {
|
|
||||||
olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable())
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDeviceId, otherSSKSignature, e))
|
|
||||||
}
|
|
||||||
|
|
||||||
return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo): DeviceTrustResult {
|
|
||||||
val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified()
|
|
||||||
myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId))
|
|
||||||
|
|
||||||
if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
|
|
||||||
|
|
||||||
otherKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherDevice.userId))
|
|
||||||
|
|
||||||
// TODO should we force verification ?
|
|
||||||
if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys))
|
|
||||||
|
|
||||||
// Check if the trust chain is valid
|
|
||||||
/*
|
|
||||||
* ┏━━━━━━━━┓ ┏━━━━━━━━┓
|
|
||||||
* ┃ ALICE ┃ ┃ BOB ┃
|
|
||||||
* ┗━━━━━━━━┛ ┗━━━━━━━━┛
|
|
||||||
* MSK ┌────────────▶MSK
|
|
||||||
* │
|
|
||||||
* │ │ │
|
|
||||||
* │ SSK │ └──▶ SSK ──────────────────┐
|
|
||||||
* │ │ │
|
|
||||||
* │ │ USK │
|
|
||||||
* └──▶ USK ────────────┘ (not visible by │
|
|
||||||
* Alice) │
|
|
||||||
* ▼
|
|
||||||
* ┌──────────────┐
|
|
||||||
* │ BOB's Device │
|
|
||||||
* └──────────────┘
|
|
||||||
*/
|
|
||||||
|
|
||||||
val otherSSKSignature = otherDevice.signatures?.get(otherKeys.userId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}")
|
|
||||||
?: return legacyFallbackTrust(
|
|
||||||
locallyTrusted,
|
|
||||||
DeviceTrustResult.MissingDeviceSignature(
|
|
||||||
otherDevice.deviceId, otherKeys.selfSigningKey()
|
|
||||||
?.unpaddedBase64PublicKey
|
|
||||||
?: ""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check bob's device is signed by bob's SSK
|
|
||||||
try {
|
|
||||||
olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable())
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDevice.deviceId, otherSSKSignature, e))
|
|
||||||
}
|
|
||||||
|
|
||||||
return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun legacyFallbackTrust(locallyTrusted: Boolean?, crossSignTrustFail: DeviceTrustResult): DeviceTrustResult {
|
|
||||||
return if (locallyTrusted == true) {
|
|
||||||
DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true))
|
|
||||||
} else {
|
|
||||||
crossSignTrustFail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUsersDeviceUpdate(userIds: List<String>) {
|
|
||||||
Timber.d("## CrossSigning - onUsersDeviceUpdate for users: ${userIds.logLimit()}")
|
|
||||||
val workerParams = UpdateTrustWorker.Params(
|
|
||||||
sessionId = sessionId,
|
|
||||||
filename = updateTrustWorkerDataRepository.createParam(userIds)
|
|
||||||
)
|
|
||||||
val workerData = WorkerParamsFactory.toData(workerParams)
|
|
||||||
|
|
||||||
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<UpdateTrustWorker>()
|
|
||||||
.setInputData(workerData)
|
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
workManagerProvider.workManager
|
|
||||||
.beginUniqueWork("TRUST_UPDATE_QUEUE", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
|
|
||||||
.enqueue()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
|
|
||||||
val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted()
|
|
||||||
cryptoStore.setUserKeysAsTrusted(otherUserId, trusted)
|
|
||||||
// If it's me, recheck trust of all users and devices?
|
|
||||||
val users = ArrayList<String>()
|
|
||||||
if (otherUserId == userId && currentTrust != trusted) {
|
|
||||||
// notify key requester
|
|
||||||
outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted)
|
|
||||||
cryptoStore.updateUsersTrust {
|
|
||||||
users.add(it)
|
|
||||||
checkUserTrust(it).isVerified()
|
|
||||||
}
|
|
||||||
|
|
||||||
users.forEach {
|
|
||||||
cryptoStore.getUserDeviceList(it)?.forEach { device ->
|
|
||||||
val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
|
|
||||||
Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
|
|
||||||
cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,350 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.crosssigning
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.work.WorkerParameters
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import io.realm.Realm
|
|
||||||
import io.realm.RealmConfiguration
|
|
||||||
import io.realm.kotlin.where
|
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.UserTrustResult
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerified
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
|
||||||
import org.matrix.android.sdk.internal.SessionManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields
|
|
||||||
import org.matrix.android.sdk.internal.database.awaitTransaction
|
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
|
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
|
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
|
||||||
import org.matrix.android.sdk.internal.database.query.where
|
|
||||||
import org.matrix.android.sdk.internal.di.CryptoDatabase
|
|
||||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
|
|
||||||
import org.matrix.android.sdk.internal.util.logLimit
|
|
||||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
|
||||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class UpdateTrustWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) :
|
|
||||||
SessionSafeCoroutineWorker<UpdateTrustWorker.Params>(context, params, sessionManager, Params::class.java) {
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
internal data class Params(
|
|
||||||
override val sessionId: String,
|
|
||||||
override val lastFailureMessage: String? = null,
|
|
||||||
// Kept for compatibility, but not used anymore (can be used for pending Worker)
|
|
||||||
val updatedUserIds: List<String>? = null,
|
|
||||||
// Passing a long list of userId can break the Work Manager due to data size limitation.
|
|
||||||
// so now we use a temporary file to store the data
|
|
||||||
val filename: String? = null
|
|
||||||
) : SessionWorkerParams
|
|
||||||
|
|
||||||
@Inject lateinit var crossSigningService: DefaultCrossSigningService
|
|
||||||
|
|
||||||
// It breaks the crypto store contract, but we need to batch things :/
|
|
||||||
@CryptoDatabase
|
|
||||||
@Inject lateinit var cryptoRealmConfiguration: RealmConfiguration
|
|
||||||
|
|
||||||
@SessionDatabase
|
|
||||||
@Inject lateinit var sessionRealmConfiguration: RealmConfiguration
|
|
||||||
|
|
||||||
@UserId
|
|
||||||
@Inject lateinit var myUserId: String
|
|
||||||
@Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper
|
|
||||||
@Inject lateinit var updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
|
|
||||||
|
|
||||||
// @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater
|
|
||||||
@Inject lateinit var cryptoStore: IMXCryptoStore
|
|
||||||
|
|
||||||
override fun injectWith(injector: SessionComponent) {
|
|
||||||
injector.inject(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun doSafeWork(params: Params): Result {
|
|
||||||
val userList = params.filename
|
|
||||||
?.let { updateTrustWorkerDataRepository.getParam(it) }
|
|
||||||
?.userIds
|
|
||||||
?: params.updatedUserIds.orEmpty()
|
|
||||||
|
|
||||||
// List should not be empty, but let's avoid go further in case of empty list
|
|
||||||
if (userList.isNotEmpty()) {
|
|
||||||
// Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
|
|
||||||
// or a new device?) So we check all again :/
|
|
||||||
Timber.v("## CrossSigning - Updating trust for users: ${userList.logLimit()}")
|
|
||||||
updateTrust(userList)
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup(params)
|
|
||||||
return Result.success()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateTrust(userListParam: List<String>) {
|
|
||||||
var userList = userListParam
|
|
||||||
var myCrossSigningInfo: MXCrossSigningInfo? = null
|
|
||||||
|
|
||||||
// First we check that the users MSK are trusted by mine
|
|
||||||
// After that we check the trust chain for each devices of each users
|
|
||||||
awaitTransaction(cryptoRealmConfiguration) { cryptoRealm ->
|
|
||||||
// By mapping here to model, this object is not live
|
|
||||||
// I should update it if needed
|
|
||||||
myCrossSigningInfo = getCrossSigningInfo(cryptoRealm, myUserId)
|
|
||||||
|
|
||||||
var myTrustResult: UserTrustResult? = null
|
|
||||||
|
|
||||||
if (userList.contains(myUserId)) {
|
|
||||||
Timber.d("## CrossSigning - Clear all trust as a change on my user was detected")
|
|
||||||
// i am in the list.. but i don't know exactly the delta of change :/
|
|
||||||
// If it's my cross signing keys we should refresh all trust
|
|
||||||
// do it anyway ?
|
|
||||||
userList = cryptoRealm.where(CrossSigningInfoEntity::class.java)
|
|
||||||
.findAll()
|
|
||||||
.mapNotNull { it.userId }
|
|
||||||
|
|
||||||
// check right now my keys and mark it as trusted as other trust depends on it
|
|
||||||
val myDevices = cryptoRealm.where<UserEntity>()
|
|
||||||
.equalTo(UserEntityFields.USER_ID, myUserId)
|
|
||||||
.findFirst()
|
|
||||||
?.devices
|
|
||||||
?.map { CryptoMapper.mapToModel(it) }
|
|
||||||
|
|
||||||
myTrustResult = crossSigningService.checkSelfTrust(myCrossSigningInfo, myDevices)
|
|
||||||
updateCrossSigningKeysTrust(cryptoRealm, myUserId, myTrustResult.isVerified())
|
|
||||||
// update model reference
|
|
||||||
myCrossSigningInfo = getCrossSigningInfo(cryptoRealm, myUserId)
|
|
||||||
}
|
|
||||||
|
|
||||||
val otherInfos = userList.associateWith { userId ->
|
|
||||||
getCrossSigningInfo(cryptoRealm, userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
val trusts = otherInfos.mapValues { entry ->
|
|
||||||
when (entry.key) {
|
|
||||||
myUserId -> myTrustResult
|
|
||||||
else -> {
|
|
||||||
crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, entry.value).also {
|
|
||||||
Timber.v("## CrossSigning - user:${entry.key} result:$it")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO! if it's me and my keys has changed... I have to reset trust for everyone!
|
|
||||||
// i have all the new trusts, update DB
|
|
||||||
trusts.forEach {
|
|
||||||
val verified = it.value?.isVerified() == true
|
|
||||||
updateCrossSigningKeysTrust(cryptoRealm, it.key, verified)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ok so now we have to check device trust for all these users..
|
|
||||||
Timber.v("## CrossSigning - Updating devices cross trust users: ${trusts.keys.logLimit()}")
|
|
||||||
trusts.keys.forEach { userId ->
|
|
||||||
val devicesEntities = cryptoRealm.where<UserEntity>()
|
|
||||||
.equalTo(UserEntityFields.USER_ID, userId)
|
|
||||||
.findFirst()
|
|
||||||
?.devices
|
|
||||||
|
|
||||||
val trustMap = devicesEntities?.associateWith { device ->
|
|
||||||
// get up to date from DB has could have been updated
|
|
||||||
val otherInfo = getCrossSigningInfo(cryptoRealm, userId)
|
|
||||||
crossSigningService.checkDeviceTrust(myCrossSigningInfo, otherInfo, CryptoMapper.mapToModel(device))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update trust if needed
|
|
||||||
devicesEntities?.forEach { device ->
|
|
||||||
val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified()
|
|
||||||
Timber.v("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
|
|
||||||
if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) {
|
|
||||||
Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
|
|
||||||
// need to save
|
|
||||||
val trustEntity = device.trustLevelEntity
|
|
||||||
if (trustEntity == null) {
|
|
||||||
device.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also {
|
|
||||||
it.locallyVerified = false
|
|
||||||
it.crossSignedVerified = crossSignedVerified
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
trustEntity.crossSignedVerified = crossSignedVerified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// So Cross Signing keys trust is updated, device trust is updated
|
|
||||||
// We can now update room shields? in the session DB?
|
|
||||||
updateTrustStep2(userList, myCrossSigningInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateTrustStep2(userList: List<String>, myCrossSigningInfo: MXCrossSigningInfo?) {
|
|
||||||
Timber.d("## CrossSigning - Updating shields for impacted rooms...")
|
|
||||||
awaitTransaction(sessionRealmConfiguration) { sessionRealm ->
|
|
||||||
Realm.getInstance(cryptoRealmConfiguration).use { cryptoRealm ->
|
|
||||||
sessionRealm.where(RoomMemberSummaryEntity::class.java)
|
|
||||||
.`in`(RoomMemberSummaryEntityFields.USER_ID, userList.toTypedArray())
|
|
||||||
.distinct(RoomMemberSummaryEntityFields.ROOM_ID)
|
|
||||||
.findAll()
|
|
||||||
.map { it.roomId }
|
|
||||||
.also { Timber.d("## CrossSigning - ... impacted rooms ${it.logLimit()}") }
|
|
||||||
.forEach { roomId ->
|
|
||||||
RoomSummaryEntity.where(sessionRealm, roomId)
|
|
||||||
.equalTo(RoomSummaryEntityFields.IS_ENCRYPTED, true)
|
|
||||||
.findFirst()
|
|
||||||
?.let { roomSummary ->
|
|
||||||
Timber.v("## CrossSigning - Check shield state for room $roomId")
|
|
||||||
val allActiveRoomMembers = RoomMemberHelper(sessionRealm, roomId).getActiveRoomMemberIds()
|
|
||||||
try {
|
|
||||||
val updatedTrust = computeRoomShield(
|
|
||||||
myCrossSigningInfo,
|
|
||||||
cryptoRealm,
|
|
||||||
allActiveRoomMembers,
|
|
||||||
roomSummary
|
|
||||||
)
|
|
||||||
if (roomSummary.roomEncryptionTrustLevel != updatedTrust) {
|
|
||||||
Timber.d("## CrossSigning - Shield change detected for $roomId -> $updatedTrust")
|
|
||||||
roomSummary.roomEncryptionTrustLevel = updatedTrust
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.e(failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCrossSigningInfo(cryptoRealm: Realm, userId: String): MXCrossSigningInfo? {
|
|
||||||
return cryptoRealm.where(CrossSigningInfoEntity::class.java)
|
|
||||||
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
|
|
||||||
.findFirst()
|
|
||||||
?.let { mapCrossSigningInfoEntity(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cleanup(params: Params) {
|
|
||||||
params.filename
|
|
||||||
?.let { updateTrustWorkerDataRepository.delete(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateCrossSigningKeysTrust(cryptoRealm: Realm, userId: String, verified: Boolean) {
|
|
||||||
cryptoRealm.where(CrossSigningInfoEntity::class.java)
|
|
||||||
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
|
|
||||||
.findFirst()
|
|
||||||
?.crossSigningKeys
|
|
||||||
?.forEach { info ->
|
|
||||||
// optimization to avoid trigger updates when there is no change..
|
|
||||||
if (info.trustLevelEntity?.isVerified() != verified) {
|
|
||||||
Timber.d("## CrossSigning - Trust change for $userId : $verified")
|
|
||||||
val level = info.trustLevelEntity
|
|
||||||
if (level == null) {
|
|
||||||
info.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also {
|
|
||||||
it.locallyVerified = verified
|
|
||||||
it.crossSignedVerified = verified
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
level.locallyVerified = verified
|
|
||||||
level.crossSignedVerified = verified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun computeRoomShield(myCrossSigningInfo: MXCrossSigningInfo?,
|
|
||||||
cryptoRealm: Realm,
|
|
||||||
activeMemberUserIds: List<String>,
|
|
||||||
roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel {
|
|
||||||
Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> ${activeMemberUserIds.logLimit()}")
|
|
||||||
// The set of “all users” depends on the type of room:
|
|
||||||
// For regular / topic rooms which have more than 2 members (including yourself) are considered when decorating a room
|
|
||||||
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
|
|
||||||
val listToCheck = if (roomSummaryEntity.isDirect || activeMemberUserIds.size <= 2) {
|
|
||||||
activeMemberUserIds.filter { it != myUserId }
|
|
||||||
} else {
|
|
||||||
activeMemberUserIds
|
|
||||||
}
|
|
||||||
|
|
||||||
val allTrustedUserIds = listToCheck
|
|
||||||
.filter { userId ->
|
|
||||||
getCrossSigningInfo(cryptoRealm, userId)?.isTrusted() == true
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (allTrustedUserIds.isEmpty()) {
|
|
||||||
RoomEncryptionTrustLevel.Default
|
|
||||||
} else {
|
|
||||||
// If one of the verified user as an untrusted device -> warning
|
|
||||||
// If all devices of all verified users are trusted -> green
|
|
||||||
// else -> black
|
|
||||||
allTrustedUserIds
|
|
||||||
.mapNotNull { userId ->
|
|
||||||
cryptoRealm.where<UserEntity>()
|
|
||||||
.equalTo(UserEntityFields.USER_ID, userId)
|
|
||||||
.findFirst()
|
|
||||||
?.devices
|
|
||||||
?.map { CryptoMapper.mapToModel(it) }
|
|
||||||
}
|
|
||||||
.flatten()
|
|
||||||
.let { allDevices ->
|
|
||||||
Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} devices ${allDevices.map { it.deviceId }.logLimit()}")
|
|
||||||
if (myCrossSigningInfo != null) {
|
|
||||||
allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() }
|
|
||||||
} else {
|
|
||||||
// Legacy method
|
|
||||||
allDevices.any { !it.isVerified }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.let { hasWarning ->
|
|
||||||
if (hasWarning) {
|
|
||||||
RoomEncryptionTrustLevel.Warning
|
|
||||||
} else {
|
|
||||||
if (listToCheck.size == allTrustedUserIds.size) {
|
|
||||||
// all users are trusted and all devices are verified
|
|
||||||
RoomEncryptionTrustLevel.Trusted
|
|
||||||
} else {
|
|
||||||
RoomEncryptionTrustLevel.Default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo {
|
|
||||||
val userId = xsignInfo.userId ?: ""
|
|
||||||
return MXCrossSigningInfo(
|
|
||||||
userId = userId,
|
|
||||||
crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull {
|
|
||||||
crossSigningKeysMapper.map(userId, it)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun buildErrorParams(params: Params, message: String): Params {
|
|
||||||
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
|
|
||||||
}
|
|
||||||
}
|
|
@ -28,31 +28,33 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
|
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.MatrixError
|
import org.matrix.android.sdk.api.failure.MatrixError
|
||||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||||
import org.matrix.android.sdk.api.listeners.StepProgressListener
|
import org.matrix.android.sdk.api.listeners.StepProgressListener
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
|
||||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrust
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
||||||
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
|
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
|
||||||
import org.matrix.android.sdk.internal.crypto.MegolmSessionImportManager
|
import org.matrix.android.sdk.internal.crypto.MegolmSessionImportManager
|
||||||
import org.matrix.android.sdk.internal.crypto.OlmMachineProvider
|
import org.matrix.android.sdk.internal.crypto.OlmMachineProvider
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrust
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody
|
||||||
import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
|
|
||||||
import org.matrix.android.sdk.internal.crypto.network.RequestSender
|
import org.matrix.android.sdk.internal.crypto.network.RequestSender
|
||||||
import org.matrix.android.sdk.internal.crypto.store.SavedKeyBackupKeyInfo
|
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
||||||
@ -263,7 +265,7 @@ internal class RustKeyBackupService @Inject constructor(
|
|||||||
private suspend fun checkBackupTrust(authData: MegolmBackupAuthData?): KeysBackupVersionTrust {
|
private suspend fun checkBackupTrust(authData: MegolmBackupAuthData?): KeysBackupVersionTrust {
|
||||||
return if (authData == null || authData.publicKey.isEmpty() || authData.signatures.isNullOrEmpty()) {
|
return if (authData == null || authData.publicKey.isEmpty() || authData.signatures.isNullOrEmpty()) {
|
||||||
Timber.v("getKeysBackupTrust: Key backup is absent or missing required data")
|
Timber.v("getKeysBackupTrust: Key backup is absent or missing required data")
|
||||||
KeysBackupVersionTrust()
|
KeysBackupVersionTrust(usable = false)
|
||||||
} else {
|
} else {
|
||||||
KeysBackupVersionTrust(olmMachine.checkAuthDataSignature(authData))
|
KeysBackupVersionTrust(olmMachine.checkAuthDataSignature(authData))
|
||||||
}
|
}
|
||||||
@ -375,7 +377,7 @@ internal class RustKeyBackupService @Inject constructor(
|
|||||||
Timber.i("## CrossSigning - onSecretKeyGossip")
|
Timber.i("## CrossSigning - onSecretKeyGossip")
|
||||||
withContext(coroutineDispatchers.crypto) {
|
withContext(coroutineDispatchers.crypto) {
|
||||||
try {
|
try {
|
||||||
val version = sender.getKeyBackupVersion()
|
val version = sender.getKeyBackupLastVersion()?.toKeysVersionResult()
|
||||||
|
|
||||||
if (version != null) {
|
if (version != null) {
|
||||||
val key = BackupRecoveryKey.fromBase64(secret)
|
val key = BackupRecoveryKey.fromBase64(secret)
|
||||||
@ -584,13 +586,13 @@ internal class RustKeyBackupService @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws
|
@Throws
|
||||||
override suspend fun getCurrentVersion(): KeysVersionResult? {
|
override suspend fun getCurrentVersion(): KeysBackupLastVersionResult? {
|
||||||
return sender.getKeyBackupVersion()
|
return sender.getKeyBackupLastVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun forceUsingLastVersion(): Boolean {
|
override suspend fun forceUsingLastVersion(): Boolean {
|
||||||
val response = withContext(coroutineDispatchers.io) {
|
val response = withContext(coroutineDispatchers.io) {
|
||||||
sender.getKeyBackupVersion()
|
sender.getKeyBackupLastVersion()?.toKeysVersionResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
return withContext(coroutineDispatchers.crypto) {
|
return withContext(coroutineDispatchers.crypto) {
|
||||||
@ -644,7 +646,7 @@ internal class RustKeyBackupService @Inject constructor(
|
|||||||
keysBackupVersion = null
|
keysBackupVersion = null
|
||||||
keysBackupStateManager.state = KeysBackupState.CheckingBackUpOnHomeserver
|
keysBackupStateManager.state = KeysBackupState.CheckingBackUpOnHomeserver
|
||||||
try {
|
try {
|
||||||
val data = getCurrentVersion()
|
val data = getCurrentVersion()?.toKeysVersionResult()
|
||||||
withContext(coroutineDispatchers.crypto) {
|
withContext(coroutineDispatchers.crypto) {
|
||||||
checkAndStartWithKeysBackupVersion(data)
|
checkAndStartWithKeysBackupVersion(data)
|
||||||
}
|
}
|
||||||
@ -718,6 +720,10 @@ internal class RustKeyBackupService @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun computePrivateKey(passphrase: String, privateKeySalt: String, privateKeyIterations: Int, progressListener: ProgressListener): ByteArray {
|
||||||
|
return deriveKey(passphrase, privateKeySalt, privateKeyIterations, progressListener)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? {
|
override suspend fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? {
|
||||||
val info = olmMachine.getBackupKeys() ?: return null
|
val info = olmMachine.getBackupKeys() ?: return null
|
||||||
return SavedKeyBackupKeyInfo(info.recoveryKey, info.backupVersion)
|
return SavedKeyBackupKeyInfo(info.recoveryKey, info.backupVersion)
|
||||||
|
@ -23,8 +23,8 @@ import kotlinx.coroutines.joinAll
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
||||||
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
|
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
|
||||||
import org.matrix.android.sdk.internal.crypto.OlmMachine
|
import org.matrix.android.sdk.internal.crypto.OlmMachine
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -22,15 +22,18 @@ import dagger.Lazy
|
|||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.MatrixError
|
import org.matrix.android.sdk.api.failure.MatrixError
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
import org.matrix.android.sdk.api.session.events.model.Content
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import org.matrix.android.sdk.api.session.uia.UiaResult
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
import org.matrix.android.sdk.internal.auth.registration.handleUIA
|
import org.matrix.android.sdk.internal.auth.registration.handleUIA
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBackupData
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBackupData
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
|
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
|
||||||
@ -42,7 +45,6 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDa
|
|||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask
|
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask
|
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
|
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse
|
import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
|
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse
|
import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse
|
||||||
@ -177,7 +179,7 @@ internal class RequestSender @Inject constructor(
|
|||||||
uploadSigningKeysTask.execute(uploadSigningKeysParams)
|
uploadSigningKeysTask.execute(uploadSigningKeysParams)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
if (interactiveAuthInterceptor == null ||
|
if (interactiveAuthInterceptor == null ||
|
||||||
!handleUIA(
|
handleUIA(
|
||||||
failure = failure,
|
failure = failure,
|
||||||
interceptor = interactiveAuthInterceptor,
|
interceptor = interactiveAuthInterceptor,
|
||||||
retryBlock = { authUpdate ->
|
retryBlock = { authUpdate ->
|
||||||
@ -186,7 +188,7 @@ internal class RequestSender @Inject constructor(
|
|||||||
REQUEST_RETRY_COUNT
|
REQUEST_RETRY_COUNT
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
) != UiaResult.SUCCESS
|
||||||
) {
|
) {
|
||||||
Timber.d("## UIA: propagate failure")
|
Timber.d("## UIA: propagate failure")
|
||||||
throw failure
|
throw failure
|
||||||
@ -217,13 +219,17 @@ internal class RequestSender @Inject constructor(
|
|||||||
sendToDeviceTask.executeRetry(sendToDeviceParams, REQUEST_RETRY_COUNT)
|
sendToDeviceTask.executeRetry(sendToDeviceParams, REQUEST_RETRY_COUNT)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getKeyBackupVersion(version: String? = null): KeysVersionResult? {
|
suspend fun getKeyBackupVersion(version: String): KeysVersionResult? = getKeyBackupVersion {
|
||||||
|
getKeysBackupVersionTask.executeRetry(version, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getKeyBackupLastVersion(): KeysBackupLastVersionResult? = getKeyBackupVersion {
|
||||||
|
getKeysBackupLastVersionTask.executeRetry(Unit, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> getKeyBackupVersion(block: ()-> T?): T?{
|
||||||
return try {
|
return try {
|
||||||
if (version != null) {
|
block()
|
||||||
getKeysBackupVersionTask.executeRetry(version, 3)
|
|
||||||
} else {
|
|
||||||
getKeysBackupLastVersionTask.executeRetry(Unit, 3)
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
if (failure is Failure.ServerError &&
|
if (failure is Failure.ServerError &&
|
||||||
failure.error.code == MatrixError.M_NOT_FOUND) {
|
failure.error.code == MatrixError.M_NOT_FOUND) {
|
||||||
|
@ -1,190 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.tasks
|
|
||||||
|
|
||||||
import dagger.Lazy
|
|
||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KeyUsage
|
|
||||||
import org.matrix.android.sdk.api.session.uia.UiaResult
|
|
||||||
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
|
||||||
import org.matrix.android.sdk.internal.auth.registration.handleUIA
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder
|
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import org.matrix.android.sdk.internal.task.Task
|
|
||||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
|
||||||
import org.matrix.olm.OlmPkSigning
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal interface InitializeCrossSigningTask : Task<InitializeCrossSigningTask.Params, InitializeCrossSigningTask.Result> {
|
|
||||||
data class Params(
|
|
||||||
val interactiveAuthInterceptor: UserInteractiveAuthInterceptor?
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Result(
|
|
||||||
val masterKeyPK: String,
|
|
||||||
val userKeyPK: String,
|
|
||||||
val selfSigningKeyPK: String,
|
|
||||||
val masterKeyInfo: CryptoCrossSigningKey,
|
|
||||||
val userKeyInfo: CryptoCrossSigningKey,
|
|
||||||
val selfSignedKeyInfo: CryptoCrossSigningKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DefaultInitializeCrossSigningTask @Inject constructor(
|
|
||||||
@UserId private val userId: String,
|
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
|
||||||
private val uploadSigningKeysTask: UploadSigningKeysTask,
|
|
||||||
private val uploadSignaturesTask: UploadSignaturesTask
|
|
||||||
) : InitializeCrossSigningTask {
|
|
||||||
|
|
||||||
override suspend fun execute(params: InitializeCrossSigningTask.Params): InitializeCrossSigningTask.Result {
|
|
||||||
var masterPkOlm: OlmPkSigning? = null
|
|
||||||
var userSigningPkOlm: OlmPkSigning? = null
|
|
||||||
var selfSigningPkOlm: OlmPkSigning? = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
// =================
|
|
||||||
// MASTER KEY
|
|
||||||
// =================
|
|
||||||
|
|
||||||
masterPkOlm = OlmPkSigning()
|
|
||||||
val masterKeyPrivateKey = OlmPkSigning.generateSeed()
|
|
||||||
val masterPublicKey = masterPkOlm.initWithSeed(masterKeyPrivateKey)
|
|
||||||
|
|
||||||
Timber.v("## CrossSigning - masterPublicKey:$masterPublicKey")
|
|
||||||
|
|
||||||
// =================
|
|
||||||
// USER KEY
|
|
||||||
// =================
|
|
||||||
userSigningPkOlm = OlmPkSigning()
|
|
||||||
val uskPrivateKey = OlmPkSigning.generateSeed()
|
|
||||||
val uskPublicKey = userSigningPkOlm.initWithSeed(uskPrivateKey)
|
|
||||||
|
|
||||||
Timber.v("## CrossSigning - uskPublicKey:$uskPublicKey")
|
|
||||||
|
|
||||||
// Sign userSigningKey with master
|
|
||||||
val signedUSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING)
|
|
||||||
.key(uskPublicKey)
|
|
||||||
.build()
|
|
||||||
.canonicalSignable()
|
|
||||||
.let { masterPkOlm.sign(it) }
|
|
||||||
|
|
||||||
// =================
|
|
||||||
// SELF SIGNING KEY
|
|
||||||
// =================
|
|
||||||
selfSigningPkOlm = OlmPkSigning()
|
|
||||||
val sskPrivateKey = OlmPkSigning.generateSeed()
|
|
||||||
val sskPublicKey = selfSigningPkOlm.initWithSeed(sskPrivateKey)
|
|
||||||
|
|
||||||
Timber.v("## CrossSigning - sskPublicKey:$sskPublicKey")
|
|
||||||
|
|
||||||
// Sign selfSigningKey with master
|
|
||||||
val signedSSK = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING)
|
|
||||||
.key(sskPublicKey)
|
|
||||||
.build()
|
|
||||||
.canonicalSignable()
|
|
||||||
.let { masterPkOlm.sign(it) }
|
|
||||||
|
|
||||||
// I need to upload the keys
|
|
||||||
val mskCrossSigningKeyInfo = CryptoCrossSigningKey.Builder(userId, KeyUsage.MASTER)
|
|
||||||
.key(masterPublicKey)
|
|
||||||
.build()
|
|
||||||
val uploadSigningKeysParams = UploadSigningKeysTask.Params(
|
|
||||||
masterKey = mskCrossSigningKeyInfo,
|
|
||||||
userKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.USER_SIGNING)
|
|
||||||
.key(uskPublicKey)
|
|
||||||
.signature(userId, masterPublicKey, signedUSK)
|
|
||||||
.build(),
|
|
||||||
selfSignedKey = CryptoCrossSigningKey.Builder(userId, KeyUsage.SELF_SIGNING)
|
|
||||||
.key(sskPublicKey)
|
|
||||||
.signature(userId, masterPublicKey, signedSSK)
|
|
||||||
.build(),
|
|
||||||
userAuthParam = null
|
|
||||||
// userAuthParam = params.authParams
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
uploadSigningKeysTask.execute(uploadSigningKeysParams)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
if (params.interactiveAuthInterceptor == null ||
|
|
||||||
handleUIA(
|
|
||||||
failure = failure,
|
|
||||||
interceptor = params.interactiveAuthInterceptor,
|
|
||||||
retryBlock = { authUpdate ->
|
|
||||||
uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate))
|
|
||||||
}
|
|
||||||
) != UiaResult.SUCCESS
|
|
||||||
) {
|
|
||||||
Timber.d("## UIA: propagate failure")
|
|
||||||
throw failure
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign the current device with SSK
|
|
||||||
val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder()
|
|
||||||
|
|
||||||
val myDevice = myDeviceInfoHolder.get().myDevice
|
|
||||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, myDevice.signalableJSONDictionary())
|
|
||||||
val signedDevice = selfSigningPkOlm.sign(canonicalJson)
|
|
||||||
val updateSignatures = (myDevice.signatures?.toMutableMap() ?: HashMap())
|
|
||||||
.also {
|
|
||||||
it[userId] = (it[userId]
|
|
||||||
?: HashMap()) + mapOf("ed25519:$sskPublicKey" to signedDevice)
|
|
||||||
}
|
|
||||||
myDevice.copy(signatures = updateSignatures).let {
|
|
||||||
uploadSignatureQueryBuilder.withDeviceInfo(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sign MSK with device key (migration) and upload signatures
|
|
||||||
val message = JsonCanonicalizer.getCanonicalJson(Map::class.java, mskCrossSigningKeyInfo.signalableJSONDictionary())
|
|
||||||
olmDevice.signMessage(message)?.let { sign ->
|
|
||||||
val mskUpdatedSignatures = (mskCrossSigningKeyInfo.signatures?.toMutableMap()
|
|
||||||
?: HashMap()).also {
|
|
||||||
it[userId] = (it[userId]
|
|
||||||
?: HashMap()) + mapOf("ed25519:${myDevice.deviceId}" to sign)
|
|
||||||
}
|
|
||||||
mskCrossSigningKeyInfo.copy(
|
|
||||||
signatures = mskUpdatedSignatures
|
|
||||||
).let {
|
|
||||||
uploadSignatureQueryBuilder.withSigningKeyInfo(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO should we ignore failure of that?
|
|
||||||
uploadSignaturesTask.execute(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build()))
|
|
||||||
|
|
||||||
return InitializeCrossSigningTask.Result(
|
|
||||||
masterKeyPK = masterKeyPrivateKey.toBase64NoPadding(),
|
|
||||||
userKeyPK = uskPrivateKey.toBase64NoPadding(),
|
|
||||||
selfSigningKeyPK = sskPrivateKey.toBase64NoPadding(),
|
|
||||||
masterKeyInfo = uploadSigningKeysParams.masterKey,
|
|
||||||
userKeyInfo = uploadSigningKeysParams.userKey,
|
|
||||||
selfSignedKeyInfo = uploadSigningKeysParams.selfSignedKey
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
masterPkOlm?.releaseSigning()
|
|
||||||
userSigningPkOlm?.releaseSigning()
|
|
||||||
selfSigningPkOlm?.releaseSigning()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,283 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.internal.crypto.verification.qrcode
|
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerificationTransaction
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
|
||||||
import org.matrix.android.sdk.api.util.fromBase64
|
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64Safe
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.ValidVerificationInfoStart
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
internal class DefaultQrCodeVerificationTransaction(
|
|
||||||
setDeviceVerificationAction: SetDeviceVerificationAction,
|
|
||||||
override val transactionId: String,
|
|
||||||
override val otherUserId: String,
|
|
||||||
override var otherDeviceId: String?,
|
|
||||||
private val crossSigningService: CrossSigningService,
|
|
||||||
outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
|
||||||
secretShareManager: SecretShareManager,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
// Not null only if other user is able to scan QR code
|
|
||||||
private val qrCodeData: QrCodeData?,
|
|
||||||
val userId: String,
|
|
||||||
val deviceId: String,
|
|
||||||
override val isIncoming: Boolean
|
|
||||||
) : DefaultVerificationTransaction(
|
|
||||||
setDeviceVerificationAction,
|
|
||||||
crossSigningService,
|
|
||||||
outgoingKeyRequestManager,
|
|
||||||
secretShareManager,
|
|
||||||
userId,
|
|
||||||
transactionId,
|
|
||||||
otherUserId,
|
|
||||||
otherDeviceId,
|
|
||||||
isIncoming
|
|
||||||
),
|
|
||||||
QrCodeVerificationTransaction {
|
|
||||||
|
|
||||||
override val qrCodeText: String?
|
|
||||||
get() = qrCodeData?.toEncodedString()
|
|
||||||
|
|
||||||
override var state: VerificationTxState = VerificationTxState.None
|
|
||||||
set(newState) {
|
|
||||||
field = newState
|
|
||||||
|
|
||||||
listeners.forEach {
|
|
||||||
try {
|
|
||||||
it.transactionUpdated(this)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Timber.e(e, "## Error while notifying listeners")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun userHasScannedOtherQrCode(otherQrCodeText: String) {
|
|
||||||
val otherQrCodeData = otherQrCodeText.toQrCodeData() ?: run {
|
|
||||||
Timber.d("## Verification QR: Invalid QR Code Data")
|
|
||||||
cancel(CancelCode.QrCodeInvalid)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform some checks
|
|
||||||
if (otherQrCodeData.transactionId != transactionId) {
|
|
||||||
Timber.d("## Verification QR: Invalid transaction actual ${otherQrCodeData.transactionId} expected:$transactionId")
|
|
||||||
cancel(CancelCode.QrCodeInvalid)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check master key
|
|
||||||
val myMasterKey = crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey
|
|
||||||
var canTrustOtherUserMasterKey = false
|
|
||||||
|
|
||||||
// Check the other device view of my MSK
|
|
||||||
when (otherQrCodeData) {
|
|
||||||
is QrCodeData.VerifyingAnotherUser -> {
|
|
||||||
// key2 (aka otherUserMasterCrossSigningPublicKey) is what the one displaying the QR code (other user) think my MSK is.
|
|
||||||
// Let's check that it's correct
|
|
||||||
// If not -> Cancel
|
|
||||||
if (otherQrCodeData.otherUserMasterCrossSigningPublicKey != myMasterKey) {
|
|
||||||
Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.otherUserMasterCrossSigningPublicKey}")
|
|
||||||
cancel(CancelCode.MismatchedKeys)
|
|
||||||
return
|
|
||||||
} else Unit
|
|
||||||
}
|
|
||||||
is QrCodeData.SelfVerifyingMasterKeyTrusted -> {
|
|
||||||
// key1 (aka userMasterCrossSigningPublicKey) is the session displaying the QR code view of our MSK.
|
|
||||||
// Let's check that I see the same MSK
|
|
||||||
// If not -> Cancel
|
|
||||||
if (otherQrCodeData.userMasterCrossSigningPublicKey != myMasterKey) {
|
|
||||||
Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}")
|
|
||||||
cancel(CancelCode.MismatchedKeys)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
// I can trust the MSK then (i see the same one, and other session tell me it's trusted by him)
|
|
||||||
canTrustOtherUserMasterKey = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> {
|
|
||||||
// key2 (aka userMasterCrossSigningPublicKey) is the session displaying the QR code view of our MSK.
|
|
||||||
// Let's check that it's the good one
|
|
||||||
// If not -> Cancel
|
|
||||||
if (otherQrCodeData.userMasterCrossSigningPublicKey != myMasterKey) {
|
|
||||||
Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}")
|
|
||||||
cancel(CancelCode.MismatchedKeys)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
// Nothing special here, we will send a reciprocate start event, and then the other session will trust it's view of the MSK
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val toVerifyDeviceIds = mutableListOf<String>()
|
|
||||||
|
|
||||||
// Let's now check the other user/device key material
|
|
||||||
when (otherQrCodeData) {
|
|
||||||
is QrCodeData.VerifyingAnotherUser -> {
|
|
||||||
// key1(aka userMasterCrossSigningPublicKey) is the MSK of the one displaying the QR code (i.e other user)
|
|
||||||
// Let's check that it matches what I think it should be
|
|
||||||
if (otherQrCodeData.userMasterCrossSigningPublicKey
|
|
||||||
!= crossSigningService.getUserCrossSigningKeys(otherUserId)?.masterKey()?.unpaddedBase64PublicKey) {
|
|
||||||
Timber.d("## Verification QR: Invalid user master key ${otherQrCodeData.userMasterCrossSigningPublicKey}")
|
|
||||||
cancel(CancelCode.MismatchedKeys)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
// It does so i should mark it as trusted
|
|
||||||
canTrustOtherUserMasterKey = true
|
|
||||||
Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is QrCodeData.SelfVerifyingMasterKeyTrusted -> {
|
|
||||||
// key2 (aka otherDeviceKey) is my current device key in POV of the one displaying the QR code (i.e other device)
|
|
||||||
// Let's check that it's correct
|
|
||||||
if (otherQrCodeData.otherDeviceKey
|
|
||||||
!= cryptoStore.getUserDevice(userId, deviceId)?.fingerprint()) {
|
|
||||||
Timber.d("## Verification QR: Invalid other device key ${otherQrCodeData.otherDeviceKey}")
|
|
||||||
cancel(CancelCode.MismatchedKeys)
|
|
||||||
return
|
|
||||||
} else Unit // Nothing special here, we will send a reciprocate start event, and then the other session will trust my device
|
|
||||||
// and thus allow me to request SSSS secret
|
|
||||||
}
|
|
||||||
is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> {
|
|
||||||
// key1 (aka otherDeviceKey) is the device key of the one displaying the QR code (i.e other device)
|
|
||||||
// Let's check that it matches what I have locally
|
|
||||||
if (otherQrCodeData.deviceKey
|
|
||||||
!= cryptoStore.getUserDevice(otherUserId, otherDeviceId ?: "")?.fingerprint()) {
|
|
||||||
Timber.d("## Verification QR: Invalid device key ${otherQrCodeData.deviceKey}")
|
|
||||||
cancel(CancelCode.MismatchedKeys)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
// Yes it does -> i should trust it and sign then upload the signature
|
|
||||||
toVerifyDeviceIds.add(otherDeviceId ?: "")
|
|
||||||
Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) {
|
|
||||||
// Nothing to verify
|
|
||||||
cancel(CancelCode.MismatchedKeys)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// All checks are correct
|
|
||||||
// Send the shared secret so that sender can trust me
|
|
||||||
// qrCodeData.sharedSecret will be used to send the start request
|
|
||||||
start(otherQrCodeData.sharedSecret)
|
|
||||||
|
|
||||||
trust(
|
|
||||||
canTrustOtherUserMasterKey = canTrustOtherUserMasterKey,
|
|
||||||
toVerifyDeviceIds = toVerifyDeviceIds.distinct(),
|
|
||||||
eventuallyMarkMyMasterKeyAsTrusted = true,
|
|
||||||
autoDone = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun start(remoteSecret: String, onDone: (() -> Unit)? = null) {
|
|
||||||
if (state != VerificationTxState.None) {
|
|
||||||
Timber.e("## Verification QR: start verification from invalid state")
|
|
||||||
// should I cancel??
|
|
||||||
throw IllegalStateException("Interactive Key verification already started")
|
|
||||||
}
|
|
||||||
|
|
||||||
state = VerificationTxState.Started
|
|
||||||
val startMessage = transport.createStartForQrCode(
|
|
||||||
deviceId,
|
|
||||||
transactionId,
|
|
||||||
remoteSecret
|
|
||||||
)
|
|
||||||
|
|
||||||
transport.sendToOther(
|
|
||||||
EventType.KEY_VERIFICATION_START,
|
|
||||||
startMessage,
|
|
||||||
VerificationTxState.WaitingOtherReciprocateConfirm,
|
|
||||||
CancelCode.User,
|
|
||||||
onDone
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel() {
|
|
||||||
cancel(CancelCode.User)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel(code: CancelCode) {
|
|
||||||
state = VerificationTxState.Cancelled(code, true)
|
|
||||||
transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isToDeviceTransport() = false
|
|
||||||
|
|
||||||
// Other user has scanned our QR code. check that the secret matched, so we can trust him
|
|
||||||
fun onStartReceived(startReq: ValidVerificationInfoStart.ReciprocateVerificationInfoStart) {
|
|
||||||
if (qrCodeData == null) {
|
|
||||||
// Should not happen
|
|
||||||
cancel(CancelCode.UnexpectedMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startReq.sharedSecret.fromBase64Safe()?.contentEquals(qrCodeData.sharedSecret.fromBase64()) == true) {
|
|
||||||
// Ok, we can trust the other user
|
|
||||||
// We can only trust the master key in this case
|
|
||||||
// But first, ask the user for a confirmation
|
|
||||||
state = VerificationTxState.QrScannedByOther
|
|
||||||
} else {
|
|
||||||
// Display a warning
|
|
||||||
cancel(CancelCode.MismatchedKeys)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDoneReceived() {
|
|
||||||
if (state != VerificationTxState.WaitingOtherReciprocateConfirm) {
|
|
||||||
cancel(CancelCode.UnexpectedMessage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
state = VerificationTxState.Verified
|
|
||||||
transport.done(transactionId) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun otherUserScannedMyQrCode() {
|
|
||||||
when (qrCodeData) {
|
|
||||||
is QrCodeData.VerifyingAnotherUser -> {
|
|
||||||
// Alice telling Bob that the code was scanned successfully is sufficient for Bob to trust Alice's key,
|
|
||||||
trust(true, emptyList(), false)
|
|
||||||
}
|
|
||||||
is QrCodeData.SelfVerifyingMasterKeyTrusted -> {
|
|
||||||
// I now know that I have the correct device key for other session,
|
|
||||||
// and can sign it with the self-signing key and upload the signature
|
|
||||||
trust(false, listOf(otherDeviceId ?: ""), false)
|
|
||||||
}
|
|
||||||
is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> {
|
|
||||||
// I now know that i can trust my MSK
|
|
||||||
trust(true, emptyList(), true)
|
|
||||||
}
|
|
||||||
null -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun otherUserDidNotScannedMyQrCode() {
|
|
||||||
// What can I do then?
|
|
||||||
// At least remove the transaction...
|
|
||||||
cancel(CancelCode.MismatchedKeys)
|
|
||||||
}
|
|
||||||
}
|
|
@ -20,15 +20,14 @@ import org.matrix.android.sdk.api.session.ToDeviceService
|
|||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
import org.matrix.android.sdk.api.session.events.model.Content
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
import org.matrix.android.sdk.internal.crypto.EncryptEventContentUseCase
|
||||||
|
import org.matrix.android.sdk.internal.crypto.OlmMachineProvider
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class DefaultToDeviceService @Inject constructor(
|
internal class DefaultToDeviceService @Inject constructor(
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
private val sendToDeviceTask: SendToDeviceTask,
|
||||||
private val messageEncrypter: MessageEncrypter,
|
|
||||||
private val cryptoStore: IMXCryptoStore
|
|
||||||
) : ToDeviceService {
|
) : ToDeviceService {
|
||||||
|
|
||||||
override suspend fun sendToDevice(eventType: String, targets: Map<String, List<String>>, content: Content, txnId: String?) {
|
override suspend fun sendToDevice(eventType: String, targets: Map<String, List<String>>, content: Content, txnId: String?) {
|
||||||
@ -53,6 +52,8 @@ internal class DefaultToDeviceService @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sendEncryptedToDevice(eventType: String, targets: Map<String, List<String>>, content: Content, txnId: String?) {
|
override suspend fun sendEncryptedToDevice(eventType: String, targets: Map<String, List<String>>, content: Content, txnId: String?) {
|
||||||
|
//TODO: add to rust-ffi
|
||||||
|
/*
|
||||||
val payloadJson = mapOf(
|
val payloadJson = mapOf(
|
||||||
"type" to eventType,
|
"type" to eventType,
|
||||||
"content" to content
|
"content" to content
|
||||||
@ -63,11 +64,13 @@ internal class DefaultToDeviceService @Inject constructor(
|
|||||||
targets.forEach { (userId, deviceIdList) ->
|
targets.forEach { (userId, deviceIdList) ->
|
||||||
deviceIdList.forEach { deviceId ->
|
deviceIdList.forEach { deviceId ->
|
||||||
cryptoStore.getUserDevice(userId, deviceId)?.let { deviceInfo ->
|
cryptoStore.getUserDevice(userId, deviceId)?.let { deviceInfo ->
|
||||||
sendToDeviceMap.setObject(userId, deviceId, messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)))
|
sendToDeviceMap.setObject(userId, deviceId, encryptEventContent(payloadJson, listOf(deviceInfo)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToDevice(EventType.ENCRYPTED, sendToDeviceMap, txnId)
|
sendToDevice(EventType.ENCRYPTED, sendToDeviceMap, txnId)
|
||||||
|
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@ import org.matrix.android.sdk.api.auth.data.SessionParams
|
|||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.internal.crypto.CryptoModule
|
import org.matrix.android.sdk.internal.crypto.CryptoModule
|
||||||
import org.matrix.android.sdk.internal.crypto.OlmMachineProvider
|
import org.matrix.android.sdk.internal.crypto.OlmMachineProvider
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
|
|
||||||
import org.matrix.android.sdk.internal.di.MatrixComponent
|
import org.matrix.android.sdk.internal.di.MatrixComponent
|
||||||
import org.matrix.android.sdk.internal.federation.FederationModule
|
import org.matrix.android.sdk.internal.federation.FederationModule
|
||||||
import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
|
import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
|
||||||
@ -132,8 +131,6 @@ internal interface SessionComponent {
|
|||||||
|
|
||||||
fun inject(worker: AddPusherWorker)
|
fun inject(worker: AddPusherWorker)
|
||||||
|
|
||||||
fun inject(worker: UpdateTrustWorker)
|
|
||||||
|
|
||||||
@Component.Factory
|
@Component.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
|
@ -40,7 +40,7 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
|||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary
|
import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary
|
||||||
import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications
|
import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications
|
||||||
import org.matrix.android.sdk.internal.crypto.EventDecryptor
|
import org.matrix.android.sdk.internal.crypto.OlmMachineProvider
|
||||||
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
||||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||||
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
|
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
|
||||||
@ -72,11 +72,12 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
private val roomDisplayNameResolver: RoomDisplayNameResolver,
|
private val roomDisplayNameResolver: RoomDisplayNameResolver,
|
||||||
private val roomAvatarResolver: RoomAvatarResolver,
|
private val roomAvatarResolver: RoomAvatarResolver,
|
||||||
private val crossSigningService: CrossSigningService,
|
private val olmMachineProvider: OlmMachineProvider,
|
||||||
private val eventDecryptor: EventDecryptor,
|
|
||||||
private val roomAccountDataDataSource: RoomAccountDataDataSource
|
private val roomAccountDataDataSource: RoomAccountDataDataSource
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val olmMachine = olmMachineProvider.olmMachine
|
||||||
|
|
||||||
fun refreshLatestPreviewContent(realm: Realm, roomId: String) {
|
fun refreshLatestPreviewContent(realm: Realm, roomId: String) {
|
||||||
val roomSummaryEntity = RoomSummaryEntity.getOrNull(realm, roomId)
|
val roomSummaryEntity = RoomSummaryEntity.getOrNull(realm, roomId)
|
||||||
if (roomSummaryEntity != null) {
|
if (roomSummaryEntity != null) {
|
||||||
@ -194,7 +195,7 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||||||
if (root.type == EventType.ENCRYPTED && root.decryptionResultJson == null) {
|
if (root.type == EventType.ENCRYPTED && root.decryptionResultJson == null) {
|
||||||
Timber.v("Should decrypt $eventId")
|
Timber.v("Should decrypt $eventId")
|
||||||
tryOrNull {
|
tryOrNull {
|
||||||
runBlocking { eventDecryptor.decryptEvent(root.asDomain(), "") }
|
runBlocking { olmMachine.decryptRoomEvent(root.asDomain()) }
|
||||||
}?.let { root.setDecryptionResult(it) }
|
}?.let { root.setDecryptionResult(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,7 @@ package org.matrix.android.sdk.internal.session.room.timeline
|
|||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
|
import org.matrix.android.sdk.internal.crypto.DecryptEventUseCase
|
||||||
import org.matrix.android.sdk.internal.crypto.EventDecryptor
|
|
||||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||||
import org.matrix.android.sdk.internal.network.executeRequest
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||||
@ -38,7 +37,7 @@ internal interface GetEventTask : Task<GetEventTask.Params, Event> {
|
|||||||
internal class DefaultGetEventTask @Inject constructor(
|
internal class DefaultGetEventTask @Inject constructor(
|
||||||
private val roomAPI: RoomAPI,
|
private val roomAPI: RoomAPI,
|
||||||
private val globalErrorReceiver: GlobalErrorReceiver,
|
private val globalErrorReceiver: GlobalErrorReceiver,
|
||||||
private val cryptoService: DefaultCryptoService,
|
private val decryptEvent: DecryptEventUseCase,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
) : GetEventTask {
|
) : GetEventTask {
|
||||||
|
|
||||||
@ -50,7 +49,7 @@ internal class DefaultGetEventTask @Inject constructor(
|
|||||||
// Try to decrypt the Event
|
// Try to decrypt the Event
|
||||||
if (event.isEncrypted()) {
|
if (event.isEncrypted()) {
|
||||||
tryOrNull(message = "Unable to decrypt the event") {
|
tryOrNull(message = "Unable to decrypt the event") {
|
||||||
cryptoService.decryptEvent(event, "")
|
decryptEvent(event)
|
||||||
}
|
}
|
||||||
?.let { result ->
|
?.let { result ->
|
||||||
event.mxDecryptionResult = OlmDecryptionResult(
|
event.mxDecryptionResult = OlmDecryptionResult(
|
||||||
|
@ -42,7 +42,7 @@ internal class TimelineEventDecryptor @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
private val newSessionListener = object : NewSessionListener {
|
private val newSessionListener = object : NewSessionListener {
|
||||||
override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
|
override fun onNewSession(roomId: String?, sessionId: String) {
|
||||||
synchronized(unknownSessionsFailure) {
|
synchronized(unknownSessionsFailure) {
|
||||||
unknownSessionsFailure[sessionId]
|
unknownSessionsFailure[sessionId]
|
||||||
?.toList()
|
?.toList()
|
||||||
|
@ -36,6 +36,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_S
|
|||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
||||||
|
@ -90,7 +90,7 @@ class HomeDetailViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val refreshRoomSummariesOnCryptoSessionChange = object : NewSessionListener {
|
private val refreshRoomSummariesOnCryptoSessionChange = object : NewSessionListener {
|
||||||
override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
|
override fun onNewSession(roomId: String?, sessionId: String) {
|
||||||
session.roomService().refreshJoinedRoomSummaryPreviews(roomId)
|
session.roomService().refreshJoinedRoomSummaryPreviews(roomId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,6 +72,7 @@ import kotlinx.coroutines.launch
|
|||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import org.matrix.android.sdk.api.extensions.getFingerprintHumanReadable
|
import org.matrix.android.sdk.api.extensions.getFingerprintHumanReadable
|
||||||
import org.matrix.android.sdk.api.raw.RawService
|
import org.matrix.android.sdk.api.raw.RawService
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
|||||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
|
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
|
||||||
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
|
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
|
||||||
import org.matrix.android.sdk.api.util.fromBase64
|
import org.matrix.android.sdk.api.util.fromBase64
|
||||||
import org.matrix.android.sdk.flow.flow
|
import org.matrix.android.sdk.flow.flow
|
||||||
|
@ -18,6 +18,7 @@ package im.vector.app.features.signout.soft
|
|||||||
|
|
||||||
import com.airbnb.epoxy.EpoxyController
|
import com.airbnb.epoxy.EpoxyController
|
||||||
import com.airbnb.mvrx.Fail
|
import com.airbnb.mvrx.Fail
|
||||||
|
import com.airbnb.mvrx.Incomplete
|
||||||
import com.airbnb.mvrx.Loading
|
import com.airbnb.mvrx.Loading
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
|
Loading…
Reference in New Issue
Block a user