WithHeld key support initial commit
This commit is contained in:
parent
a6f4cd74d5
commit
dbe78f160b
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -285,5 +285,9 @@ class KeyShareTests : InstrumentedTest {
|
|||
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mTestHelper.signOutAndClose(aliceSession1)
|
||||
mTestHelper.signOutAndClose(aliceSession2)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,5 +71,5 @@ internal interface IMXDecrypting {
|
|||
|
||||
fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue : String) {}
|
||||
|
||||
fun requestKeysForEvent(event: Event)
|
||||
fun requestKeysForEvent(event: Event, withHeld: Boolean)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -212,7 +212,7 @@ internal class MXOlmDecryption(
|
|||
return res["payload"]
|
||||
}
|
||||
|
||||
override fun requestKeysForEvent(event: Event) {
|
||||
override fun requestKeysForEvent(event: Event, withHeld: Boolean) {
|
||||
// nop
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -385,7 +385,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
|||
// }
|
||||
// }
|
||||
|
||||
sendToUnverifiedDevicesPref.isChecked = false
|
||||
|
||||
sendToUnverifiedDevicesPref.isChecked = session.cryptoService().getGlobalBlacklistUnverifiedDevices()
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
Loading…
Reference in New Issue