WithHeld key support initial commit

This commit is contained in:
Valere 2020-05-13 17:30:06 +02:00 committed by Benoit Marty
parent a6f4cd74d5
commit dbe78f160b
33 changed files with 953 additions and 87 deletions

View File

@ -150,7 +150,7 @@ class CommonTestHelper(context: Context) {
timeline.dispose()
// Check that all events has been created
assertEquals(nbOfMessages.toLong(), sentEvents.size.toLong())
assertEquals("Message number do not match ${sentEvents}", nbOfMessages.toLong(), sentEvents.size.toLong())
return sentEvents
}

View File

@ -17,8 +17,14 @@
package im.vector.matrix.android.common
import android.os.SystemClock
import android.util.Log
import androidx.lifecycle.Observer
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toContent
@ -34,6 +40,8 @@ import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.internal.crypto.verification.SASDefaultVerificationTransaction
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@ -41,6 +49,8 @@ import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import java.util.UUID
import java.util.concurrent.CountDownLatch
class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
@ -274,4 +284,149 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
authData = createFakeMegolmBackupAuthData()
)
}
fun createDM(alice: Session, bob: Session): String {
val roomId = mTestHelper.doSync<String> {
alice.createRoom(
CreateRoomParams(invitedUserIds = listOf(bob.myUserId))
.setDirectMessage()
.enableEncryptionIfInvitedUsersSupportIt(),
it
)
}
mTestHelper.waitWithLatch { latch ->
val bobRoomSummariesLive = runBlocking(Dispatchers.Main) {
bob.getRoomSummariesLive(roomSummaryQueryParams { })
}
val newRoomObserver = object : Observer<List<RoomSummary>> {
override fun onChanged(t: List<RoomSummary>?) {
val indexOfFirst = t?.indexOfFirst { it.roomId == roomId } ?: -1
if (indexOfFirst != -1) {
latch.countDown()
bobRoomSummariesLive.removeObserver(this)
}
}
}
GlobalScope.launch(Dispatchers.Main) {
bobRoomSummariesLive.observeForever(newRoomObserver)
}
}
mTestHelper.waitWithLatch { latch ->
val bobRoomSummariesLive = runBlocking(Dispatchers.Main) {
bob.getRoomSummariesLive(roomSummaryQueryParams { })
}
val newRoomObserver = object : Observer<List<RoomSummary>> {
override fun onChanged(t: List<RoomSummary>?) {
if (bob.getRoom(roomId)
?.getRoomMember(bob.myUserId)
?.membership == Membership.JOIN) {
latch.countDown()
bobRoomSummariesLive.removeObserver(this)
}
}
}
GlobalScope.launch(Dispatchers.Main) {
bobRoomSummariesLive.observeForever(newRoomObserver)
}
mTestHelper.doSync<Unit> { bob.joinRoom(roomId, callback = it) }
}
return roomId
}
fun initializeCrossSigning(session: Session) {
mTestHelper.doSync<Unit> {
session.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = session.myUserId,
password = TestConstants.PASSWORD
), it)
}
}
fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) {
assertTrue(alice.cryptoService().crossSigningService().canCrossSign())
assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
val requestID = UUID.randomUUID().toString()
val aliceVerificationService = alice.cryptoService().verificationService()
val bobVerificationService = bob.cryptoService().verificationService()
aliceVerificationService.beginKeyVerificationInDMs(
VerificationMethod.SAS,
requestID,
roomId,
bob.myUserId,
bob.sessionParams.credentials.deviceId!!,
null)
// we should reach SHOW SAS on both
var alicePovTx: OutgoingSasVerificationTransaction? = null
var bobPovTx: IncomingSasVerificationTransaction? = null
// wait for alice to get the ready
mTestHelper.waitWithLatch {
mTestHelper.retryPeriodicallyWithLatch(it) {
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
if (bobPovTx?.state == VerificationTxState.OnStarted) {
bobPovTx?.performAccept()
true
} else {
false
}
}
}
mTestHelper.waitWithLatch {
mTestHelper.retryPeriodicallyWithLatch(it) {
alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID) as? OutgoingSasVerificationTransaction
Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}")
alicePovTx?.state == VerificationTxState.ShortCodeReady
}
}
// wait for alice to get the ready
mTestHelper.waitWithLatch {
mTestHelper.retryPeriodicallyWithLatch(it) {
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
if (bobPovTx?.state == VerificationTxState.OnStarted) {
bobPovTx?.performAccept()
}
bobPovTx?.state == VerificationTxState.ShortCodeReady
}
}
assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation())
bobPovTx!!.userHasVerifiedShortCode()
alicePovTx!!.userHasVerifiedShortCode()
mTestHelper.waitWithLatch {
mTestHelper.retryPeriodicallyWithLatch(it) {
alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
}
}
mTestHelper.waitWithLatch {
mTestHelper.retryPeriodicallyWithLatch(it) {
alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
}
}
}
}

