WithHeld key support initial commit
This commit is contained in:
parent
a6f4cd74d5
commit
dbe78f160b
|
@ -150,7 +150,7 @@ class CommonTestHelper(context: Context) {
|
||||||
timeline.dispose()
|
timeline.dispose()
|
||||||
|
|
||||||
// Check that all events has been created
|
// 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
|
return sentEvents
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,14 @@
|
||||||
package im.vector.matrix.android.common
|
package im.vector.matrix.android.common
|
||||||
|
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import im.vector.matrix.android.api.session.Session
|
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.Event
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.toContent
|
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.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
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.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.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -41,6 +49,8 @@ import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import java.util.UUID
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||||
|
@ -274,4 +284,149 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||||
authData = createFakeMegolmBackupAuthData()
|
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
|
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.CryptoDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
|
||||||
import org.matrix.olm.OlmException
|
import org.matrix.olm.OlmException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,7 +60,8 @@ sealed class MXCryptoError : Throwable() {
|
||||||
MISSING_PROPERTY,
|
MISSING_PROPERTY,
|
||||||
OLM,
|
OLM,
|
||||||
UNKNOWN_DEVICES,
|
UNKNOWN_DEVICES,
|
||||||
UNKNOWN_MESSAGE_INDEX
|
UNKNOWN_MESSAGE_INDEX,
|
||||||
|
KEYS_WITHHELD
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -82,6 +82,9 @@ data class Event(
|
||||||
@Transient
|
@Transient
|
||||||
var mCryptoError: MXCryptoError.ErrorType? = null
|
var mCryptoError: MXCryptoError.ErrorType? = null
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
var mCryptoErrorReason: String? = null
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
var sendState: SendState = SendState.UNKNOWN
|
var sendState: SendState = SendState.UNKNOWN
|
||||||
|
|
||||||
|
@ -182,6 +185,7 @@ data class Event(
|
||||||
if (redacts != other.redacts) return false
|
if (redacts != other.redacts) return false
|
||||||
if (mxDecryptionResult != other.mxDecryptionResult) return false
|
if (mxDecryptionResult != other.mxDecryptionResult) return false
|
||||||
if (mCryptoError != other.mCryptoError) return false
|
if (mCryptoError != other.mCryptoError) return false
|
||||||
|
if (mCryptoErrorReason != other.mCryptoErrorReason) return false
|
||||||
if (sendState != other.sendState) return false
|
if (sendState != other.sendState) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
@ -200,6 +204,7 @@ data class Event(
|
||||||
result = 31 * result + (redacts?.hashCode() ?: 0)
|
result = 31 * result + (redacts?.hashCode() ?: 0)
|
||||||
result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0)
|
result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0)
|
||||||
result = 31 * result + (mCryptoError?.hashCode() ?: 0)
|
result = 31 * result + (mCryptoError?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0)
|
||||||
result = 31 * result + sendState.hashCode()
|
result = 31 * result + sendState.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,7 @@ object EventType {
|
||||||
// Key share events
|
// Key share events
|
||||||
const val ROOM_KEY_REQUEST = "m.room_key_request"
|
const val ROOM_KEY_REQUEST = "m.room_key_request"
|
||||||
const val FORWARDED_ROOM_KEY = "m.forwarded_room_key"
|
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 REQUEST_SECRET = "m.secret.request"
|
||||||
const val SEND_SECRET = "m.secret.send"
|
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.MessageEncrypter
|
||||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
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.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.megolm.MXMegolmEncryptionFactory
|
||||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService
|
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.EncryptedEventContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.OlmEventContent
|
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.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.event.SecretSendEventContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||||
|
@ -807,6 +809,9 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
cryptoStore.saveGossipingEvent(event)
|
cryptoStore.saveGossipingEvent(event)
|
||||||
onSecretSendReceived(event)
|
onSecretSendReceived(event)
|
||||||
}
|
}
|
||||||
|
EventType.ROOM_KEY_WITHHELD -> {
|
||||||
|
onKeyWithHeldReceived(event)
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
@ -834,6 +839,22 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
alg.onRoomKeyEvent(event, keysBackupService)
|
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) {
|
private fun onSecretSendReceived(event: Event) {
|
||||||
Timber.i("## CRYPTO | GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}")
|
Timber.i("## CRYPTO | GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}")
|
||||||
if (!event.isEncrypted()) {
|
if (!event.isEncrypted()) {
|
||||||
|
@ -1197,7 +1218,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
// }
|
// }
|
||||||
roomDecryptorProvider
|
roomDecryptorProvider
|
||||||
.getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
|
.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}")
|
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 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.EnsureOlmSessionsForDevicesAction
|
||||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
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.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.keysbackup.DefaultKeysBackupService
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
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.EncryptedEventContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
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.ForwardedRoomKeyContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
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 sendToDeviceTask: SendToDeviceTask,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val cryptoCoroutineScope: CoroutineScope
|
private val cryptoCoroutineScope: CoroutineScope
|
||||||
) : IMXDecrypting {
|
) : IMXDecrypting, IMXWithHeldExtension {
|
||||||
|
|
||||||
var newSessionListener: NewSessionListener? = null
|
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
|
* Events which we couldn't decrypt due to unknown sessions / indexes: map from
|
||||||
* senderKey|sessionId to timelines to list of MatrixEvents.
|
* 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)
|
@Throws(MXCryptoError::class)
|
||||||
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||||
|
@ -113,9 +115,21 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
if (throwable is MXCryptoError.OlmError) {
|
if (throwable is MXCryptoError.OlmError) {
|
||||||
// TODO Check the value of .message
|
// TODO Check the value of .message
|
||||||
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
|
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) {
|
if (requestKeysOnFail) {
|
||||||
requestKeysForEvent(event)
|
requestKeysForEvent(event, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,10 +142,25 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
detailedReason)
|
detailedReason)
|
||||||
}
|
}
|
||||||
if (throwable is MXCryptoError.Base) {
|
if (throwable is MXCryptoError.Base) {
|
||||||
if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
if (
|
||||||
addEventToPendingList(event, timeline)
|
/** if the session is unknown*/
|
||||||
if (requestKeysOnFail) {
|
throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
|
||||||
requestKeysForEvent(event)
|
) {
|
||||||
|
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
|
* @param event the event
|
||||||
*/
|
*/
|
||||||
override fun requestKeysForEvent(event: Event) {
|
override fun requestKeysForEvent(event: Event, withHeld: Boolean) {
|
||||||
val sender = event.senderId ?: return
|
val sender = event.senderId ?: return
|
||||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
||||||
val senderDevice = encryptedEventContent?.deviceId ?: return
|
val senderDevice = encryptedEventContent?.deviceId ?: return
|
||||||
|
|
||||||
val recipients = if (event.senderId == userId) {
|
val recipients = if (event.senderId == userId || withHeld) {
|
||||||
mapOf(
|
mapOf(
|
||||||
userId to listOf("*")
|
userId to listOf("*")
|
||||||
)
|
)
|
||||||
|
@ -176,25 +205,25 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients)
|
outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Add an event to the list of those we couldn't decrypt the first time we
|
// * Add an event to the list of those we couldn't decrypt the first time we
|
||||||
* saw them.
|
// * saw them.
|
||||||
*
|
// *
|
||||||
* @param event the event to try to decrypt later
|
// * @param event the event to try to decrypt later
|
||||||
* @param timelineId the timeline identifier
|
// * @param timelineId the timeline identifier
|
||||||
*/
|
// */
|
||||||
private fun addEventToPendingList(event: Event, timelineId: String) {
|
// private fun addEventToPendingList(event: Event, timelineId: String) {
|
||||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return
|
// val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return
|
||||||
val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}"
|
// val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}"
|
||||||
|
//
|
||||||
val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() }
|
// val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() }
|
||||||
val events = timeline.getOrPut(timelineId) { ArrayList() }
|
// val events = timeline.getOrPut(timelineId) { ArrayList() }
|
||||||
|
//
|
||||||
if (event !in events) {
|
// if (event !in events) {
|
||||||
Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}")
|
// Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}")
|
||||||
events.add(event)
|
// events.add(event)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a key 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
|
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.auth.data.Credentials
|
||||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
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.keysbackup.DefaultKeysBackupService
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
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.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.repository.WarnOnUnknownDeviceRepository
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
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.JsonCanonicalizer
|
||||||
import im.vector.matrix.android.internal.util.convertToUTF8
|
import im.vector.matrix.android.internal.util.convertToUTF8
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -49,7 +55,8 @@ internal class MXMegolmEncryption(
|
||||||
private val credentials: Credentials,
|
private val credentials: Credentials,
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
private val sendToDeviceTask: SendToDeviceTask,
|
||||||
private val messageEncrypter: MessageEncrypter,
|
private val messageEncrypter: MessageEncrypter,
|
||||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository
|
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
||||||
|
private val taskExecutor: TaskExecutor
|
||||||
) : IMXEncrypting {
|
) : IMXEncrypting {
|
||||||
|
|
||||||
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
|
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
|
||||||
|
@ -69,9 +76,26 @@ internal class MXMegolmEncryption(
|
||||||
val ts = System.currentTimeMillis()
|
val ts = System.currentTimeMillis()
|
||||||
Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom")
|
Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom")
|
||||||
val devices = getDevicesInRoom(userIds)
|
val devices = getDevicesInRoom(userIds)
|
||||||
Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.map}")
|
Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}")
|
||||||
val outboundSession = ensureOutboundSession(devices)
|
val outboundSession = ensureOutboundSession(devices.allowedDevices)
|
||||||
|
|
||||||
return encryptContent(outboundSession, eventType, eventContent)
|
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() {
|
override fun discardSessionKey() {
|
||||||
|
@ -198,15 +222,16 @@ internal class MXMegolmEncryption(
|
||||||
if (sessionResult?.sessionId == null) {
|
if (sessionResult?.sessionId == null) {
|
||||||
// no session with this device, probably because there
|
// no session with this device, probably because there
|
||||||
// were no one-time keys.
|
// were no one-time keys.
|
||||||
//
|
|
||||||
// we could send them a to_device message anyway, as a
|
// MSC 2399
|
||||||
// signal that they have missed out on the key sharing
|
// send withheld m.no_olm: an olm session could not be established.
|
||||||
// message because of the lack of keys, but there's not
|
// This may happen, for example, if the sender was unable to obtain a one-time key from the recipient.
|
||||||
// much point in that really; it will mostly serve to clog
|
notifyKeyWithHeld(
|
||||||
// up to_device inboxes.
|
listOf(UserDevice(userId, deviceID)),
|
||||||
//
|
session.sessionId,
|
||||||
// ensureOlmSessionsForUsers has already done the logging,
|
WithHeldCode.NO_OLM
|
||||||
// so just skip it.
|
)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
Timber.v("## CRYPTO | shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
|
Timber.v("## CRYPTO | shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
|
||||||
|
@ -214,29 +239,59 @@ internal class MXMegolmEncryption(
|
||||||
haveTargets = true
|
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) {
|
if (haveTargets) {
|
||||||
t0 = System.currentTimeMillis()
|
t0 = System.currentTimeMillis()
|
||||||
Timber.v("## CRYPTO | shareUserDevicesKey() : has target")
|
Timber.v("## CRYPTO | shareUserDevicesKey() : has target")
|
||||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
|
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
|
||||||
sendToDeviceTask.execute(sendToDeviceParams)
|
try {
|
||||||
Timber.v("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after "
|
sendToDeviceTask.execute(sendToDeviceParams)
|
||||||
+ (System.currentTimeMillis() - t0) + " ms")
|
Timber.v("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms")
|
||||||
|
} catch (failure: Throwable) {
|
||||||
// Add the devices we have shared with to session.sharedWithDevices.
|
// What to do here...
|
||||||
// we deliberately iterate over devicesByUser (ie, the devices we
|
Timber.e("## CRYPTO | shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ")
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.v("## CRYPTO | shareUserDevicesKey() : no need to sharekey")
|
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
|
* process the pending encryptions
|
||||||
*/
|
*/
|
||||||
|
@ -271,7 +326,7 @@ internal class MXMegolmEncryption(
|
||||||
*
|
*
|
||||||
* @param userIds the user ids whose devices must be checked.
|
* @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
|
// 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
|
// 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
|
// with them, which means that they will have announced any new devices via
|
||||||
|
@ -280,9 +335,10 @@ internal class MXMegolmEncryption(
|
||||||
val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices()
|
val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices()
|
||||||
|| cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId)
|
|| cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId)
|
||||||
|
|
||||||
val devicesInRoom = MXUsersDevicesMap<CryptoDeviceInfo>()
|
val devicesInRoom = DeviceInRoomInfo()
|
||||||
val unknownDevices = MXUsersDevicesMap<CryptoDeviceInfo>()
|
val unknownDevices = MXUsersDevicesMap<CryptoDeviceInfo>()
|
||||||
|
|
||||||
|
|
||||||
for (userId in keys.userIds) {
|
for (userId in keys.userIds) {
|
||||||
val deviceIds = keys.getUserDeviceIds(userId) ?: continue
|
val deviceIds = keys.getUserDeviceIds(userId) ?: continue
|
||||||
for (deviceId in deviceIds) {
|
for (deviceId in deviceIds) {
|
||||||
|
@ -294,10 +350,12 @@ internal class MXMegolmEncryption(
|
||||||
}
|
}
|
||||||
if (deviceInfo.isBlocked) {
|
if (deviceInfo.isBlocked) {
|
||||||
// Remove any blocked devices
|
// Remove any blocked devices
|
||||||
|
devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.BLACKLISTED)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) {
|
if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) {
|
||||||
|
devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.UNVERIFIED)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,7 +363,7 @@ internal class MXMegolmEncryption(
|
||||||
// Don't bother sending to ourself
|
// Don't bother sending to ourself
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
devicesInRoom.setObject(userId, deviceId, deviceInfo)
|
devicesInRoom.allowedDevices.setObject(userId, deviceId, deviceInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (unknownDevices.isEmpty) {
|
if (unknownDevices.isEmpty) {
|
||||||
|
@ -354,9 +412,24 @@ internal class MXMegolmEncryption(
|
||||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||||
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
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)
|
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||||
sendToDeviceTask.execute(sendToDeviceParams)
|
return try {
|
||||||
return true
|
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.repository.WarnOnUnknownDeviceRepository
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||||
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class MXMegolmEncryptionFactory @Inject constructor(
|
internal class MXMegolmEncryptionFactory @Inject constructor(
|
||||||
|
@ -36,7 +37,8 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
|
||||||
private val credentials: Credentials,
|
private val credentials: Credentials,
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
private val sendToDeviceTask: SendToDeviceTask,
|
||||||
private val messageEncrypter: MessageEncrypter,
|
private val messageEncrypter: MessageEncrypter,
|
||||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository) {
|
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
||||||
|
private val taskExecutor: TaskExecutor) {
|
||||||
|
|
||||||
fun create(roomId: String): MXMegolmEncryption {
|
fun create(roomId: String): MXMegolmEncryption {
|
||||||
return MXMegolmEncryption(
|
return MXMegolmEncryption(
|
||||||
|
@ -49,6 +51,8 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
|
||||||
credentials,
|
credentials,
|
||||||
sendToDeviceTask,
|
sendToDeviceTask,
|
||||||
messageEncrypter,
|
messageEncrypter,
|
||||||
warnOnUnknownDevicesRepository)
|
warnOnUnknownDevicesRepository,
|
||||||
|
taskExecutor
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -212,7 +212,7 @@ internal class MXOlmDecryption(
|
||||||
return res["payload"]
|
return res["payload"]
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun requestKeysForEvent(event: Event) {
|
override fun requestKeysForEvent(event: Event, withHeld: Boolean) {
|
||||||
// nop
|
// nop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,3 +119,13 @@ class MXUsersDevicesMap<E> {
|
||||||
return "MXUsersDevicesMap $map"
|
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 2016 OpenMarket Ltd
|
||||||
* Copyright 2018 New Vector 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.CryptoDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
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.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.DeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||||
|
@ -416,6 +418,9 @@ internal interface IMXCryptoStore {
|
||||||
|
|
||||||
fun updateUsersTrust(check: (String) -> Boolean)
|
fun updateUsersTrust(check: (String) -> Boolean)
|
||||||
|
|
||||||
|
fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent)
|
||||||
|
fun getWithHeldMegolmSession(roomId: String, sessionId: String) : RoomKeyWithHeldContent?
|
||||||
|
|
||||||
// Dev tools
|
// Dev tools
|
||||||
|
|
||||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
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.IncomingRoomKeyRequest
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
|
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingShareRequestCommon
|
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.NewSessionListener
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
|
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
|
||||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
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.CryptoDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
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.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.DeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
import im.vector.matrix.android.internal.crypto.model.toEntity
|
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.TrustLevelEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
|
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.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.model.createPrimaryKey
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.query.delete
|
import im.vector.matrix.android.internal.crypto.store.db.query.delete
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.query.get
|
import im.vector.matrix.android.internal.crypto.store.db.query.get
|
||||||
|
@ -1427,4 +1431,32 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
return existing
|
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.OutgoingGossipingRequestEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields
|
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.UserEntityFields
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntityFields
|
||||||
import im.vector.matrix.android.internal.di.SerializeNulls
|
import im.vector.matrix.android.internal.di.SerializeNulls
|
||||||
import io.realm.DynamicRealm
|
import io.realm.DynamicRealm
|
||||||
import io.realm.RealmMigration
|
import io.realm.RealmMigration
|
||||||
|
@ -52,7 +53,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||||
// 0, 1, 2: legacy Riot-Android
|
// 0, 1, 2: legacy Riot-Android
|
||||||
// 3: migrate to RiotX schema
|
// 3: migrate to RiotX schema
|
||||||
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
|
// 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) {
|
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 <= 6) migrateTo7(realm)
|
||||||
if (oldVersion <= 7) migrateTo8(realm)
|
if (oldVersion <= 7) migrateTo8(realm)
|
||||||
if (oldVersion <= 8) migrateTo9(realm)
|
if (oldVersion <= 8) migrateTo9(realm)
|
||||||
|
if (oldVersion <= 9) migrateTo10(realm)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun migrateTo1Legacy(realm: DynamicRealm) {
|
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.OutgoingGossipingRequestEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity
|
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.UserEntity
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntity
|
||||||
import io.realm.annotations.RealmModule
|
import io.realm.annotations.RealmModule
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,6 +51,7 @@ import io.realm.annotations.RealmModule
|
||||||
GossipingEventEntity::class,
|
GossipingEventEntity::class,
|
||||||
IncomingGossipingRequestEntity::class,
|
IncomingGossipingRequestEntity::class,
|
||||||
OutgoingGossipingRequestEntity::class,
|
OutgoingGossipingRequestEntity::class,
|
||||||
MyDeviceLastSeenInfoEntity::class
|
MyDeviceLastSeenInfoEntity::class,
|
||||||
|
WithHeldSessionEntity::class
|
||||||
])
|
])
|
||||||
internal class RealmCryptoStoreModule
|
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.redacts = event.redacts
|
||||||
eventEntity.age = event.unsignedData?.age ?: event.originServerTs
|
eventEntity.age = event.unsignedData?.age ?: event.originServerTs
|
||||||
eventEntity.unsignedData = uds
|
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
|
return eventEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +91,7 @@ internal object EventMapper {
|
||||||
it.mCryptoError = eventEntity.decryptionErrorCode?.let { errorCode ->
|
it.mCryptoError = eventEntity.decryptionErrorCode?.let { errorCode ->
|
||||||
MXCryptoError.ErrorType.valueOf(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 redacts: String? = null,
|
||||||
var decryptionResultJson: String? = null,
|
var decryptionResultJson: String? = null,
|
||||||
var decryptionErrorCode: String? = null,
|
var decryptionErrorCode: String? = null,
|
||||||
|
var decryptionErrorReason: String? = null,
|
||||||
var ageLocalTs: Long? = null
|
var ageLocalTs: Long? = null
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
|
@ -62,5 +63,6 @@ internal open class EventEntity(@Index var eventId: String = "",
|
||||||
val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java)
|
val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java)
|
||||||
decryptionResultJson = adapter.toJson(decryptionResult)
|
decryptionResultJson = adapter.toJson(decryptionResult)
|
||||||
decryptionErrorCode = null
|
decryptionErrorCode = null
|
||||||
|
decryptionErrorReason = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,9 +115,10 @@ internal class TimelineEventDecryptor @Inject constructor(
|
||||||
eventEntity.setDecryptionResult(result)
|
eventEntity.setDecryptionResult(result)
|
||||||
} catch (e: MXCryptoError) {
|
} catch (e: MXCryptoError) {
|
||||||
Timber.v(e, "Failed to decrypt event $eventId")
|
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
|
// Keep track of unknown sessions to automatically try to decrypt on new session
|
||||||
eventEntity.decryptionErrorCode = e.errorType.name
|
eventEntity.decryptionErrorCode = e.errorType.name
|
||||||
|
eventEntity.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
|
||||||
event.content?.toModel<EncryptedEventContent>()?.let { content ->
|
event.content?.toModel<EncryptedEventContent>()?.let { content ->
|
||||||
content.sessionId?.let { sessionId ->
|
content.sessionId?.let { sessionId ->
|
||||||
synchronized(unknownSessionsFailure) {
|
synchronized(unknownSessionsFailure) {
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package im.vector.matrix.android.internal.session.sync
|
package im.vector.matrix.android.internal.session.sync
|
||||||
|
|
||||||
import im.vector.matrix.android.R
|
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.Event
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
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.read.FullyReadContent
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
|
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.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.room.typing.TypingEventContent
|
||||||
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
|
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
|
||||||
import im.vector.matrix.android.internal.session.sync.model.RoomSync
|
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 roomMemberEventHandler: RoomMemberEventHandler,
|
||||||
private val roomTypingUsersHandler: RoomTypingUsersHandler,
|
private val roomTypingUsersHandler: RoomTypingUsersHandler,
|
||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
private val eventBus: EventBus) {
|
private val eventBus: EventBus,
|
||||||
|
private val timelineEventDecryptor: TimelineEventDecryptor) {
|
||||||
|
|
||||||
sealed class HandlingStrategy {
|
sealed class HandlingStrategy {
|
||||||
data class JOINED(val data: Map<String, RoomSync>) : 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.events,
|
||||||
roomSync.timeline.prevToken,
|
roomSync.timeline.prevToken,
|
||||||
roomSync.timeline.limited,
|
roomSync.timeline.limited,
|
||||||
syncLocalTimestampMillis
|
syncLocalTimestampMillis,
|
||||||
|
!isInitialSync
|
||||||
)
|
)
|
||||||
roomEntity.addOrUpdate(chunkEntity)
|
roomEntity.addOrUpdate(chunkEntity)
|
||||||
}
|
}
|
||||||
|
@ -259,8 +263,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||||
eventList: List<Event>,
|
eventList: List<Event>,
|
||||||
prevToken: String? = null,
|
prevToken: String? = null,
|
||||||
isLimited: Boolean = true,
|
isLimited: Boolean = true,
|
||||||
syncLocalTimestampMillis: Long): ChunkEntity {
|
syncLocalTimestampMillis: Long,
|
||||||
val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
|
decryptOnTheFly: Boolean): ChunkEntity {
|
||||||
|
val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId)
|
||||||
val chunkEntity = if (!isLimited && lastChunk != null) {
|
val chunkEntity = if (!isLimited && lastChunk != null) {
|
||||||
lastChunk
|
lastChunk
|
||||||
} else {
|
} else {
|
||||||
|
@ -278,6 +283,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
eventIds.add(event.eventId)
|
eventIds.add(event.eventId)
|
||||||
|
|
||||||
|
if (event.isEncrypted() && decryptOnTheFly) {
|
||||||
|
decryptIfNeeded(event, roomId)
|
||||||
|
}
|
||||||
|
|
||||||
val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
|
val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
|
||||||
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm)
|
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm)
|
||||||
if (event.stateKey != null) {
|
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
|
val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
|
||||||
ContentMapper.map(rootStateEvent?.content).toModel()
|
ContentMapper.map(rootStateEvent?.content).toModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
|
chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
|
||||||
// Give info to crypto module
|
// Give info to crypto module
|
||||||
cryptoService.onLiveEvent(roomEntity.roomId, event)
|
cryptoService.onLiveEvent(roomEntity.roomId, event)
|
||||||
|
|
||||||
// Try to remove local echo
|
// Try to remove local echo
|
||||||
event.unsignedData?.transactionId?.also {
|
event.unsignedData?.transactionId?.also {
|
||||||
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
|
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
|
||||||
|
@ -324,6 +336,23 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||||
return chunkEntity
|
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(
|
data class EphemeralResult(
|
||||||
val typingUserIds: List<String> = emptyList()
|
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.crypto.MXCryptoError
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
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.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
import im.vector.riotx.core.resources.ColorProvider
|
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.core.resources.StringProvider
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
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.helper.MessageItemAttributesFactory
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
||||||
|
import me.gujun.android.span.image
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -37,6 +40,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
||||||
private val colorProvider: ColorProvider,
|
private val colorProvider: ColorProvider,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val avatarSizeProvider: AvatarSizeProvider,
|
private val avatarSizeProvider: AvatarSizeProvider,
|
||||||
|
private val drawableProvider: DrawableProvider,
|
||||||
private val attributesFactory: MessageItemAttributesFactory) {
|
private val attributesFactory: MessageItemAttributesFactory) {
|
||||||
|
|
||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
|
@ -48,20 +52,62 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
||||||
return when {
|
return when {
|
||||||
EventType.ENCRYPTED == event.root.getClearType() -> {
|
EventType.ENCRYPTED == event.root.getClearType() -> {
|
||||||
val cryptoError = event.root.mCryptoError
|
val cryptoError = event.root.mCryptoError
|
||||||
val errorDescription =
|
// val errorDescription =
|
||||||
if (cryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
// if (cryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
||||||
stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id)
|
// stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id)
|
||||||
} else {
|
// } else {
|
||||||
// TODO i18n
|
// // TODO i18n
|
||||||
cryptoError?.name
|
// 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
|
// 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()
|
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_topic_hint">Topic</string>
|
||||||
<string name="room_settings_save_success">You changed room settings successfully</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>
|
</resources>
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
<!-- android:title="@string/encryption_information_device_key" />-->
|
<!-- android:title="@string/encryption_information_device_key" />-->
|
||||||
|
|
||||||
<im.vector.riotx.core.preference.VectorSwitchPreference
|
<im.vector.riotx.core.preference.VectorSwitchPreference
|
||||||
android:enabled="false"
|
android:enabled="true"
|
||||||
android:key="SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY"
|
android:key="SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY"
|
||||||
android:summary="@string/encryption_never_send_to_unverified_devices_summary"
|
android:summary="@string/encryption_never_send_to_unverified_devices_summary"
|
||||||
android:title="@string/encryption_never_send_to_unverified_devices_title" />
|
android:title="@string/encryption_never_send_to_unverified_devices_title" />
|
||||||
|
|
Loading…
Reference in New Issue