Code review

This commit is contained in:
Valere 2022-04-27 09:45:26 +02:00
parent 728faaee19
commit 8920ed3de8
14 changed files with 1797 additions and 1865 deletions

View file

@ -432,6 +432,7 @@ class KeyShareTests : InstrumentedTest {
.markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!)
// /!\ Stop initial alice session syncing so that it can't reply
aliceSession.cryptoService().enableKeyGossiping(false)
aliceSession.stopSync()
// Let's now try to request
@ -440,6 +441,7 @@ class KeyShareTests : InstrumentedTest {
// Should get a reply from bob and not from alice
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
// Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}")
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId }
val result = bobReply?.result
@ -453,6 +455,7 @@ class KeyShareTests : InstrumentedTest {
assertEquals("The request should not be canceled", OutgoingRoomKeyRequestState.SENT, outgoingReq.state)
// let's wake up alice
aliceSession.cryptoService().enableKeyGossiping(true)
aliceSession.startSync(true)
// We should now get a reply from first session

View file

@ -37,5 +37,5 @@ data class MXCryptoConfig constructor(
* Currently megolm keys are requested to the sender device and to all of our devices.
* You can limit request only to your sessions by turning this setting to `true`
*/
val limitRoomKeyRequestsToMyDevices: Boolean = false
val limitRoomKeyRequestsToMyDevices: Boolean = false,
)

View file

@ -1192,7 +1192,7 @@ internal class DefaultCryptoService @Inject constructor(
*/
override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
incomingKeyRequestManager.removeRoomKeysRequestListener(listener)
secretShareManager.addListener(listener)
secretShareManager.removeListener(listener)
}
/**

View file

@ -1,27 +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.session.crypto.OutgoingRoomKeyRequestState
interface OutgoingGossipingRequest {
var recipients: Map<String, List<String>>
var requestId: String
var state: OutgoingRoomKeyRequestState
// transaction id for the cancellation, if any
// var cancellationTxnId: String?
}

View file

@ -1,39 +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 com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
/**
* Represents an outgoing room key request
*/
@JsonClass(generateAdapter = true)
internal class OutgoingSecretRequest(
// Secret Name
val secretName: String?,
// list of recipients for the request
override var recipients: Map<String, List<String>>,
// Unique id for this request. Used for both
// an id within the request for later pairing with a cancellation, and for
// the transaction id when sending the to_device messages to our local
override var requestId: String,
// current state of this request
override var state: OutgoingRoomKeyRequestState) : OutgoingGossipingRequest {
// transaction id for the cancellation, if any
}

View file

@ -66,7 +66,7 @@ internal class PerSessionBackupQueryRateLimiter @Inject constructor(
private var backupVersion: KeysVersionResult? = null
private var savedKeyBackupKeyInfo: SavedKeyBackupKeyInfo? = null
var backupWasCheckedFromServer: Boolean = false
var now = System.currentTimeMillis()
val now = System.currentTimeMillis()
fun refreshBackupInfoIfNeeded(force: Boolean = false) {
if (backupWasCheckedFromServer && !force) return

View file

@ -28,7 +28,6 @@ import javax.inject.Inject
@SessionScope
internal class RoomEncryptorsStore @Inject constructor(
private val cryptoStore: IMXCryptoStore,
// Repository
private val megolmEncryptionFactory: MXMegolmEncryptionFactory,
private val olmEncryptionFactory: MXOlmEncryptionFactory,
) {

View file

@ -1,298 +1,298 @@
/*
* Copyright (c) 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.launch
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.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
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
import org.matrix.android.sdk.internal.session.SessionScope
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("SecretShareManager", LoggerTag.CRYPTO)
@SessionScope
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
) {
companion object {
private const val SECRET_SHARE_WINDOW_DURATION = 5 * 60 * 1000 // 5 minutes
}
/**
* Secret gossiping only occurs during a limited window period after interactive verification.
* We keep track of recent verification in memory for that purpose (no need to persist)
*/
private val recentlyVerifiedDevices = mutableMapOf<String, Long>()
private val verifMutex = Mutex()
/**
* Secrets are exchanged as part of interactive verification,
* so we can just store in memory.
*/
private val outgoingSecretRequests = mutableListOf<SecretShareRequest>()
// the listeners
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet()
fun addListener(listener: GossipingRequestListener) {
synchronized(gossipingRequestListeners) {
gossipingRequestListeners.add(listener)
}
}
fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
synchronized(gossipingRequestListeners) {
gossipingRequestListeners.remove(listener)
}
}
/**
* Called when a session has been verified.
* This information can be used by the manager to decide whether or not to fullfill gossiping requests.
* This should be called as fast as possible after a successful self interactive verification
*/
fun onVerificationCompleteForDevice(deviceId: String) {
// For now we just keep an in memory cache
cryptoCoroutineScope.launch {
verifMutex.withLock {
recentlyVerifiedDevices[deviceId] = System.currentTimeMillis()
}
}
}
suspend fun handleSecretRequest(toDevice: Event) {
val request = toDevice.getClearContent().toModel<SecretShareRequest>()
?: return Unit.also {
Timber.tag(loggerTag.value)
.w("handleSecretRequest() : malformed request")
}
// val (action, requestingDeviceId, requestId, secretName) = it
val secretName = request.secretName ?: return Unit.also {
Timber.tag(loggerTag.value)
.v("handleSecretRequest() : Missing secret name")
}
val userId = toDevice.senderId ?: return Unit.also {
Timber.tag(loggerTag.value)
.v("handleSecretRequest() : Missing secret name")
}
if (userId != credentials.userId) {
// secrets are only shared between our own devices
Timber.tag(loggerTag.value)
.e("Ignoring secret share request from other users $userId")
return
}
val deviceId = request.requestingDeviceId
?: return Unit.also {
Timber.tag(loggerTag.value)
.w("handleSecretRequest() : malformed request norequestingDeviceId ")
}
val device = cryptoStore.getUserDevice(credentials.userId, deviceId)
?: return Unit.also {
Timber.tag(loggerTag.value)
.e("Received secret share request from unknown device $deviceId")
}
val isRequestingDeviceTrusted = device.isVerified
val isRecentInteractiveVerification = hasBeenVerifiedLessThanFiveMinutesFromNow(device.deviceId)
if (isRequestingDeviceTrusted && isRecentInteractiveVerification) {
// we can share the secret
val secretValue = when (secretName) {
MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master
SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned
USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user
KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey
?.let {
extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding()
}
else -> null
}
if (secretValue == null) {
Timber.tag(loggerTag.value)
.i("The secret is unknown $secretName, passing to app layer")
val toList = synchronized(gossipingRequestListeners) { gossipingRequestListeners.toList() }
toList.onEach { listener ->
listener.onSecretShareRequest(request)
}
return
}
val payloadJson = mapOf(
"type" to EventType.SEND_SECRET,
"content" to mapOf(
"request_id" to request.requestId,
"secret" to secretValue
)
)
// Is it possible that we don't have an olm session?
val devicesByUser = mapOf(device.userId to listOf(device))
val usersDeviceMap = try {
ensureOlmSessionsForDevicesAction.handle(devicesByUser)
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.w("Can't share secret ${request.secretName}: Failed to establish olm session")
return
}
val olmSessionResult = usersDeviceMap.getObject(device.userId, device.deviceId)
if (olmSessionResult?.sessionId == null) {
Timber.tag(loggerTag.value)
.w("secret share: no session with this device $deviceId, probably because there were no one-time keys")
return
}
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(device))
val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(device.userId, device.deviceId, encodedPayload)
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
try {
// raise the retries for secret
sendToDeviceTask.executeRetry(sendToDeviceParams, 6)
Timber.tag(loggerTag.value)
.i("successfully shared secret $secretName to ${device.shortDebugString()}")
// TODO add a trail for that in audit logs
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.e(failure, "failed to send shared secret $secretName to ${device.shortDebugString()}")
}
} else {
Timber.tag(loggerTag.value)
.d(" Received secret share request from un-authorised device ${device.deviceId}")
}
}
private suspend fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean {
val verifTimestamp = verifMutex.withLock {
recentlyVerifiedDevices[deviceId]
} ?: return false
val age = System.currentTimeMillis() - verifTimestamp
return age < SECRET_SHARE_WINDOW_DURATION
}
suspend fun requestSecretTo(deviceId: String, secretName: String) {
val cryptoDeviceInfo = cryptoStore.getUserDevice(credentials.userId, deviceId) ?: return Unit.also {
Timber.tag(loggerTag.value)
.d("Can't request secret for $secretName unknown device $deviceId")
}
val toDeviceContent = SecretShareRequest(
requestingDeviceId = credentials.deviceId,
secretName = secretName,
requestId = createUniqueTxnId()
)
verifMutex.withLock {
outgoingSecretRequests.add(toDeviceContent)
}
val contentMap = MXUsersDevicesMap<Any>()
contentMap.setObject(cryptoDeviceInfo.userId, cryptoDeviceInfo.deviceId, toDeviceContent)
val params = SendToDeviceTask.Params(
eventType = EventType.REQUEST_SECRET,
contentMap = contentMap
)
try {
withContext(coroutineDispatchers.io) {
sendToDeviceTask.executeRetry(params, 3)
}
Timber.tag(loggerTag.value)
.d("Secret request sent for $secretName to ${cryptoDeviceInfo.shortDebugString()}")
// TODO update the audit trail
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.w("Failed to request secret $secretName to ${cryptoDeviceInfo.shortDebugString()}")
}
}
suspend fun onSecretSendReceived(toDevice: Event, handleGossip: ((name: String, value: String) -> Boolean)) {
Timber.tag(loggerTag.value)
.i("onSecretSend() from ${toDevice.senderId} : onSecretSendReceived ${toDevice.content?.get("sender_key")}")
if (!toDevice.isEncrypted()) {
// secret send messages must be encrypted
Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event")
return
}
// Was that sent by us?
if (toDevice.senderId != credentials.userId) {
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}")
return
}
val secretContent = toDevice.getClearContent().toModel<SecretSendEventContent>() ?: return
val existingRequest = verifMutex.withLock {
outgoingSecretRequests.firstOrNull { it.requestId == secretContent.requestId }
}
// As per spec:
// Clients should ignore m.secret.send events received from devices that it did not send an m.secret.request event to.
if (existingRequest?.secretName == null) {
Timber.tag(loggerTag.value).i("onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
return
}
// we don't need to cancel the request as we only request to one device
// just forget about the request now
verifMutex.withLock {
outgoingSecretRequests.remove(existingRequest)
}
if (!handleGossip(existingRequest.secretName, secretContent.secretValue)) {
// TODO Ask to application layer?
Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK")
}
}
}
/*
* Copyright (c) 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.launch
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.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
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
import org.matrix.android.sdk.internal.session.SessionScope
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("SecretShareManager", LoggerTag.CRYPTO)
@SessionScope
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
) {
companion object {
private const val SECRET_SHARE_WINDOW_DURATION = 5 * 60 * 1000 // 5 minutes
}
/**
* Secret gossiping only occurs during a limited window period after interactive verification.
* We keep track of recent verification in memory for that purpose (no need to persist)
*/
private val recentlyVerifiedDevices = mutableMapOf<String, Long>()
private val verifMutex = Mutex()
/**
* Secrets are exchanged as part of interactive verification,
* so we can just store in memory.
*/
private val outgoingSecretRequests = mutableListOf<SecretShareRequest>()
// the listeners
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet()
fun addListener(listener: GossipingRequestListener) {
synchronized(gossipingRequestListeners) {
gossipingRequestListeners.add(listener)
}
}
fun removeListener(listener: GossipingRequestListener) {
synchronized(gossipingRequestListeners) {
gossipingRequestListeners.remove(listener)
}
}
/**
* Called when a session has been verified.
* This information can be used by the manager to decide whether or not to fullfill gossiping requests.
* This should be called as fast as possible after a successful self interactive verification
*/
fun onVerificationCompleteForDevice(deviceId: String) {
// For now we just keep an in memory cache
cryptoCoroutineScope.launch {
verifMutex.withLock {
recentlyVerifiedDevices[deviceId] = System.currentTimeMillis()
}
}
}
suspend fun handleSecretRequest(toDevice: Event) {
val request = toDevice.getClearContent().toModel<SecretShareRequest>()
?: return Unit.also {
Timber.tag(loggerTag.value)
.w("handleSecretRequest() : malformed request")
}
// val (action, requestingDeviceId, requestId, secretName) = it
val secretName = request.secretName ?: return Unit.also {
Timber.tag(loggerTag.value)
.v("handleSecretRequest() : Missing secret name")
}
val userId = toDevice.senderId ?: return Unit.also {
Timber.tag(loggerTag.value)
.v("handleSecretRequest() : Missing senderId")
}
if (userId != credentials.userId) {
// secrets are only shared between our own devices
Timber.tag(loggerTag.value)
.e("Ignoring secret share request from other users $userId")
return
}
val deviceId = request.requestingDeviceId
?: return Unit.also {
Timber.tag(loggerTag.value)
.w("handleSecretRequest() : malformed request norequestingDeviceId ")
}
val device = cryptoStore.getUserDevice(credentials.userId, deviceId)
?: return Unit.also {
Timber.tag(loggerTag.value)
.e("Received secret share request from unknown device $deviceId")
}
val isRequestingDeviceTrusted = device.isVerified
val isRecentInteractiveVerification = hasBeenVerifiedLessThanFiveMinutesFromNow(device.deviceId)
if (isRequestingDeviceTrusted && isRecentInteractiveVerification) {
// we can share the secret
val secretValue = when (secretName) {
MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master
SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned
USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user
KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey
?.let {
extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding()
}
else -> null
}
if (secretValue == null) {
Timber.tag(loggerTag.value)
.i("The secret is unknown $secretName, passing to app layer")
val toList = synchronized(gossipingRequestListeners) { gossipingRequestListeners.toList() }
toList.onEach { listener ->
listener.onSecretShareRequest(request)
}
return
}
val payloadJson = mapOf(
"type" to EventType.SEND_SECRET,
"content" to mapOf(
"request_id" to request.requestId,
"secret" to secretValue
)
)
// Is it possible that we don't have an olm session?
val devicesByUser = mapOf(device.userId to listOf(device))
val usersDeviceMap = try {
ensureOlmSessionsForDevicesAction.handle(devicesByUser)
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.w("Can't share secret ${request.secretName}: Failed to establish olm session")
return
}
val olmSessionResult = usersDeviceMap.getObject(device.userId, device.deviceId)
if (olmSessionResult?.sessionId == null) {
Timber.tag(loggerTag.value)
.w("secret share: no session with this device $deviceId, probably because there were no one-time keys")
return
}
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(device))
val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(device.userId, device.deviceId, encodedPayload)
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
try {
// raise the retries for secret
sendToDeviceTask.executeRetry(sendToDeviceParams, 6)
Timber.tag(loggerTag.value)
.i("successfully shared secret $secretName to ${device.shortDebugString()}")
// TODO add a trail for that in audit logs
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.e(failure, "failed to send shared secret $secretName to ${device.shortDebugString()}")
}
} else {
Timber.tag(loggerTag.value)
.d(" Received secret share request from un-authorised device ${device.deviceId}")
}
}
private suspend fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean {
val verifTimestamp = verifMutex.withLock {
recentlyVerifiedDevices[deviceId]
} ?: return false
val age = System.currentTimeMillis() - verifTimestamp
return age < SECRET_SHARE_WINDOW_DURATION
}
suspend fun requestSecretTo(deviceId: String, secretName: String) {
val cryptoDeviceInfo = cryptoStore.getUserDevice(credentials.userId, deviceId) ?: return Unit.also {
Timber.tag(loggerTag.value)
.d("Can't request secret for $secretName unknown device $deviceId")
}
val toDeviceContent = SecretShareRequest(
requestingDeviceId = credentials.deviceId,
secretName = secretName,
requestId = createUniqueTxnId()
)
verifMutex.withLock {
outgoingSecretRequests.add(toDeviceContent)
}
val contentMap = MXUsersDevicesMap<Any>()
contentMap.setObject(cryptoDeviceInfo.userId, cryptoDeviceInfo.deviceId, toDeviceContent)
val params = SendToDeviceTask.Params(
eventType = EventType.REQUEST_SECRET,
contentMap = contentMap
)
try {
withContext(coroutineDispatchers.io) {
sendToDeviceTask.executeRetry(params, 3)
}
Timber.tag(loggerTag.value)
.d("Secret request sent for $secretName to ${cryptoDeviceInfo.shortDebugString()}")
// TODO update the audit trail
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.w("Failed to request secret $secretName to ${cryptoDeviceInfo.shortDebugString()}")
}
}
suspend fun onSecretSendReceived(toDevice: Event, handleGossip: ((name: String, value: String) -> Boolean)) {
Timber.tag(loggerTag.value)
.i("onSecretSend() from ${toDevice.senderId} : onSecretSendReceived ${toDevice.content?.get("sender_key")}")
if (!toDevice.isEncrypted()) {
// secret send messages must be encrypted
Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event")
return
}
// Was that sent by us?
if (toDevice.senderId != credentials.userId) {
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}")
return
}
val secretContent = toDevice.getClearContent().toModel<SecretSendEventContent>() ?: return
val existingRequest = verifMutex.withLock {
outgoingSecretRequests.firstOrNull { it.requestId == secretContent.requestId }
}
// As per spec:
// Clients should ignore m.secret.send events received from devices that it did not send an m.secret.request event to.
if (existingRequest?.secretName == null) {
Timber.tag(loggerTag.value).i("onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
return
}
// we don't need to cancel the request as we only request to one device
// just forget about the request now
verifMutex.withLock {
outgoingSecretRequests.remove(existingRequest)
}
if (!handleGossip(existingRequest.secretName, secretContent.secretValue)) {
// TODO Ask to application layer?
Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK")
}
}
}