View File

@ -285,5 +285,9 @@ class KeyShareTests : InstrumentedTest {
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
}
}
mTestHelper.signOutAndClose(aliceSession1)
mTestHelper.signOutAndClose(aliceSession2)
}
}

View File

@ -0,0 +1,126 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.gossiping
import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.common.CommonTestHelper
import im.vector.matrix.android.common.CryptoTestHelper
import im.vector.matrix.android.common.SessionTestParams
import im.vector.matrix.android.common.TestConstants
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
import org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class WithHeldTests : InstrumentedTest {
private val mTestHelper = CommonTestHelper(context())
private val mCryptoTestHelper = CryptoTestHelper(mTestHelper)
@Test
fun test_WithHeldUnverifiedReason() {
//=============================
// ARRANGE
//=============================
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val bobSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
// Initialize cross signing on both
mCryptoTestHelper.initializeCrossSigning(aliceSession)
mCryptoTestHelper.initializeCrossSigning(bobSession)
val roomId = mCryptoTestHelper.createDM(aliceSession, bobSession)
mCryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, roomId)
val roomAlicePOV = aliceSession.getRoom(roomId)!!
val bobUnverifiedSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
//=============================
// ACT
//=============================
// Alice decide to not send to unverified sessions
aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true)
val timelineEvent = mTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first()
// await for bob unverified session to get the message
mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) {
bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId) != null
}
}
val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId)!!
//=============================
// ASSERT
//=============================
// Bob should not be able to decrypt because the keys is withheld
try {
// .. might need to wait a bit for stability?
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
Assert.fail("This session should not be able to decrypt")
} catch (failure: Throwable) {
val type = (failure as MXCryptoError.Base).errorType
val technicalMessage = failure.technicalMessage
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
}
// enable back sending to unverified
aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false)
val secondEvent = mTestHelper.sendTextMessage(roomAlicePOV, "Verify your device!!", 1).first()
mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) {
val ev = bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(secondEvent.eventId)
// wait until it's decrypted
ev?.root?.getClearType() == EventType.MESSAGE
}
}
// Previous message should still be undecryptable (partially withheld session)
try {
// .. might need to wait a bit for stability?
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
Assert.fail("This session should not be able to decrypt")
} catch (failure: Throwable) {
val type = (failure as MXCryptoError.Base).errorType
val technicalMessage = failure.technicalMessage
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
}
mTestHelper.signOutAndClose(aliceSession)
mTestHelper.signOutAndClose(bobSession)
mTestHelper.signOutAndClose(bobUnverifiedSession)
}
}

View File

