Try to clean up after merging upstream develop

This commit is contained in:
ganfra 2022-05-13 17:26:26 +02:00
parent 725e56db08
commit 7e49bad411
65 changed files with 330 additions and 7290 deletions

View File

@ -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 {

View File

@ -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)
}

View File

@ -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 {

View File

@ -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
*

View File

@ -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
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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}")
}
}
}
}
}
}

View File

@ -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))
}
}

View File

@ -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")
}
}
}

View File

@ -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()
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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) }
}
}
}

View File

@ -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}")

View File

@ -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")
}
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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
}
}
}
}

View File

@ -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

View File

@ -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]

View File

@ -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)
}
}

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
)
}
}

View File

@ -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)
}
}
}

View File

@ -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) {}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
)
}

View File

@ -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,
)
}
}

View File

@ -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
}
}

View File

@ -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
)
}
}

View File

@ -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"]
}
}

View File

@ -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)
}
}

View File

@ -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
)
}
}

View File

@ -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()
}
}

View File

@ -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())
}
}
}
}
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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

View File

@ -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? {
suspend fun getKeyBackupVersion(version: String): KeysVersionResult? = getKeyBackupVersion {
getKeysBackupVersionTask.executeRetry(version, 3)
}
suspend fun getKeyBackupLastVersion(): KeysBackupLastVersionResult? = getKeyBackupVersion {
getKeysBackupLastVersionTask.executeRetry(Unit, 3)
}
private inline fun <reified T> getKeyBackupVersion(block: ()-> T?): T?{
return try {
if (version != null) {
getKeysBackupVersionTask.executeRetry(version, 3)
} else {
getKeysBackupLastVersionTask.executeRetry(Unit, 3)
}
block()
} catch (failure: Throwable) {
if (failure is Failure.ServerError &&
failure.error.code == MatrixError.M_NOT_FOUND) {

View File

@ -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()
}
}
}

View File

@ -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)
}
}

View File

@ -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)
*/
}
}

View File

@ -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(

View File

@ -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) }
}
}

View File

@ -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(

View File

@ -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()

View File

@ -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

View File

@ -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)
}
}

View File

@ -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

View File

@ -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

View File

@ -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