View file

@ -229,7 +229,7 @@ internal class MXMegolmDecryption(
}
Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId,
val addSessionResult = olmDevice.addInboundGroupSession(roomKeyContent.sessionId,
roomKeyContent.sessionKey,
roomKeyContent.roomId,
senderKey,
@ -237,9 +237,9 @@ internal class MXMegolmDecryption(
keysClaimed,
exportFormat)
when (added) {
is MXOlmDevice.AddSessionResult.Imported -> added.ratchetIndex
is MXOlmDevice.AddSessionResult.NotImportedHigherIndex -> added.newIndex
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) {
@ -273,7 +273,7 @@ internal class MXMegolmDecryption(
}
}
if (added is MXOlmDevice.AddSessionResult.Imported) {
if (addSessionResult is MXOlmDevice.AddSessionResult.Imported) {
Timber.tag(loggerTag.value)
.d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}")
defaultKeysBackupService.maybeBackupKeys()

View file

@ -892,7 +892,8 @@ internal class RealmCryptoStore @Inject constructor(
val sessionIdentifier = olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier()
val key = OlmInboundGroupSessionEntity.createPrimaryKey(
sessionIdentifier,
olmInboundGroupSessionWrapper.senderKey)
olmInboundGroupSessionWrapper.senderKey
)
val existing = realm.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
@ -1049,7 +1050,7 @@ internal class RealmCryptoStore @Inject constructor(
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId)
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId)
}.map {
it.toOutgoingGossipingRequest()
it.toOutgoingKeyRequest()
}.firstOrNull {
it.requestBody?.algorithm == requestBody.algorithm &&
it.requestBody?.roomId == requestBody.roomId &&
@ -1063,7 +1064,7 @@ internal class RealmCryptoStore @Inject constructor(
realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
}.map {
it.toOutgoingGossipingRequest()
it.toOutgoingKeyRequest()
}.firstOrNull()
}
@ -1074,7 +1075,7 @@ internal class RealmCryptoStore @Inject constructor(
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId)
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId)
}.map {
it.toOutgoingGossipingRequest()
it.toOutgoingKeyRequest()
}.filter {
it.requestBody?.algorithm == algorithm &&
it.requestBody?.senderKey == senderKey
@ -1088,30 +1089,35 @@ internal class RealmCryptoStore @Inject constructor(
val dataSourceFactory = realmDataSourceFactory.map {
AuditTrailMapper.map(it)
// mm we can't map not null...
?: AuditTrail(
System.currentTimeMillis(),
TrailType.Unknown,
IncomingKeyRequestInfo(
"",
"",
"",
"",
"",
"",
"",
)
)
?: createUnknownTrail()
}
return monarchy.findAllPagedWithChanges(realmDataSourceFactory,
LivePagedListBuilder(dataSourceFactory,
return monarchy.findAllPagedWithChanges(
realmDataSourceFactory,
LivePagedListBuilder(
dataSourceFactory,
PagedList.Config.Builder()
.setPageSize(20)
.setEnablePlaceholders(false)
.setPrefetchDistance(1)
.build())
.build()
)
)
}
private fun createUnknownTrail() = AuditTrail(
System.currentTimeMillis(),
TrailType.Unknown,
IncomingKeyRequestInfo(
"",
"",
"",
"",
"",
"",
"",
)
)
override fun <T> getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData<PagedList<T>> {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
realm.where<AuditTrailEntity>()
@ -1121,28 +1127,19 @@ internal class RealmCryptoStore @Inject constructor(
val dataSourceFactory = realmDataSourceFactory.map { entity ->
(AuditTrailMapper.map(entity)
// mm we can't map not null...
?: AuditTrail(
System.currentTimeMillis(),
type,
IncomingKeyRequestInfo(
"",
"",
"",
"",
"",
"",
"",
)
)
?: createUnknownTrail()
).let { mapper.invoke(it) }
}
return monarchy.findAllPagedWithChanges(realmDataSourceFactory,
LivePagedListBuilder(dataSourceFactory,
return monarchy.findAllPagedWithChanges(
realmDataSourceFactory,
LivePagedListBuilder(
dataSourceFactory,
PagedList.Config.Builder()
.setPageSize(20)
.setEnablePlaceholders(false)
.setPrefetchDistance(1)
.build())
.build()
)
)
}
@ -1166,7 +1163,7 @@ internal class RealmCryptoStore @Inject constructor(
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId)
.findAll()
.map {
it.toOutgoingGossipingRequest()
it.toOutgoingKeyRequest()
}.also {
if (it.size > 1) {
// there should be one or zero but not more, worth warning
@ -1188,7 +1185,7 @@ internal class RealmCryptoStore @Inject constructor(
this.requestState = OutgoingRoomKeyRequestState.UNSENT
this.setRequestBody(requestBody)
this.creationTimeStamp = System.currentTimeMillis()
}.toOutgoingGossipingRequest()
}.toOutgoingKeyRequest()
} else {
request = existing
}
@ -1231,7 +1228,7 @@ internal class RealmCryptoStore @Inject constructor(
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId)
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId)
.findAll().firstOrNull { entity ->
entity.toOutgoingGossipingRequest().let {
entity.toOutgoingKeyRequest().let {
it.requestBody?.senderKey == senderKey &&
it.requestBody?.algorithm == algorithm
}
@ -1473,7 +1470,7 @@ internal class RealmCryptoStore @Inject constructor(
realm
.where(OutgoingKeyRequestEntity::class.java)
}, { entity ->
entity.toOutgoingGossipingRequest()
entity.toOutgoingKeyRequest()
})
.filterNotNull()
}
@ -1484,7 +1481,7 @@ internal class RealmCryptoStore @Inject constructor(
.where(OutgoingKeyRequestEntity::class.java)
.`in`(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, inStates.map { it.name }.toTypedArray())
}, { entity ->
entity.toOutgoingGossipingRequest()
entity.toOutgoingKeyRequest()
})
.filterNotNull()
}
@ -1495,15 +1492,18 @@ internal class RealmCryptoStore @Inject constructor(
.where(OutgoingKeyRequestEntity::class.java)
}
val dataSourceFactory = realmDataSourceFactory.map {
it.toOutgoingGossipingRequest()
it.toOutgoingKeyRequest()
}
val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory,
LivePagedListBuilder(dataSourceFactory,
val trail = monarchy.findAllPagedWithChanges(
realmDataSourceFactory,
LivePagedListBuilder(
dataSourceFactory,
PagedList.Config.Builder()
.setPageSize(20)
.setEnablePlaceholders(false)
.setPrefetchDistance(1)
.build())
.build()
)
)
return trail
}