@ -20,6 +20,7 @@ package im.vector.matrix.android.api.session.crypto
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
import org.matrix.olm.OlmException
/**
@ -59,7 +60,8 @@ sealed class MXCryptoError : Throwable() {
MISSING_PROPERTY,
OLM,
UNKNOWN_DEVICES,
UNKNOWN_MESSAGE_INDEX
UNKNOWN_MESSAGE_INDEX,
KEYS_WITHHELD
}
companion object {

View File

@ -82,6 +82,9 @@ data class Event(
@Transient
var mCryptoError: MXCryptoError.ErrorType? = null
@Transient
var mCryptoErrorReason: String? = null
@Transient
var sendState: SendState = SendState.UNKNOWN
@ -182,6 +185,7 @@ data class Event(
if (redacts != other.redacts) return false
if (mxDecryptionResult != other.mxDecryptionResult) return false
if (mCryptoError != other.mCryptoError) return false
if (mCryptoErrorReason != other.mCryptoErrorReason) return false
if (sendState != other.sendState) return false
return true
@ -200,6 +204,7 @@ data class Event(
result = 31 * result + (redacts?.hashCode() ?: 0)
result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0)
result = 31 * result + (mCryptoError?.hashCode() ?: 0)
result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0)
result = 31 * result + sendState.hashCode()
return result
}

View File

@ -66,6 +66,7 @@ object EventType {
// Key share events
const val ROOM_KEY_REQUEST = "m.room_key_request"
const val FORWARDED_ROOM_KEY = "m.forwarded_room_key"
const val ROOM_KEY_WITHHELD = "org.matrix.room_key.withheld"
const val REQUEST_SECRET = "m.secret.request"
const val SEND_SECRET = "m.secret.send"

View File

@ -52,6 +52,7 @@ import im.vector.matrix.android.internal.crypto.actions.MegolmSessionDataImporte
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
import im.vector.matrix.android.internal.crypto.algorithms.IMXWithHeldExtension
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService
@ -65,6 +66,7 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.crypto.model.event.OlmEventContent
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
@ -807,6 +809,9 @@ internal class DefaultCryptoService @Inject constructor(
cryptoStore.saveGossipingEvent(event)
onSecretSendReceived(event)
}
EventType.ROOM_KEY_WITHHELD -> {
onKeyWithHeldReceived(event)
}
else -> {
// ignore
}
@ -834,6 +839,22 @@ internal class DefaultCryptoService @Inject constructor(
alg.onRoomKeyEvent(event, keysBackupService)
}
private fun onKeyWithHeldReceived(event: Event) {
val withHeldContent = event.getClearContent().toModel<RoomKeyWithHeldContent>() ?: return Unit.also {
Timber.e("## CRYPTO | Malformed onKeyWithHeldReceived() : missing fields")
}
Timber.d("## CRYPTO | onKeyWithHeldReceived() received : content <${withHeldContent}>")
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm)
if (alg is IMXWithHeldExtension) {
alg.onRoomKeyWithHeldEvent(withHeldContent)
} else {
Timber.e("## CRYPTO | onKeyWithHeldReceived() : Unable to handle WithHeldContent for ${withHeldContent.algorithm}")
return
}
}
private fun onSecretSendReceived(event: Event) {
Timber.i("## CRYPTO | GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}")
if (!event.isEncrypted()) {
@ -1197,7 +1218,7 @@ internal class DefaultCryptoService @Inject constructor(
// }
roomDecryptorProvider
.getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
?.requestKeysForEvent(event) ?: run {
?.requestKeysForEvent(event, false) ?: run {
Timber.v("## CRYPTO | requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
}
}

View File

@ -71,5 +71,5 @@ internal interface IMXDecrypting {
fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue : String) {}
fun requestKeysForEvent(event: Event)
fun requestKeysForEvent(event: Event, withHeld: Boolean)
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.algorithms
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
internal interface IMXWithHeldExtension {
fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent)
}

View File

@ -30,10 +30,12 @@ import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting
import im.vector.matrix.android.internal.crypto.algorithms.IMXWithHeldExtension
import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
import im.vector.matrix.android.internal.crypto.model.rest.ForwardedRoomKeyContent
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
@ -53,7 +55,7 @@ internal class MXMegolmDecryption(private val userId: String,
private val sendToDeviceTask: SendToDeviceTask,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope
) : IMXDecrypting {
) : IMXDecrypting, IMXWithHeldExtension {
var newSessionListener: NewSessionListener? = null
@ -61,7 +63,7 @@ internal class MXMegolmDecryption(private val userId: String,
* 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()
// private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()
@Throws(MXCryptoError::class)
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
@ -113,9 +115,21 @@ internal class MXMegolmDecryption(private val userId: String,
if (throwable is MXCryptoError.OlmError) {
// TODO Check the value of .message
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
addEventToPendingList(event, timeline)
//addEventToPendingList(event, timeline)
// The session might has been partially withheld (and only pass ratcheted)
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
if (withHeldInfo != null) {
if (requestKeysOnFail) {
requestKeysForEvent(event, true)
}
// Encapsulate as withHeld exception
throw MXCryptoError.Base(MXCryptoError.ErrorType.KEYS_WITHHELD,
withHeldInfo.code?.value ?: "",
withHeldInfo.reason)
}
if (requestKeysOnFail) {
requestKeysForEvent(event)
requestKeysForEvent(event, false)
}
}
@ -128,10 +142,25 @@ internal class MXMegolmDecryption(private val userId: String,
detailedReason)
}
if (throwable is MXCryptoError.Base) {
if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
addEventToPendingList(event, timeline)
if (requestKeysOnFail) {
requestKeysForEvent(event)
if (
/** if the session is unknown*/
throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
) {
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
if (withHeldInfo != null) {
if (requestKeysOnFail) {
requestKeysForEvent(event, true)
}
// Encapsulate as withHeld exception
throw MXCryptoError.Base(MXCryptoError.ErrorType.KEYS_WITHHELD,
withHeldInfo.code?.value ?: "",
withHeldInfo.reason)
} else {
// This is un-used in riotX SDK, not sure if needed
//addEventToPendingList(event, timeline)
if (requestKeysOnFail) {
requestKeysForEvent(event, false)
}
}
}
}
@ -147,12 +176,12 @@ internal class MXMegolmDecryption(private val userId: String,
*
* @param event the event
*/
override fun requestKeysForEvent(event: Event) {
override fun requestKeysForEvent(event: Event, withHeld: Boolean) {
val sender = event.senderId ?: return
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
val senderDevice = encryptedEventContent?.deviceId ?: return
val recipients = if (event.senderId == userId) {
val recipients = if (event.senderId == userId || withHeld) {
mapOf(
userId to listOf("*")
)
@ -176,25 +205,25 @@ internal class MXMegolmDecryption(private val userId: String,
outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients)
}
/**
* Add an event to the list of those we couldn't decrypt the first time we
* saw them.
*
* @param event the event to try to decrypt later
* @param timelineId the timeline identifier
*/
private fun addEventToPendingList(event: Event, timelineId: String) {
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return
val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}"
val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() }
val events = timeline.getOrPut(timelineId) { ArrayList() }
if (event !in events) {
Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}")
events.add(event)
}
}
// /**
// * Add an event to the list of those we couldn't decrypt the first time we
// * saw them.
// *
// * @param event the event to try to decrypt later
// * @param timelineId the timeline identifier
// */
// private fun addEventToPendingList(event: Event, timelineId: String) {
// val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return
// val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}"
//
// val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() }
// val events = timeline.getOrPut(timelineId) { ArrayList() }
//
// if (event !in events) {
// Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}")
// events.add(event)
// }
// }
/**
* Handle a key event.
@ -349,4 +378,10 @@ internal class MXMegolmDecryption(private val userId: String,
}
}
}
override fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoStore.addWithHeldMegolmSession(withHeldInfo)
}
}
}

View File

@ -18,6 +18,7 @@
package im.vector.matrix.android.internal.crypto.algorithms.megolm
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.Content
@ -31,9 +32,14 @@ import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
import im.vector.matrix.android.internal.crypto.model.forEach
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.convertToUTF8
import timber.log.Timber
@ -49,7 +55,8 @@ internal class MXMegolmEncryption(
private val credentials: Credentials,
private val sendToDeviceTask: SendToDeviceTask,
private val messageEncrypter: MessageEncrypter,
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
private val taskExecutor: TaskExecutor
) : IMXEncrypting {
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
@ -69,9 +76,26 @@ internal class MXMegolmEncryption(
val ts = System.currentTimeMillis()
Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom")
val devices = getDevicesInRoom(userIds)
Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.map}")
val outboundSession = ensureOutboundSession(devices)
Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}")
val outboundSession = ensureOutboundSession(devices.allowedDevices)
return encryptContent(outboundSession, eventType, eventContent)
.also {
notifyWithheldForSession(devices.withHeldDevices, outboundSession)
}
}
private fun notifyWithheldForSession(devices: MXUsersDevicesMap<WithHeldCode>, outboundSession: MXOutboundSessionInfo) {
ArrayList<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, code)
}
}
override fun discardSessionKey() {
@ -198,15 +222,16 @@ internal class MXMegolmEncryption(
if (sessionResult?.sessionId == null) {
// no session with this device, probably because there
// were no one-time keys.
//
// we could send them a to_device message anyway, as a
// signal that they have missed out on the key sharing
// message because of the lack of keys, but there's not
// much point in that really; it will mostly serve to clog
// up to_device inboxes.
//
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
// 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.
notifyKeyWithHeld(
listOf(UserDevice(userId, deviceID)),
session.sessionId,
WithHeldCode.NO_OLM
)
continue
}
Timber.v("## CRYPTO | shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
@ -214,29 +239,59 @@ internal class MXMegolmEncryption(
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 ((userId, devicesToShareWith) in devicesByUser) {
for ((deviceId) in devicesToShareWith) {
session.sharedWithDevices.setObject(userId, deviceId, chainIndex)
}
}
if (haveTargets) {
t0 = System.currentTimeMillis()
Timber.v("## CRYPTO | shareUserDevicesKey() : has target")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
sendToDeviceTask.execute(sendToDeviceParams)
Timber.v("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after "
+ (System.currentTimeMillis() - t0) + " ms")
// 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 ((userId, devicesToShareWith) in devicesByUser) {
for ((deviceId) in devicesToShareWith) {
session.sharedWithDevices.setObject(userId, deviceId, chainIndex)
}
try {
sendToDeviceTask.execute(sendToDeviceParams)
Timber.v("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms")
} catch (failure: Throwable) {
// What to do here...
Timber.e("## CRYPTO | shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ")
}
} else {
Timber.v("## CRYPTO | shareUserDevicesKey() : no need to sharekey")
}
}
private fun notifyKeyWithHeld(targets: List<UserDevice>, sessionId: String, code: WithHeldCode) {
val withHeldContent = RoomKeyWithHeldContent(
roomId = roomId,
senderKey = olmDevice.deviceCurve25519Key,
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
sessionId = sessionId,
codeString = code.value
)
val params = SendToDeviceTask.Params(
EventType.ROOM_KEY_WITHHELD,
MXUsersDevicesMap<Any>().apply {
targets.forEach {
setObject(it.userId, it.deviceId, withHeldContent)
}
}
)
sendToDeviceTask.configureWith(params) {
callback = object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
Timber.e("## CRYPTO | notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ")
}
}
}.executeBy(taskExecutor)
}
/**
* process the pending encryptions
*/
@ -271,7 +326,7 @@ internal class MXMegolmEncryption(
*
* @param userIds the user ids whose devices must be checked.
*/
private suspend fun getDevicesInRoom(userIds: List<String>): MXUsersDevicesMap<CryptoDeviceInfo> {
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
@ -280,9 +335,10 @@ internal class MXMegolmEncryption(
val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices()
|| cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId)
val devicesInRoom = MXUsersDevicesMap<CryptoDeviceInfo>()
val devicesInRoom = DeviceInRoomInfo()
val unknownDevices = MXUsersDevicesMap<CryptoDeviceInfo>()
for (userId in keys.userIds) {
val deviceIds = keys.getUserDeviceIds(userId) ?: continue
for (deviceId in deviceIds) {
@ -294,10 +350,12 @@ internal class MXMegolmEncryption(
}
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
}
@ -305,7 +363,7 @@ internal class MXMegolmEncryption(
// Don't bother sending to ourself
continue
}
devicesInRoom.setObject(userId, deviceId, deviceInfo)
devicesInRoom.allowedDevices.setObject(userId, deviceId, deviceInfo)
}
}
if (unknownDevices.isEmpty) {
@ -354,9 +412,24 @@ internal class MXMegolmEncryption(
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
Timber.v("## CRYPTO | CRYPTO | shareKeysWithDevice() : sending to $userId:$deviceId")
Timber.v("## CRYPTO | CRYPTO | reshareKey() : sending to $userId:$deviceId")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
sendToDeviceTask.execute(sendToDeviceParams)
return true
return try {
sendToDeviceTask.execute(sendToDeviceParams)
true
} catch (failure: Throwable) {
Timber.v("## CRYPTO | CRYPTO | reshareKey() : fail to send <$sessionId> 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

@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupServ
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.task.TaskExecutor
import javax.inject.Inject
internal class MXMegolmEncryptionFactory @Inject constructor(
@ -36,7 +37,8 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
private val credentials: Credentials,
private val sendToDeviceTask: SendToDeviceTask,
private val messageEncrypter: MessageEncrypter,
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository) {
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
private val taskExecutor: TaskExecutor) {
fun create(roomId: String): MXMegolmEncryption {
return MXMegolmEncryption(
@ -49,6 +51,8 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
credentials,
sendToDeviceTask,
messageEncrypter,
warnOnUnknownDevicesRepository)
warnOnUnknownDevicesRepository,
taskExecutor
)
}
}

View File

@ -212,7 +212,7 @@ internal class MXOlmDecryption(
return res["payload"]
}
override fun requestKeysForEvent(event: Event) {
override fun requestKeysForEvent(event: Event, withHeld: Boolean) {
// nop
}
}

View File

@ -119,3 +119,13 @@ class MXUsersDevicesMap<E> {
return "MXUsersDevicesMap $map"
}
}
inline fun <T> MXUsersDevicesMap<T>.forEach(action: (String, String, T) -> Unit) {
userIds.forEach { userId ->
getUserDeviceIds(userId)?.forEach { deviceId ->
getObject(userId, deviceId)?.let {
action(userId, deviceId, it)
}
}
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.model.event
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.extensions.tryThis
/**
* Class representing an sharekey content
*/
@JsonClass(generateAdapter = true)
data class RoomKeyWithHeldContent(
/**
* Required if code is not m.no_olm. The ID of the room that the session belongs to.
*/
@Json(name = "room_id") val roomId: String? = null,
/**
* Required. The encryption algorithm that the key is for.
*/
@Json(name = "algorithm") val algorithm: String? = null,
/**
* Required if code is not m.no_olm. The ID of the session.
*/
@Json(name = "session_id") val sessionId: String? = null,
/**
* Required. The key of the session creator.
*/
@Json(name = "sender_key") val senderKey: String? = null,
/**
* Required. A machine-readable code for why the key was not sent
*/
@Json(name = "code") val codeString: String? = null,
/**
* A human-readable reason for why the key was not sent. The receiving client should only use this string if it does not understand the code.
*/
@Json(name = "reason") val reason: String? = null
) {
val code: WithHeldCode?
get() {
return WithHeldCode.fromCode(codeString)
}
}
enum class WithHeldCode(val value: String) {
/**
* the user/device was blacklisted
*/
BLACKLISTED("m.blacklisted"),
/**
* the user/devices is unverified
*/
UNVERIFIED("m.unverified"),
/**
* the user/device is not allowed have the key. For example, this would usually be sent in response
* to a key request if the user was not in the room when the message was sent
*/
UNAUTHORISED("m.unauthorised"),
/**
* Sent in reply to a key request if the device that the key is requested from does not have the requested key
*/
UNAVAILABLE("m.unavailable"),
/**
* 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.
*/
NO_OLM("m.no_olm");
companion object {
fun fromCode(code: String?): WithHeldCode? {
return when (code) {
BLACKLISTED.value -> BLACKLISTED
UNVERIFIED.value -> UNVERIFIED
UNAUTHORISED.value -> UNAUTHORISED
UNAVAILABLE.value -> UNAVAILABLE
NO_OLM.value -> NO_OLM
else -> null
}
}
}
}

View File

@ -1,3 +1,4 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
@ -32,6 +33,7 @@ import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
@ -416,6 +418,9 @@ internal interface IMXCryptoStore {
fun updateUsersTrust(check: (String) -> Boolean)
fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent)
fun getWithHeldMegolmSession(roomId: String, sessionId: String) : RoomKeyWithHeldContent?
// Dev tools
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>

View File

@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.crypto.GossipingRequestState
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
import im.vector.matrix.android.internal.crypto.IncomingShareRequestCommon
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.NewSessionListener
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
@ -40,6 +41,8 @@ import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.model.toEntity
@ -69,6 +72,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossiping
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntity
import im.vector.matrix.android.internal.crypto.store.db.model.createPrimaryKey
import im.vector.matrix.android.internal.crypto.store.db.query.delete
import im.vector.matrix.android.internal.crypto.store.db.query.get
@ -1427,4 +1431,32 @@ internal class RealmCryptoStore @Inject constructor(
return existing
}
}
override fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent) {
val roomId = withHeldContent.roomId ?: return
val sessionId = withHeldContent.sessionId ?: return
if (withHeldContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return
doRealmTransaction(realmConfiguration) { realm ->
WithHeldSessionEntity.getOrCreate(realm, roomId, sessionId)?.let {
it.code = withHeldContent.code
it.senderKey = withHeldContent.senderKey
it.reason = withHeldContent.reason
}
}
}
override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? {
return doWithRealm(realmConfiguration) { realm ->
WithHeldSessionEntity.get(realm, roomId, sessionId)?.let {
RoomKeyWithHeldContent(
roomId = roomId,
sessionId = sessionId,
algorithm = it.algorithm,
codeString = it.codeString,
reason = it.reason,
senderKey = it.senderKey
)
}
}
}
}

View File

@ -38,6 +38,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntityF
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntityFields
import im.vector.matrix.android.internal.di.SerializeNulls
import io.realm.DynamicRealm
import io.realm.RealmMigration
@ -52,7 +53,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
// 0, 1, 2: legacy Riot-Android
// 3: migrate to RiotX schema
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
const val CRYPTO_STORE_SCHEMA_VERSION = 9L
const val CRYPTO_STORE_SCHEMA_VERSION = 10L
}
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
@ -67,6 +68,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
if (oldVersion <= 6) migrateTo7(realm)
if (oldVersion <= 7) migrateTo8(realm)
if (oldVersion <= 8) migrateTo9(realm)
if (oldVersion <= 9) migrateTo10(realm)
}
private fun migrateTo1Legacy(realm: DynamicRealm) {
@ -416,4 +418,18 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
}
}
}
// Version 10L added WithHeld Keys Info (MSC2399)
private fun migrateTo10(realm: DynamicRealm) {
Timber.d("Step 9 -> 10")
realm.schema.create("WithHeldSessionEntity")
.addField(WithHeldSessionEntityFields.ROOM_ID, String::class.java)
.addField(WithHeldSessionEntityFields.ALGORITHM, String::class.java)
.addField(WithHeldSessionEntityFields.SESSION_ID, String::class.java)
.addIndex(WithHeldSessionEntityFields.SESSION_ID)
.addField(WithHeldSessionEntityFields.SENDER_KEY, String::class.java)
.addIndex(WithHeldSessionEntityFields.SENDER_KEY)
.addField(WithHeldSessionEntityFields.CODE_STRING, String::class.java)
.addField(WithHeldSessionEntityFields.REASON, String::class.java)
}
}

