mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-26 15:18:19 +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.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.internal.crypto.NewSessionListener
|
||||
|
||||
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 senderKey the sender key of the device which the Megolm session is shared with
|
||||
* @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 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
|
||||
|
||||
interface CrossSigningService {
|
||||
|
@ -104,6 +104,11 @@ class MXUsersDevicesMap<E> {
|
||||
map.clear()
|
||||
}
|
||||
|
||||
fun join(other: Map<out String, HashMap<String, E>>) {
|
||||
map.putAll(other)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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.verification.VerificationService
|
||||
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.tasks.CreateKeysBackupVersionTask
|
||||
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.DefaultGetDeviceInfoTask
|
||||
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.DefaultSendToDeviceTask
|
||||
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.GetDeviceInfoTask
|
||||
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.SendToDeviceTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
|
||||
@ -247,12 +243,6 @@ internal abstract class CryptoModule {
|
||||
@Binds
|
||||
abstract fun bindCryptoStore(store: RealmCryptoStore): IMXCryptoStore
|
||||
|
||||
@Binds
|
||||
abstract fun bindComputeShieldTrustTask(task: DefaultComputeTrustTask): ComputeTrustTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindInitializeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
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.internal.database.model.EventEntity
|
||||
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");
|
||||
* 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
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
interface NewSessionListener {
|
||||
fun onNewSession(roomId: String?, sessionId: String)
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
||||
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 megolmSessionImportManager: MegolmSessionImportManager,
|
||||
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 {
|
||||
|
||||
private val isStarting = AtomicBoolean(false)
|
||||
@ -152,14 +156,12 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
// private val deviceObserver: DeviceUpdateObserver = DeviceUpdateObserver()
|
||||
|
||||
// Locks for some of our operations
|
||||
private val keyClaimLock: Mutex = Mutex()
|
||||
private val outgoingRequestsProcessor = OutgoingRequestsProcessor(
|
||||
requestSender = requestSender,
|
||||
coroutineScope = cryptoCoroutineScope,
|
||||
cryptoSessionInfoProvider = cryptoSessionInfoProvider,
|
||||
shieldComputer = crossSigningService::shieldForGroup
|
||||
)
|
||||
private val roomKeyShareLocks: ConcurrentHashMap<String, Mutex> = ConcurrentHashMap()
|
||||
|
||||
fun onStateEvent(roomId: String, event: Event) {
|
||||
when (event.type) {
|
||||
@ -450,27 +452,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
override suspend fun encryptEventContent(eventContent: Content,
|
||||
eventType: String,
|
||||
roomId: String): MXEncryptEventContentResult {
|
||||
// moved to crypto scope to have up to date values
|
||||
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))
|
||||
}
|
||||
}
|
||||
return encryptEventContent.invoke(eventContent, eventType, roomId)
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
@ -593,7 +569,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
when (event.type) {
|
||||
EventType.ROOM_KEY -> {
|
||||
val content = event.getClearContent().toModel<RoomKeyContent>() ?: return@forEach
|
||||
|
||||
content.sessionKey
|
||||
val roomId = content.sessionId ?: return@forEach
|
||||
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) {
|
||||
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
|
||||
*
|
||||
@ -966,32 +841,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
cryptoStore.logDbUsageInfo()
|
||||
}
|
||||
|
||||
override suspend fun prepareToEncrypt(roomId: String) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
override suspend fun prepareToEncrypt(roomId: String) = prepareToEncrypt.invoke(roomId, ensureAllMembersAreLoaded = true)
|
||||
|
||||
/* ==========================================================================================
|
||||
* For test only
|
||||
|
@ -18,11 +18,11 @@ package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import kotlinx.coroutines.withContext
|
||||
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.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.verification.prepareMethods
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -14,19 +14,14 @@
|
||||
* 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
|
||||
|
||||
internal class MXOlmDecryptionFactory @Inject constructor(private val olmDevice: MXOlmDevice,
|
||||
@UserId private val userId: String) {
|
||||
internal class GetRoomUserIdsUseCase @Inject constructor(private val shouldEncryptForInvitedMembers: ShouldEncryptForInvitedMembersUseCase,
|
||||
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider) {
|
||||
|
||||
fun create(): MXOlmDecryption {
|
||||
return MXOlmDecryption(
|
||||
olmDevice,
|
||||
userId
|
||||
)
|
||||
operator fun invoke(roomId: String): List<String> {
|
||||
return cryptoSessionInfoProvider.getRoomUserIds(roomId, shouldEncryptForInvitedMembers(roomId))
|
||||
}
|
||||
}
|
@ -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 org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
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.internal.session.SessionScope
|
||||
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.model.ImportRoomKeysResult
|
||||
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.util.time.Clock
|
||||
import timber.log.Timber
|
||||
@ -39,7 +40,7 @@ private val loggerTag = LoggerTag("OutgoingGossipingRequestManager", LoggerTag.C
|
||||
*/
|
||||
internal class PerSessionBackupQueryRateLimiter @Inject constructor(
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val keysBackupService: Lazy<DefaultKeysBackupService>,
|
||||
private val keysBackupService: Lazy<RustKeyBackupService>,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val clock: Clock,
|
||||
) {
|
||||
@ -103,16 +104,14 @@ internal class PerSessionBackupQueryRateLimiter @Inject constructor(
|
||||
|
||||
val successfullyImported = withContext(coroutineDispatchers.io) {
|
||||
try {
|
||||
awaitCallback<ImportRoomKeysResult> {
|
||||
keysBackupService.get().restoreKeysWithRecoveryKey(
|
||||
currentVersion,
|
||||
savedKeyBackupKeyInfo?.recoveryKey ?: "",
|
||||
roomId,
|
||||
sessionId,
|
||||
null,
|
||||
it
|
||||
)
|
||||
}.successfullyNumberOfImportedKeys
|
||||
.successfullyNumberOfImportedKeys
|
||||
} catch (failure: Throwable) {
|
||||
// Fail silently
|
||||
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.VerificationTxState
|
||||
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.verification.UpdateDispatcher
|
||||
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 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.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.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.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 javax.inject.Inject
|
||||
|
||||
|
@ -24,11 +24,6 @@ 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.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.model.MXUsersDevicesMap
|
||||
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.content.SecretSendEventContent
|
||||
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.tasks.SendToDeviceTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
|
||||
@ -54,8 +46,6 @@ internal class SecretShareManager @Inject constructor(
|
||||
private val credentials: Credentials,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val clock: Clock,
|
||||
@ -107,6 +97,7 @@ internal class SecretShareManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
suspend fun handleSecretRequest(toDevice: Event) {
|
||||
val request = toDevice.getClearContent().toModel<SecretShareRequest>()
|
||||
?: return Unit.also {
|
||||
@ -214,6 +205,8 @@ internal class SecretShareManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
private suspend fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean {
|
||||
val verifTimestamp = verifMutex.withLock {
|
||||
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 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.verification.VerificationMethod
|
||||
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.verification.prepareMethods
|
||||
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.VerificationService
|
||||
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.network.RequestSender
|
||||
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.withContext
|
||||
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.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||
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.KeysBackupState
|
||||
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.MegolmSessionImportManager
|
||||
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.rest.CreateKeysBackupVersionBody
|
||||
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.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.model.ImportRoomKeysResult
|
||||
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.session.SessionScope
|
||||
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
||||
@ -263,7 +265,7 @@ internal class RustKeyBackupService @Inject constructor(
|
||||
private suspend fun checkBackupTrust(authData: MegolmBackupAuthData?): KeysBackupVersionTrust {
|
||||
return if (authData == null || authData.publicKey.isEmpty() || authData.signatures.isNullOrEmpty()) {
|
||||
Timber.v("getKeysBackupTrust: Key backup is absent or missing required data")
|
||||
KeysBackupVersionTrust()
|
||||
KeysBackupVersionTrust(usable = false)
|
||||
} else {
|
||||
KeysBackupVersionTrust(olmMachine.checkAuthDataSignature(authData))
|
||||
}
|
||||
@ -375,7 +377,7 @@ internal class RustKeyBackupService @Inject constructor(
|
||||
Timber.i("## CrossSigning - onSecretKeyGossip")
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
try {
|
||||
val version = sender.getKeyBackupVersion()
|
||||
val version = sender.getKeyBackupLastVersion()?.toKeysVersionResult()
|
||||
|
||||
if (version != null) {
|
||||
val key = BackupRecoveryKey.fromBase64(secret)
|
||||
@ -584,13 +586,13 @@ internal class RustKeyBackupService @Inject constructor(
|
||||
}
|
||||
|
||||
@Throws
|
||||
override suspend fun getCurrentVersion(): KeysVersionResult? {
|
||||
return sender.getKeyBackupVersion()
|
||||
override suspend fun getCurrentVersion(): KeysBackupLastVersionResult? {
|
||||
return sender.getKeyBackupLastVersion()
|
||||
}
|
||||
|
||||
override suspend fun forceUsingLastVersion(): Boolean {
|
||||
val response = withContext(coroutineDispatchers.io) {
|
||||
sender.getKeyBackupVersion()
|
||||
sender.getKeyBackupLastVersion()?.toKeysVersionResult()
|
||||
}
|
||||
|
||||
return withContext(coroutineDispatchers.crypto) {
|
||||
@ -644,7 +646,7 @@ internal class RustKeyBackupService @Inject constructor(
|
||||
keysBackupVersion = null
|
||||
keysBackupStateManager.state = KeysBackupState.CheckingBackUpOnHomeserver
|
||||
try {
|
||||
val data = getCurrentVersion()
|
||||
val data = getCurrentVersion()?.toKeysVersionResult()
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
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? {
|
||||
val info = olmMachine.getBackupKeys() ?: return null
|
||||
return SavedKeyBackupKeyInfo(info.recoveryKey, info.backupVersion)
|
||||
|
@ -23,8 +23,8 @@ import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
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.session.crypto.model.RoomEncryptionTrustLevel
|
||||
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
|
||||
import org.matrix.android.sdk.internal.crypto.OlmMachine
|
||||
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.failure.Failure
|
||||
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.Event
|
||||
import org.matrix.android.sdk.api.session.uia.UiaResult
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
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.CreateKeysBackupVersionBody
|
||||
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.UpdateKeysBackupVersionBody
|
||||
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.StoreSessionsDataTask
|
||||
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.KeysQueryResponse
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse
|
||||
@ -177,7 +179,7 @@ internal class RequestSender @Inject constructor(
|
||||
uploadSigningKeysTask.execute(uploadSigningKeysParams)
|
||||
} catch (failure: Throwable) {
|
||||
if (interactiveAuthInterceptor == null ||
|
||||
!handleUIA(
|
||||
handleUIA(
|
||||
failure = failure,
|
||||
interceptor = interactiveAuthInterceptor,
|
||||
retryBlock = { authUpdate ->
|
||||
@ -186,7 +188,7 @@ internal class RequestSender @Inject constructor(
|
||||
REQUEST_RETRY_COUNT
|
||||
)
|
||||
}
|
||||
)
|
||||
) != UiaResult.SUCCESS
|
||||
) {
|
||||
Timber.d("## UIA: propagate failure")
|
||||
throw failure
|
||||
@ -217,13 +219,17 @@ internal class RequestSender @Inject constructor(
|
||||
sendToDeviceTask.executeRetry(sendToDeviceParams, REQUEST_RETRY_COUNT)
|
||||
}
|
||||
|
||||
suspend fun getKeyBackupVersion(version: String? = null): KeysVersionResult? {
|
||||
return try {
|
||||
if (version != null) {
|
||||
suspend fun getKeyBackupVersion(version: String): KeysVersionResult? = getKeyBackupVersion {
|
||||
getKeysBackupVersionTask.executeRetry(version, 3)
|
||||
} else {
|
||||
}
|
||||
|
||||
suspend fun getKeyBackupLastVersion(): KeysBackupLastVersionResult? = getKeyBackupVersion {
|
||||
getKeysBackupLastVersionTask.executeRetry(Unit, 3)
|
||||
}
|
||||
|
||||
private inline fun <reified T> getKeyBackupVersion(block: ()-> T?): T?{
|
||||
return try {
|
||||
block()
|
||||
} catch (failure: Throwable) {
|
||||
if (failure is Failure.ServerError &&
|
||||
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.events.model.Content
|
||||
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.tasks.SendToDeviceTask
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultToDeviceService @Inject constructor(
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val cryptoStore: IMXCryptoStore
|
||||
) : ToDeviceService {
|
||||
|
||||
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?) {
|
||||
//TODO: add to rust-ffi
|
||||
/*
|
||||
val payloadJson = mapOf(
|
||||
"type" to eventType,
|
||||
"content" to content
|
||||
@ -63,11 +64,13 @@ internal class DefaultToDeviceService @Inject constructor(
|
||||
targets.forEach { (userId, deviceIdList) ->
|
||||
deviceIdList.forEach { deviceId ->
|
||||
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)
|
||||
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
@ -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.internal.crypto.CryptoModule
|
||||
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.federation.FederationModule
|
||||
import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
|
||||
@ -132,8 +131,6 @@ internal interface SessionComponent {
|
||||
|
||||
fun inject(worker: AddPusherWorker)
|
||||
|
||||
fun inject(worker: UpdateTrustWorker)
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
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.sync.model.RoomSyncSummary
|
||||
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.asDomain
|
||||
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
|
||||
@ -72,11 +72,12 @@ internal class RoomSummaryUpdater @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
private val roomDisplayNameResolver: RoomDisplayNameResolver,
|
||||
private val roomAvatarResolver: RoomAvatarResolver,
|
||||
private val crossSigningService: CrossSigningService,
|
||||
private val eventDecryptor: EventDecryptor,
|
||||
private val olmMachineProvider: OlmMachineProvider,
|
||||
private val roomAccountDataDataSource: RoomAccountDataDataSource
|
||||
) {
|
||||
|
||||
private val olmMachine = olmMachineProvider.olmMachine
|
||||
|
||||
fun refreshLatestPreviewContent(realm: Realm, roomId: String) {
|
||||
val roomSummaryEntity = RoomSummaryEntity.getOrNull(realm, roomId)
|
||||
if (roomSummaryEntity != null) {
|
||||
@ -194,7 +195,7 @@ internal class RoomSummaryUpdater @Inject constructor(
|
||||
if (root.type == EventType.ENCRYPTED && root.decryptionResultJson == null) {
|
||||
Timber.v("Should decrypt $eventId")
|
||||
tryOrNull {
|
||||
runBlocking { eventDecryptor.decryptEvent(root.asDomain(), "") }
|
||||
runBlocking { olmMachine.decryptRoomEvent(root.asDomain()) }
|
||||
}?.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.session.crypto.model.OlmDecryptionResult
|
||||
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.EventDecryptor
|
||||
import org.matrix.android.sdk.internal.crypto.DecryptEventUseCase
|
||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
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(
|
||||
private val roomAPI: RoomAPI,
|
||||
private val globalErrorReceiver: GlobalErrorReceiver,
|
||||
private val cryptoService: DefaultCryptoService,
|
||||
private val decryptEvent: DecryptEventUseCase,
|
||||
private val clock: Clock,
|
||||
) : GetEventTask {
|
||||
|
||||
@ -50,7 +49,7 @@ internal class DefaultGetEventTask @Inject constructor(
|
||||
// Try to decrypt the Event
|
||||
if (event.isEncrypted()) {
|
||||
tryOrNull(message = "Unable to decrypt the event") {
|
||||
cryptoService.decryptEvent(event, "")
|
||||
decryptEvent(event)
|
||||
}
|
||||
?.let { result ->
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
|
@ -42,7 +42,7 @@ internal class TimelineEventDecryptor @Inject constructor(
|
||||
) {
|
||||
|
||||
private val newSessionListener = object : NewSessionListener {
|
||||
override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
|
||||
override fun onNewSession(roomId: String?, sessionId: String) {
|
||||
synchronized(unknownSessionsFailure) {
|
||||
unknownSessionsFailure[sessionId]
|
||||
?.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.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.isVerified
|
||||
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.verification.CancelCode
|
||||
|
@ -90,7 +90,7 @@ class HomeDetailViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -72,6 +72,7 @@ import kotlinx.coroutines.launch
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.extensions.getFingerprintHumanReadable
|
||||
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 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.nextUncompletedStage
|
||||
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.util.fromBase64
|
||||
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.mvrx.Fail
|
||||
import com.airbnb.mvrx.Incomplete
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
|
Loading…
Reference in New Issue
Block a user