View file

@ -58,7 +58,7 @@ internal open class OutgoingKeyRequestEntity(
)
}
fun getRequestedKeyInfo(): RoomKeyRequestBody? = RoomKeyRequestBody.fromJson(requestedInfoStr)
private fun getRequestedKeyInfo(): RoomKeyRequestBody? = RoomKeyRequestBody.fromJson(requestedInfoStr)
fun setRequestBody(body: RoomKeyRequestBody) {
requestedInfoStr = body.toJson()
@ -92,7 +92,7 @@ internal open class OutgoingKeyRequestEntity(
replies.add(newReply)
}
fun toOutgoingGossipingRequest(): OutgoingKeyRequest {
fun toOutgoingKeyRequest(): OutgoingKeyRequest {
return OutgoingKeyRequest(
requestBody = getRequestedKeyInfo(),
recipients = getRecipients().orEmpty(),

View file

@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.room.membership.joining
import io.realm.RealmConfiguration
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult
@ -71,13 +70,11 @@ internal class DefaultJoinRoomTask @Inject constructor(
}
val joinRoomResponse = try {
executeRequest(globalErrorReceiver) {
withContext(coroutineDispatcher.io) {
roomAPI.join(
roomIdOrAlias = params.roomIdOrAlias,
viaServers = params.viaServers.take(3),
params = extraParams
)
}
roomAPI.join(
roomIdOrAlias = params.roomIdOrAlias,
viaServers = params.viaServers.take(3),
params = extraParams
)
}
} catch (failure: Throwable) {
roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.FailedJoining(failure))