View File

@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntity
import io.realm.annotations.RealmModule
/**
@ -50,6 +51,7 @@ import io.realm.annotations.RealmModule
GossipingEventEntity::class,
IncomingGossipingRequestEntity::class,
OutgoingGossipingRequestEntity::class,
MyDeviceLastSeenInfoEntity::class
MyDeviceLastSeenInfoEntity::class,
WithHeldSessionEntity::class
])
internal class RealmCryptoStoreModule

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.store.db.model
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
import io.realm.RealmObject
import io.realm.annotations.Index
/**
* When an encrypted message is sent in a room, the megolm key might not be sent to all devices present in the room.
* Sometimes this may be inadvertent (for example, if the sending device is not aware of some devices that have joined),
* but some times, this may be purposeful.
* For example, the sender may have blacklisted certain devices or users,
* or may be choosing to not send the megolm key to devices that they have not verified yet.
*/
internal open class WithHeldSessionEntity(
var roomId: String? = null,
var algorithm: String? = null,
@Index var sessionId: String? = null,
@Index var senderKey: String? = null,
var codeString: String? = null,
var reason: String? = null
) : RealmObject() {
var code: WithHeldCode?
get() {
return WithHeldCode.fromCode(codeString)
}
set(code) {
codeString = code?.value
}
companion object
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.store.db.query
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntity
import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntityFields
import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where
internal fun WithHeldSessionEntity.Companion.get(realm: Realm, roomId: String, sessionId: String): WithHeldSessionEntity? {
return realm.where<WithHeldSessionEntity>()
.equalTo(WithHeldSessionEntityFields.ROOM_ID, roomId)
.equalTo(WithHeldSessionEntityFields.SESSION_ID, sessionId)
.equalTo(WithHeldSessionEntityFields.ALGORITHM, MXCRYPTO_ALGORITHM_MEGOLM)
.findFirst()
}
internal fun WithHeldSessionEntity.Companion.getOrCreate(realm: Realm, roomId: String, sessionId: String): WithHeldSessionEntity? {
return get(realm, roomId, sessionId)
?: realm.createObject<WithHeldSessionEntity>().apply {
this.roomId = roomId
this.algorithm = MXCRYPTO_ALGORITHM_MEGOLM
this.sessionId = sessionId
}
}

View File

@ -45,6 +45,12 @@ internal object EventMapper {
eventEntity.redacts = event.redacts
eventEntity.age = event.unsignedData?.age ?: event.originServerTs
eventEntity.unsignedData = uds
eventEntity.decryptionResultJson = event.mxDecryptionResult?.let {
MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(it)
}
eventEntity.decryptionErrorReason = event.mCryptoErrorReason
eventEntity.decryptionErrorCode = event.mCryptoError?.name
return eventEntity
}
@ -85,6 +91,7 @@ internal object EventMapper {
it.mCryptoError = eventEntity.decryptionErrorCode?.let { errorCode ->
MXCryptoError.ErrorType.valueOf(errorCode)
}
it.mCryptoErrorReason = eventEntity.decryptionErrorReason
}
}
}

View File

@ -37,6 +37,7 @@ internal open class EventEntity(@Index var eventId: String = "",
var redacts: String? = null,
var decryptionResultJson: String? = null,
var decryptionErrorCode: String? = null,
var decryptionErrorReason: String? = null,
var ageLocalTs: Long? = null
) : RealmObject() {
@ -62,5 +63,6 @@ internal open class EventEntity(@Index var eventId: String = "",
val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java)
decryptionResultJson = adapter.toJson(decryptionResult)
decryptionErrorCode = null
decryptionErrorReason = null
}
}

View File

@ -115,9 +115,10 @@ internal class TimelineEventDecryptor @Inject constructor(
eventEntity.setDecryptionResult(result)
} catch (e: MXCryptoError) {
Timber.v(e, "Failed to decrypt event $eventId")
if (e is MXCryptoError.Base && e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
if (e is MXCryptoError.Base /*&& e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID*/) {
// Keep track of unknown sessions to automatically try to decrypt on new session
eventEntity.decryptionErrorCode = e.errorType.name
eventEntity.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
event.content?.toModel<EncryptedEventContent>()?.let { content ->
content.sessionId?.let { sessionId ->
synchronized(unknownSessionsFailure) {

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.sync
import im.vector.matrix.android.R
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
@ -51,6 +52,7 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMemberEvent
import im.vector.matrix.android.internal.session.room.read.FullyReadContent
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor
import im.vector.matrix.android.internal.session.room.typing.TypingEventContent
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
import im.vector.matrix.android.internal.session.sync.model.RoomSync
@ -71,7 +73,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private val roomMemberEventHandler: RoomMemberEventHandler,
private val roomTypingUsersHandler: RoomTypingUsersHandler,
@UserId private val userId: String,
private val eventBus: EventBus) {
private val eventBus: EventBus,
private val timelineEventDecryptor: TimelineEventDecryptor) {
sealed class HandlingStrategy {
data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
@ -162,7 +165,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
roomSync.timeline.events,
roomSync.timeline.prevToken,
roomSync.timeline.limited,
syncLocalTimestampMillis
syncLocalTimestampMillis,
!isInitialSync
)
roomEntity.addOrUpdate(chunkEntity)
}
@ -259,8 +263,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
eventList: List<Event>,
prevToken: String? = null,
isLimited: Boolean = true,
syncLocalTimestampMillis: Long): ChunkEntity {
val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
syncLocalTimestampMillis: Long,
decryptOnTheFly: Boolean): ChunkEntity {
val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId)
val chunkEntity = if (!isLimited && lastChunk != null) {
lastChunk
} else {
@ -278,6 +283,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
continue
}
eventIds.add(event.eventId)
if (event.isEncrypted() && decryptOnTheFly) {
decryptIfNeeded(event, roomId)
}
val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm)
if (event.stateKey != null) {
@ -295,9 +305,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
ContentMapper.map(rootStateEvent?.content).toModel()
}
chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
// Give info to crypto module
cryptoService.onLiveEvent(roomEntity.roomId, event)
// Try to remove local echo
event.unsignedData?.transactionId?.also {
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
@ -324,6 +336,23 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
return chunkEntity
}
private fun decryptIfNeeded(event: Event, roomId: String) {
try {
val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
if (e is MXCryptoError.Base) {
event.mCryptoError = e.errorType
event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
}
}
}
data class EphemeralResult(
val typingUserIds: List<String> = emptyList()
)

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.resources
import android.content.Context
import android.graphics.drawable.Drawable
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import im.vector.riotx.features.themes.ThemeUtils
import javax.inject.Inject
class DrawableProvider @Inject constructor(private val context: Context) {
fun getDrawable(@DrawableRes colorRes: Int): Drawable? {
return ContextCompat.getDrawable(context, colorRes)
}
fun getDrawable(@DrawableRes colorRes: Int, @ColorInt color: Int): Drawable? {
return ContextCompat.getDrawable(context, colorRes)?.let {
ThemeUtils.tintDrawableWithColor(it, color)
}
}
}

View File

@ -19,9 +19,11 @@ package im.vector.riotx.features.home.room.detail.timeline.factory
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.DrawableProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
@ -29,6 +31,7 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformat
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_
import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod
import me.gujun.android.span.image
import me.gujun.android.span.span
import javax.inject.Inject
@ -37,6 +40,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
private val colorProvider: ColorProvider,
private val stringProvider: StringProvider,
private val avatarSizeProvider: AvatarSizeProvider,
private val drawableProvider: DrawableProvider,
private val attributesFactory: MessageItemAttributesFactory) {
fun create(event: TimelineEvent,
@ -48,20 +52,62 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
return when {
EventType.ENCRYPTED == event.root.getClearType() -> {
val cryptoError = event.root.mCryptoError
val errorDescription =
if (cryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id)
} else {
// TODO i18n
cryptoError?.name
// val errorDescription =
// if (cryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
// stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id)
// } else {
// // TODO i18n
// cryptoError?.name
// }
val colorFromAttribute = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
val spannableStr = if(cryptoError == null) {
span(stringProvider.getString(R.string.encrypted_message)) {
textStyle = "italic"
textColor = colorFromAttribute
}
} else {
when(cryptoError) {
MXCryptoError.ErrorType.KEYS_WITHHELD -> {
// val why = when (event.root.mCryptoErrorReason) {
// WithHeldCode.BLACKLISTED.value -> stringProvider.getString(R.string.crypto_error_withheld_blacklisted)
// WithHeldCode.UNVERIFIED.value -> stringProvider.getString(R.string.crypto_error_withheld_unverified)
// else -> stringProvider.getString(R.string.crypto_error_withheld_generic)
// }
//stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, why)
span {
apply {
drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let {
image(it, "baseline")
}
}
span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_final)) {
textStyle = "italic"
textColor = colorFromAttribute
}
}
}
else -> {
span {
apply {
drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let {
image(it, "baseline")
}
}
span(stringProvider.getString(R.string.notice_crypto_unable_to_decrypt_friendly)) {
textStyle = "italic"
textColor = colorFromAttribute
}
}
}
}
val message = stringProvider.getString(R.string.encrypted_message).takeIf { cryptoError == null }
?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription)
val spannableStr = span(message) {
textStyle = "italic"
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
}
// val spannableStr = span(message) {
// textStyle = "italic"
// textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
// }
// TODO This is not correct format for error, change it

View File

@ -385,7 +385,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
// }
// }
sendToUnverifiedDevicesPref.isChecked = false
sendToUnverifiedDevicesPref.isChecked = session.cryptoService().getGlobalBlacklistUnverifiedDevices()

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="10dp"
android:height="10dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,22C17.5228,22 22,17.5228 22,12C22,6.4771 17.5228,2 12,2C6.4771,2 2,6.4771 2,12C2,17.5228 6.4771,22 12,22Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
<path
android:pathData="M12,6V12L15,15"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,11 @@
<vector android:height="10dp" android:viewportHeight="22"
android:viewportWidth="22" android:width="10dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M11,21C16.5228,21 21,16.5228 21,11C21,5.4771 16.5228,1 11,1C5.4771,1 1,5.4771 1,11C1,16.5228 5.4771,21 11,21Z"
android:strokeColor="#2E2F32" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M17.4692,4.3983L4.8165,17.6833"
android:strokeColor="#2E2F32" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

View File

@ -2501,4 +2501,9 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="room_settings_topic_hint">Topic</string>
<string name="room_settings_save_success">You changed room settings successfully</string>
<string name="notice_crypto_unable_to_decrypt_final">You cannot access this message</string>
<string name="notice_crypto_unable_to_decrypt_friendly">Waiting for this message, this may take a while</string>
<string name="crypto_error_withheld_blacklisted">You have been blocked</string>
<string name="crypto_error_withheld_unverified">Session not trusted by sender</string>
<string name="crypto_error_withheld_generic">Sender purposely did not send the keys</string>
</resources>

View File

@ -29,7 +29,7 @@
<!-- android:title="@string/encryption_information_device_key" />-->
<im.vector.riotx.core.preference.VectorSwitchPreference
android:enabled="false"
android:enabled="true"
android:key="SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY"
android:summary="@string/encryption_never_send_to_unverified_devices_summary"
android:title="@string/encryption_never_send_to_unverified_devices_title" />