Merge pull request #1441 from vector-im/feature/MSC2399
Feature/msc2399
This commit is contained in:
commit
6cb82421e4
@ -31,6 +31,11 @@ android {
|
||||
multiDexEnabled true
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// The following argument makes the Android Test Orchestrator run its
|
||||
// "pm clear" command after each test invocation. This command ensures
|
||||
// that the app's state is completely cleared between tests.
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
|
||||
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
||||
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
|
||||
resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
|
||||
@ -41,6 +46,10 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
debug {
|
||||
@ -181,6 +190,7 @@ dependencies {
|
||||
// Plant Timber tree for test
|
||||
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||
|
||||
kaptAndroidTest "com.google.dagger:dagger-compiler:$daggerVersion"
|
||||
androidTestImplementation 'androidx.test:core:1.2.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
@ -193,4 +203,7 @@ dependencies {
|
||||
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
// Plant Timber tree for test
|
||||
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||
|
||||
androidTestUtil 'androidx.test:orchestrator:1.2.0'
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.api
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.WorkManager
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.BuildConfig
|
||||
import im.vector.matrix.android.api.auth.AuthenticationService
|
||||
import im.vector.matrix.android.common.DaggerTestMatrixComponent
|
||||
import im.vector.matrix.android.internal.SessionManager
|
||||
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
||||
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
||||
import im.vector.matrix.android.internal.network.UserAgentHolder
|
||||
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
|
||||
import org.matrix.olm.OlmManager
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* This is the main entry point to the matrix sdk.
|
||||
* To get the singleton instance, use getInstance static method.
|
||||
*/
|
||||
class Matrix private constructor(context: Context, matrixConfiguration: MatrixConfiguration) {
|
||||
|
||||
@Inject internal lateinit var authenticationService: AuthenticationService
|
||||
@Inject internal lateinit var userAgentHolder: UserAgentHolder
|
||||
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
|
||||
@Inject internal lateinit var olmManager: OlmManager
|
||||
@Inject internal lateinit var sessionManager: SessionManager
|
||||
|
||||
init {
|
||||
Monarchy.init(context)
|
||||
DaggerTestMatrixComponent.factory().create(context, matrixConfiguration).inject(this)
|
||||
if (context.applicationContext !is Configuration.Provider) {
|
||||
WorkManager.initialize(context, Configuration.Builder().setExecutor(Executors.newCachedThreadPool()).build())
|
||||
}
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver)
|
||||
}
|
||||
|
||||
fun getUserAgent() = userAgentHolder.userAgent
|
||||
|
||||
fun authenticationService(): AuthenticationService {
|
||||
return authenticationService
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private lateinit var instance: Matrix
|
||||
private val isInit = AtomicBoolean(false)
|
||||
|
||||
fun initialize(context: Context, matrixConfiguration: MatrixConfiguration) {
|
||||
if (isInit.compareAndSet(false, true)) {
|
||||
instance = Matrix(context.applicationContext, matrixConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
fun getInstance(context: Context): Matrix {
|
||||
if (isInit.compareAndSet(false, true)) {
|
||||
val appContext = context.applicationContext
|
||||
if (appContext is MatrixConfiguration.Provider) {
|
||||
val matrixConfiguration = (appContext as MatrixConfiguration.Provider).providesMatrixConfiguration()
|
||||
instance = Matrix(appContext, matrixConfiguration)
|
||||
} else {
|
||||
throw IllegalStateException("Matrix is not initialized properly." +
|
||||
" You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.")
|
||||
}
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
fun getSdkVersion(): String {
|
||||
return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")"
|
||||
}
|
||||
|
||||
fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? {
|
||||
return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
|
||||
}
|
||||
}
|
||||
}
|
@ -57,9 +57,10 @@ class CommonTestHelper(context: Context) {
|
||||
|
||||
val matrix: Matrix
|
||||
|
||||
fun getTestInterceptor(session: Session): MockOkHttpInterceptor? = TestNetworkModule.interceptorForSession(session.sessionId) as? MockOkHttpInterceptor
|
||||
|
||||
init {
|
||||
Matrix.initialize(context, MatrixConfiguration("TestFlavor"))
|
||||
|
||||
matrix = Matrix.getInstance(context)
|
||||
}
|
||||
|
||||
@ -116,6 +117,7 @@ class CommonTestHelper(context: Context) {
|
||||
* @param nbOfMessages the number of time the message will be sent
|
||||
*/
|
||||
fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List<TimelineEvent> {
|
||||
val timeline = room.createTimeline(null, TimelineSettings(10))
|
||||
val sentEvents = ArrayList<TimelineEvent>(nbOfMessages)
|
||||
val latch = CountDownLatch(1)
|
||||
val timelineListener = object : Timeline.Listener {
|
||||
@ -134,11 +136,12 @@ class CommonTestHelper(context: Context) {
|
||||
|
||||
if (newMessages.size == nbOfMessages) {
|
||||
sentEvents.addAll(newMessages)
|
||||
// Remove listener now, if not at the next update sendEvents could change
|
||||
timeline.removeListener(this)
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
val timeline = room.createTimeline(null, TimelineSettings(10))
|
||||
timeline.start()
|
||||
timeline.addListener(timelineListener)
|
||||
for (i in 0 until nbOfMessages) {
|
||||
@ -146,11 +149,10 @@ class CommonTestHelper(context: Context) {
|
||||
}
|
||||
// Wait 3 second more per message
|
||||
await(latch, timeout = TestConstants.timeOutMillis + 3_000L * nbOfMessages)
|
||||
timeline.removeListener(timelineListener)
|
||||
timeline.dispose()
|
||||
|
||||
// Check that all events has been created
|
||||
assertEquals(nbOfMessages.toLong(), sentEvents.size.toLong())
|
||||
assertEquals("Message number do not match $sentEvents", nbOfMessages.toLong(), sentEvents.size.toLong())
|
||||
|
||||
return sentEvents
|
||||
}
|
||||
|
@ -17,8 +17,13 @@
|
||||
package im.vector.matrix.android.common
|
||||
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.Observer
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
@ -34,6 +39,7 @@ import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
@ -41,6 +47,8 @@ import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||
@ -274,4 +282,141 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||
authData = createFakeMegolmBackupAuthData()
|
||||
)
|
||||
}
|
||||
|
||||
fun createDM(alice: Session, bob: Session): String {
|
||||
val roomId = mTestHelper.doSync<String> {
|
||||
alice.createRoom(
|
||||
CreateRoomParams(invitedUserIds = listOf(bob.myUserId))
|
||||
.setDirectMessage()
|
||||
.enableEncryptionIfInvitedUsersSupportIt(),
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
val bobRoomSummariesLive = runBlocking(Dispatchers.Main) {
|
||||
bob.getRoomSummariesLive(roomSummaryQueryParams { })
|
||||
}
|
||||
|
||||
val newRoomObserver = object : Observer<List<RoomSummary>> {
|
||||
override fun onChanged(t: List<RoomSummary>?) {
|
||||
val indexOfFirst = t?.indexOfFirst { it.roomId == roomId } ?: -1
|
||||
if (indexOfFirst != -1) {
|
||||
latch.countDown()
|
||||
bobRoomSummariesLive.removeObserver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
bobRoomSummariesLive.observeForever(newRoomObserver)
|
||||
}
|
||||
}
|
||||
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
val bobRoomSummariesLive = runBlocking(Dispatchers.Main) {
|
||||
bob.getRoomSummariesLive(roomSummaryQueryParams { })
|
||||
}
|
||||
|
||||
val newRoomObserver = object : Observer<List<RoomSummary>> {
|
||||
override fun onChanged(t: List<RoomSummary>?) {
|
||||
if (bob.getRoom(roomId)
|
||||
?.getRoomMember(bob.myUserId)
|
||||
?.membership == Membership.JOIN) {
|
||||
latch.countDown()
|
||||
bobRoomSummariesLive.removeObserver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
bobRoomSummariesLive.observeForever(newRoomObserver)
|
||||
}
|
||||
|
||||
mTestHelper.doSync<Unit> { bob.joinRoom(roomId, callback = it) }
|
||||
}
|
||||
|
||||
return roomId
|
||||
}
|
||||
|
||||
fun initializeCrossSigning(session: Session) {
|
||||
mTestHelper.doSync<Unit> {
|
||||
session.cryptoService().crossSigningService()
|
||||
.initializeCrossSigning(UserPasswordAuth(
|
||||
user = session.myUserId,
|
||||
password = TestConstants.PASSWORD
|
||||
), it)
|
||||
}
|
||||
}
|
||||
|
||||
fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) {
|
||||
assertTrue(alice.cryptoService().crossSigningService().canCrossSign())
|
||||
assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
|
||||
|
||||
val requestID = UUID.randomUUID().toString()
|
||||
val aliceVerificationService = alice.cryptoService().verificationService()
|
||||
val bobVerificationService = bob.cryptoService().verificationService()
|
||||
|
||||
aliceVerificationService.beginKeyVerificationInDMs(
|
||||
VerificationMethod.SAS,
|
||||
requestID,
|
||||
roomId,
|
||||
bob.myUserId,
|
||||
bob.sessionParams.credentials.deviceId!!,
|
||||
null)
|
||||
|
||||
// we should reach SHOW SAS on both
|
||||
var alicePovTx: OutgoingSasVerificationTransaction? = null
|
||||
var bobPovTx: IncomingSasVerificationTransaction? = null
|
||||
|
||||
// wait for alice to get the ready
|
||||
mTestHelper.waitWithLatch {
|
||||
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
|
||||
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
|
||||
if (bobPovTx?.state == VerificationTxState.OnStarted) {
|
||||
bobPovTx?.performAccept()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mTestHelper.waitWithLatch {
|
||||
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||
alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID) as? OutgoingSasVerificationTransaction
|
||||
Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}")
|
||||
alicePovTx?.state == VerificationTxState.ShortCodeReady
|
||||
}
|
||||
}
|
||||
// wait for alice to get the ready
|
||||
mTestHelper.waitWithLatch {
|
||||
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
|
||||
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
|
||||
if (bobPovTx?.state == VerificationTxState.OnStarted) {
|
||||
bobPovTx?.performAccept()
|
||||
}
|
||||
bobPovTx?.state == VerificationTxState.ShortCodeReady
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation())
|
||||
|
||||
bobPovTx!!.userHasVerifiedShortCode()
|
||||
alicePovTx!!.userHasVerifiedShortCode()
|
||||
|
||||
mTestHelper.waitWithLatch {
|
||||
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||
alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
|
||||
}
|
||||
}
|
||||
|
||||
mTestHelper.waitWithLatch {
|
||||
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||
alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package im.vector.matrix.android.common
|
||||
|
||||
import im.vector.matrix.android.internal.session.TestInterceptor
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.Request
|
||||
@ -37,7 +38,7 @@ import javax.net.ssl.HttpsURLConnection
|
||||
* AutoDiscovery().findClientConfig("matrix.org", <callback>)
|
||||
* </code>
|
||||
*/
|
||||
class MockOkHttpInterceptor : Interceptor {
|
||||
class MockOkHttpInterceptor : TestInterceptor {
|
||||
|
||||
private var rules: ArrayList<Rule> = ArrayList()
|
||||
|
||||
@ -45,6 +46,12 @@ class MockOkHttpInterceptor : Interceptor {
|
||||
rules.add(rule)
|
||||
}
|
||||
|
||||
fun clearRules() {
|
||||
rules.clear()
|
||||
}
|
||||
|
||||
override var sessionId: String? = null
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.common
|
||||
|
||||
import android.content.Context
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import im.vector.matrix.android.api.MatrixConfiguration
|
||||
import im.vector.matrix.android.internal.auth.AuthModule
|
||||
import im.vector.matrix.android.internal.di.MatrixComponent
|
||||
import im.vector.matrix.android.internal.di.MatrixModule
|
||||
import im.vector.matrix.android.internal.di.MatrixScope
|
||||
import im.vector.matrix.android.internal.di.NetworkModule
|
||||
|
||||
@Component(modules = [TestModule::class, MatrixModule::class, NetworkModule::class, AuthModule::class, TestNetworkModule::class])
|
||||
@MatrixScope
|
||||
internal interface TestMatrixComponent : MatrixComponent {
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
fun create(@BindsInstance context: Context,
|
||||
@BindsInstance matrixConfiguration: MatrixConfiguration): TestMatrixComponent
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.common
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import im.vector.matrix.android.internal.di.MatrixComponent
|
||||
|
||||
@Module
|
||||
internal abstract class TestModule {
|
||||
@Binds
|
||||
abstract fun providesMatrixComponent(testMatrixComponent: TestMatrixComponent): MatrixComponent
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.common
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import im.vector.matrix.android.internal.session.MockHttpInterceptor
|
||||
import im.vector.matrix.android.internal.session.TestInterceptor
|
||||
|
||||
@Module
|
||||
internal object TestNetworkModule {
|
||||
|
||||
val interceptors = ArrayList<TestInterceptor>()
|
||||
|
||||
fun interceptorForSession(sessionId: String): TestInterceptor? = interceptors.firstOrNull { it.sessionId == sessionId }
|
||||
|
||||
@Provides
|
||||
@JvmStatic
|
||||
@MockHttpInterceptor
|
||||
fun providesTestInterceptor(): TestInterceptor? {
|
||||
return MockOkHttpInterceptor().also {
|
||||
interceptors.add(it)
|
||||
}
|
||||
}
|
||||
}
|
@ -22,10 +22,8 @@ import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import io.realm.Realm
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@ -45,22 +43,22 @@ class CryptoStoreTest : InstrumentedTest {
|
||||
Realm.init(context())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_metadata_realm_ok() {
|
||||
val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore()
|
||||
|
||||
assertFalse(cryptoStore.hasData())
|
||||
|
||||
cryptoStore.open()
|
||||
|
||||
assertEquals("deviceId_sample", cryptoStore.getDeviceId())
|
||||
|
||||
assertTrue(cryptoStore.hasData())
|
||||
|
||||
// Cleanup
|
||||
cryptoStore.close()
|
||||
cryptoStore.deleteStore()
|
||||
}
|
||||
// @Test
|
||||
// fun test_metadata_realm_ok() {
|
||||
// val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore()
|
||||
//
|
||||
// assertFalse(cryptoStore.hasData())
|
||||
//
|
||||
// cryptoStore.open()
|
||||
//
|
||||
// assertEquals("deviceId_sample", cryptoStore.getDeviceId())
|
||||
//
|
||||
// assertTrue(cryptoStore.hasData())
|
||||
//
|
||||
// // Cleanup
|
||||
// cryptoStore.close()
|
||||
// cryptoStore.deleteStore()
|
||||
// }
|
||||
|
||||
@Test
|
||||
fun test_lastSessionUsed() {
|
||||
|
@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
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.GossipingRequestState
|
||||
@ -56,6 +57,7 @@ import java.util.concurrent.CountDownLatch
|
||||
class KeyShareTests : InstrumentedTest {
|
||||
|
||||
private val mTestHelper = CommonTestHelper(context())
|
||||
private val mCryptoTestHelper = CryptoTestHelper(mTestHelper)
|
||||
|
||||
@Test
|
||||
fun test_DoNotSelfShareIfNotTrusted() {
|
||||
@ -234,6 +236,7 @@ class KeyShareTests : InstrumentedTest {
|
||||
}
|
||||
if (tx.state == VerificationTxState.ShortCodeReady) {
|
||||
session1ShortCode = tx.getDecimalCodeRepresentation()
|
||||
Thread.sleep(500)
|
||||
tx.userHasVerifiedShortCode()
|
||||
}
|
||||
}
|
||||
@ -246,6 +249,7 @@ class KeyShareTests : InstrumentedTest {
|
||||
if (tx is SasVerificationTransaction) {
|
||||
if (tx.state == VerificationTxState.ShortCodeReady) {
|
||||
session2ShortCode = tx.getDecimalCodeRepresentation()
|
||||
Thread.sleep(500)
|
||||
tx.userHasVerifiedShortCode()
|
||||
}
|
||||
}
|
||||
@ -285,5 +289,8 @@ class KeyShareTests : InstrumentedTest {
|
||||
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
|
||||
}
|
||||
}
|
||||
|
||||
mTestHelper.signOutAndClose(aliceSession1)
|
||||
mTestHelper.signOutAndClose(aliceSession2)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,245 @@
|
||||
/*
|
||||
* 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 android.util.Log
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import im.vector.matrix.android.api.NoOpMatrixCallback
|
||||
import im.vector.matrix.android.api.extensions.tryThis
|
||||
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.toModel
|
||||
import im.vector.matrix.android.common.CommonTestHelper
|
||||
import im.vector.matrix.android.common.CryptoTestHelper
|
||||
import im.vector.matrix.android.common.MockOkHttpInterceptor
|
||||
import im.vector.matrix.android.common.SessionTestParams
|
||||
import im.vector.matrix.android.common.TestConstants
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
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)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_WithHeldNoOlm() {
|
||||
val testData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
val aliceSession = testData.firstSession
|
||||
val bobSession = testData.secondSession!!
|
||||
val aliceInterceptor = mTestHelper.getTestInterceptor(aliceSession)
|
||||
|
||||
// Simulate no OTK
|
||||
aliceInterceptor!!.addRule(MockOkHttpInterceptor.SimpleRule(
|
||||
"/keys/claim",
|
||||
200,
|
||||
"""
|
||||
{ "one_time_keys" : {} }
|
||||
"""
|
||||
))
|
||||
Log.d("#TEST", "Recovery :${aliceSession.sessionParams.credentials.accessToken}")
|
||||
|
||||
val roomAlicePov = aliceSession.getRoom(testData.roomId)!!
|
||||
|
||||
val eventId = mTestHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId
|
||||
|
||||
// await for bob session to get the message
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
bobSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId) != null
|
||||
}
|
||||
}
|
||||
|
||||
// Previous message should still be undecryptable (partially withheld session)
|
||||
val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)
|
||||
try {
|
||||
// .. might need to wait a bit for stability?
|
||||
bobSession.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.NO_OLM.value, technicalMessage)
|
||||
}
|
||||
|
||||
// Ensure that alice has marked the session to be shared with bob
|
||||
val sessionId = eventBobPOV!!.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||
val chainIndex = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSession.myUserId, bobSession.sessionParams.credentials.deviceId)
|
||||
|
||||
Assert.assertEquals("Alice should have marked bob's device for this session", 0, chainIndex)
|
||||
// Add a new device for bob
|
||||
|
||||
aliceInterceptor.clearRules()
|
||||
val bobSecondSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(withInitialSync = true))
|
||||
// send a second message
|
||||
val secondMessageId = mTestHelper.sendTextMessage(roomAlicePov, "second message", 1).first().eventId
|
||||
|
||||
// Check that the
|
||||
// await for bob SecondSession session to get the message
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(secondMessageId) != null
|
||||
}
|
||||
}
|
||||
|
||||
val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSecondSession.myUserId, bobSecondSession.sessionParams.credentials.deviceId)
|
||||
|
||||
Assert.assertEquals("Alice should have marked bob's device for this session", 1, chainIndex2)
|
||||
|
||||
aliceInterceptor.clearRules()
|
||||
testData.cleanUp(mTestHelper)
|
||||
mTestHelper.signOutAndClose(bobSecondSession)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_WithHeldKeyRequest() {
|
||||
val testData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
val aliceSession = testData.firstSession
|
||||
val bobSession = testData.secondSession!!
|
||||
|
||||
val roomAlicePov = aliceSession.getRoom(testData.roomId)!!
|
||||
|
||||
val eventId = mTestHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId
|
||||
|
||||
mTestHelper.signOutAndClose(bobSession)
|
||||
|
||||
// Create a new session for bob
|
||||
|
||||
val bobSecondSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
|
||||
// initialize to force request keys if missing
|
||||
mCryptoTestHelper.initializeCrossSigning(bobSecondSession)
|
||||
|
||||
// Trust bob second device from Alice POV
|
||||
aliceSession.cryptoService().crossSigningService().trustDevice(bobSecondSession.sessionParams.deviceId!!, NoOpMatrixCallback())
|
||||
bobSecondSession.cryptoService().crossSigningService().trustDevice(aliceSession.sessionParams.deviceId!!, NoOpMatrixCallback())
|
||||
|
||||
var sessionId: String? = null
|
||||
// Check that the
|
||||
// await for bob SecondSession session to get the message
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)?.also {
|
||||
// try to decrypt and force key request
|
||||
tryThis { bobSecondSession.cryptoService().decryptEvent(it.root, "") }
|
||||
}
|
||||
sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId
|
||||
timeLineEvent != null
|
||||
}
|
||||
}
|
||||
|
||||
// Check that bob second session requested the key
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!)
|
||||
wc?.code == WithHeldCode.UNAUTHORISED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
|
||||
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.rest.DeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
@ -145,4 +146,8 @@ interface CryptoService {
|
||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||
|
||||
fun getGossipingEventsTrail(): List<Event>
|
||||
|
||||
// For testing shared session
|
||||
fun getSharedWithInfo(roomId: String?, sessionId: String) : MXUsersDevicesMap<Int>
|
||||
fun getWithHeldMegolmSession(roomId: String, sessionId: String) : RoomKeyWithHeldContent?
|
||||
}
|
||||
|
@ -59,7 +59,8 @@ sealed class MXCryptoError : Throwable() {
|
||||
MISSING_PROPERTY,
|
||||
OLM,
|
||||
UNKNOWN_DEVICES,
|
||||
UNKNOWN_MESSAGE_INDEX
|
||||
UNKNOWN_MESSAGE_INDEX,
|
||||
KEYS_WITHHELD
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -82,6 +82,9 @@ data class Event(
|
||||
@Transient
|
||||
var mCryptoError: MXCryptoError.ErrorType? = null
|
||||
|
||||
@Transient
|
||||
var mCryptoErrorReason: String? = null
|
||||
|
||||
@Transient
|
||||
var sendState: SendState = SendState.UNKNOWN
|
||||
|
||||
@ -182,6 +185,7 @@ data class Event(
|
||||
if (redacts != other.redacts) return false
|
||||
if (mxDecryptionResult != other.mxDecryptionResult) return false
|
||||
if (mCryptoError != other.mCryptoError) return false
|
||||
if (mCryptoErrorReason != other.mCryptoErrorReason) return false
|
||||
if (sendState != other.sendState) return false
|
||||
|
||||
return true
|
||||
@ -200,6 +204,7 @@ data class Event(
|
||||
result = 31 * result + (redacts?.hashCode() ?: 0)
|
||||
result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0)
|
||||
result = 31 * result + (mCryptoError?.hashCode() ?: 0)
|
||||
result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0)
|
||||
result = 31 * result + sendState.hashCode()
|
||||
return result
|
||||
}
|
||||
|
@ -66,6 +66,7 @@ object EventType {
|
||||
// Key share events
|
||||
const val ROOM_KEY_REQUEST = "m.room_key_request"
|
||||
const val FORWARDED_ROOM_KEY = "m.forwarded_room_key"
|
||||
const val ROOM_KEY_WITHHELD = "org.matrix.room_key.withheld"
|
||||
|
||||
const val REQUEST_SECRET = "m.secret.request"
|
||||
const val SEND_SECRET = "m.secret.send"
|
||||
|
@ -52,6 +52,7 @@ import im.vector.matrix.android.internal.crypto.actions.MegolmSessionDataImporte
|
||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.IMXWithHeldExtension
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService
|
||||
@ -65,6 +66,7 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.OlmEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||
@ -807,6 +809,9 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
cryptoStore.saveGossipingEvent(event)
|
||||
onSecretSendReceived(event)
|
||||
}
|
||||
EventType.ROOM_KEY_WITHHELD -> {
|
||||
onKeyWithHeldReceived(event)
|
||||
}
|
||||
else -> {
|
||||
// ignore
|
||||
}
|
||||
@ -834,6 +839,20 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
alg.onRoomKeyEvent(event, keysBackupService)
|
||||
}
|
||||
|
||||
private fun onKeyWithHeldReceived(event: Event) {
|
||||
val withHeldContent = event.getClearContent().toModel<RoomKeyWithHeldContent>() ?: return Unit.also {
|
||||
Timber.e("## CRYPTO | Malformed onKeyWithHeldReceived() : missing fields")
|
||||
}
|
||||
Timber.d("## CRYPTO | onKeyWithHeldReceived() received : content <$withHeldContent>")
|
||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm)
|
||||
if (alg is IMXWithHeldExtension) {
|
||||
alg.onRoomKeyWithHeldEvent(withHeldContent)
|
||||
} else {
|
||||
Timber.e("## CRYPTO | onKeyWithHeldReceived() : Unable to handle WithHeldContent for ${withHeldContent.algorithm}")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSecretSendReceived(event: Event) {
|
||||
Timber.i("## CRYPTO | GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}")
|
||||
if (!event.isEncrypted()) {
|
||||
@ -1197,7 +1216,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
// }
|
||||
roomDecryptorProvider
|
||||
.getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
|
||||
?.requestKeysForEvent(event) ?: run {
|
||||
?.requestKeysForEvent(event, false) ?: run {
|
||||
Timber.v("## CRYPTO | requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
|
||||
}
|
||||
}
|
||||
@ -1311,6 +1330,13 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
return cryptoStore.getGossipingEventsTrail()
|
||||
}
|
||||
|
||||
override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int> {
|
||||
return cryptoStore.getSharedWithInfo(roomId, sessionId)
|
||||
}
|
||||
|
||||
override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? {
|
||||
return cryptoStore.getWithHeldMegolmSession(roomId, sessionId)
|
||||
}
|
||||
/* ==========================================================================================
|
||||
* For test only
|
||||
* ========================================================================================== */
|
||||
|
@ -71,5 +71,5 @@ internal interface IMXDecrypting {
|
||||
|
||||
fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue : String) {}
|
||||
|
||||
fun requestKeysForEvent(event: Event)
|
||||
fun requestKeysForEvent(event: Event, withHeld: Boolean)
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.algorithms
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
|
||||
|
||||
internal interface IMXWithHeldExtension {
|
||||
fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent)
|
||||
}
|
@ -30,10 +30,12 @@ import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
|
||||
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.IMXWithHeldExtension
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.ForwardedRoomKeyContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
@ -53,7 +55,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope
|
||||
) : IMXDecrypting {
|
||||
) : IMXDecrypting, IMXWithHeldExtension {
|
||||
|
||||
var newSessionListener: NewSessionListener? = null
|
||||
|
||||
@ -61,7 +63,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||
* Events which we couldn't decrypt due to unknown sessions / indexes: map from
|
||||
* senderKey|sessionId to timelines to list of MatrixEvents.
|
||||
*/
|
||||
private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()
|
||||
// private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()
|
||||
|
||||
@Throws(MXCryptoError::class)
|
||||
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
@ -113,9 +115,21 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||
if (throwable is MXCryptoError.OlmError) {
|
||||
// TODO Check the value of .message
|
||||
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
|
||||
addEventToPendingList(event, timeline)
|
||||
// addEventToPendingList(event, timeline)
|
||||
// The session might has been partially withheld (and only pass ratcheted)
|
||||
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
||||
if (withHeldInfo != null) {
|
||||
if (requestKeysOnFail) {
|
||||
requestKeysForEvent(event, true)
|
||||
}
|
||||
// Encapsulate as withHeld exception
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.KEYS_WITHHELD,
|
||||
withHeldInfo.code?.value ?: "",
|
||||
withHeldInfo.reason)
|
||||
}
|
||||
|
||||
if (requestKeysOnFail) {
|
||||
requestKeysForEvent(event)
|
||||
requestKeysForEvent(event, false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,10 +142,25 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||
detailedReason)
|
||||
}
|
||||
if (throwable is MXCryptoError.Base) {
|
||||
if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
||||
addEventToPendingList(event, timeline)
|
||||
if (requestKeysOnFail) {
|
||||
requestKeysForEvent(event)
|
||||
if (
|
||||
/** if the session is unknown*/
|
||||
throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
|
||||
) {
|
||||
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
||||
if (withHeldInfo != null) {
|
||||
if (requestKeysOnFail) {
|
||||
requestKeysForEvent(event, true)
|
||||
}
|
||||
// Encapsulate as withHeld exception
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.KEYS_WITHHELD,
|
||||
withHeldInfo.code?.value ?: "",
|
||||
withHeldInfo.reason)
|
||||
} else {
|
||||
// This is un-used in riotX SDK, not sure if needed
|
||||
// addEventToPendingList(event, timeline)
|
||||
if (requestKeysOnFail) {
|
||||
requestKeysForEvent(event, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -147,12 +176,12 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||
*
|
||||
* @param event the event
|
||||
*/
|
||||
override fun requestKeysForEvent(event: Event) {
|
||||
override fun requestKeysForEvent(event: Event, withHeld: Boolean) {
|
||||
val sender = event.senderId ?: return
|
||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
||||
val senderDevice = encryptedEventContent?.deviceId ?: return
|
||||
|
||||
val recipients = if (event.senderId == userId) {
|
||||
val recipients = if (event.senderId == userId || withHeld) {
|
||||
mapOf(
|
||||
userId to listOf("*")
|
||||
)
|
||||
@ -176,25 +205,25 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||
outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event to the list of those we couldn't decrypt the first time we
|
||||
* saw them.
|
||||
*
|
||||
* @param event the event to try to decrypt later
|
||||
* @param timelineId the timeline identifier
|
||||
*/
|
||||
private fun addEventToPendingList(event: Event, timelineId: String) {
|
||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return
|
||||
val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}"
|
||||
|
||||
val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() }
|
||||
val events = timeline.getOrPut(timelineId) { ArrayList() }
|
||||
|
||||
if (event !in events) {
|
||||
Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}")
|
||||
events.add(event)
|
||||
}
|
||||
}
|
||||
// /**
|
||||
// * Add an event to the list of those we couldn't decrypt the first time we
|
||||
// * saw them.
|
||||
// *
|
||||
// * @param event the event to try to decrypt later
|
||||
// * @param timelineId the timeline identifier
|
||||
// */
|
||||
// private fun addEventToPendingList(event: Event, timelineId: String) {
|
||||
// val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return
|
||||
// val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}"
|
||||
//
|
||||
// val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() }
|
||||
// val events = timeline.getOrPut(timelineId) { ArrayList() }
|
||||
//
|
||||
// if (event !in events) {
|
||||
// Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}")
|
||||
// events.add(event)
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Handle a key event.
|
||||
@ -349,4 +378,10 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
cryptoStore.addWithHeldMegolmSession(withHeldInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.algorithms.megolm
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
@ -31,9 +32,14 @@ import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
|
||||
import im.vector.matrix.android.internal.crypto.model.forEach
|
||||
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
import im.vector.matrix.android.internal.util.convertToUTF8
|
||||
import timber.log.Timber
|
||||
@ -49,7 +55,8 @@ internal class MXMegolmEncryption(
|
||||
private val credentials: Credentials,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository
|
||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
||||
private val taskExecutor: TaskExecutor
|
||||
) : IMXEncrypting {
|
||||
|
||||
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
|
||||
@ -69,9 +76,26 @@ internal class MXMegolmEncryption(
|
||||
val ts = System.currentTimeMillis()
|
||||
Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom")
|
||||
val devices = getDevicesInRoom(userIds)
|
||||
Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.map}")
|
||||
val outboundSession = ensureOutboundSession(devices)
|
||||
Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}")
|
||||
val outboundSession = ensureOutboundSession(devices.allowedDevices)
|
||||
|
||||
return encryptContent(outboundSession, eventType, eventContent)
|
||||
.also {
|
||||
notifyWithheldForSession(devices.withHeldDevices, outboundSession)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyWithheldForSession(devices: MXUsersDevicesMap<WithHeldCode>, outboundSession: MXOutboundSessionInfo) {
|
||||
mutableListOf<Pair<UserDevice, WithHeldCode>>().apply {
|
||||
devices.forEach { userId, deviceId, withheldCode ->
|
||||
this.add(UserDevice(userId, deviceId) to withheldCode)
|
||||
}
|
||||
}.groupBy(
|
||||
{ it.second },
|
||||
{ it.first }
|
||||
).forEach { (code, targets) ->
|
||||
notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code)
|
||||
}
|
||||
}
|
||||
|
||||
override fun discardSessionKey() {
|
||||
@ -95,7 +119,7 @@ internal class MXMegolmEncryption(
|
||||
|
||||
defaultKeysBackupService.maybeBackupKeys()
|
||||
|
||||
return MXOutboundSessionInfo(sessionId)
|
||||
return MXOutboundSessionInfo(sessionId, SharedWithHelper(roomId, sessionId, cryptoStore))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -121,7 +145,7 @@ internal class MXMegolmEncryption(
|
||||
val deviceIds = devicesInRoom.getUserDeviceIds(userId)
|
||||
for (deviceId in deviceIds!!) {
|
||||
val deviceInfo = devicesInRoom.getObject(userId, deviceId)
|
||||
if (deviceInfo != null && null == safeSession.sharedWithDevices.getObject(userId, deviceId)) {
|
||||
if (deviceInfo != null && !cryptoStore.wasSessionSharedWithUser(roomId, safeSession.sessionId, userId, deviceId).found) {
|
||||
val devices = shareMap.getOrPut(userId) { ArrayList() }
|
||||
devices.add(deviceInfo)
|
||||
}
|
||||
@ -198,15 +222,17 @@ internal class MXMegolmEncryption(
|
||||
if (sessionResult?.sessionId == null) {
|
||||
// no session with this device, probably because there
|
||||
// were no one-time keys.
|
||||
//
|
||||
// we could send them a to_device message anyway, as a
|
||||
// signal that they have missed out on the key sharing
|
||||
// message because of the lack of keys, but there's not
|
||||
// much point in that really; it will mostly serve to clog
|
||||
// up to_device inboxes.
|
||||
//
|
||||
// ensureOlmSessionsForUsers has already done the logging,
|
||||
// so just skip it.
|
||||
|
||||
// MSC 2399
|
||||
// send withheld m.no_olm: an olm session could not be established.
|
||||
// This may happen, for example, if the sender was unable to obtain a one-time key from the recipient.
|
||||
notifyKeyWithHeld(
|
||||
listOf(UserDevice(userId, deviceID)),
|
||||
session.sessionId,
|
||||
olmDevice.deviceCurve25519Key,
|
||||
WithHeldCode.NO_OLM
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
Timber.v("## CRYPTO | shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
|
||||
@ -214,29 +240,59 @@ internal class MXMegolmEncryption(
|
||||
haveTargets = true
|
||||
}
|
||||
}
|
||||
|
||||
// Add the devices we have shared with to session.sharedWithDevices.
|
||||
// we deliberately iterate over devicesByUser (ie, the devices we
|
||||
// attempted to share with) rather than the contentMap (those we did
|
||||
// share with), because we don't want to try to claim a one-time-key
|
||||
// for dead devices on every message.
|
||||
for ((userId, devicesToShareWith) in devicesByUser) {
|
||||
for ((deviceId) in devicesToShareWith) {
|
||||
session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex)
|
||||
}
|
||||
}
|
||||
|
||||
if (haveTargets) {
|
||||
t0 = System.currentTimeMillis()
|
||||
Timber.v("## CRYPTO | shareUserDevicesKey() : has target")
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
|
||||
sendToDeviceTask.execute(sendToDeviceParams)
|
||||
Timber.v("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after "
|
||||
+ (System.currentTimeMillis() - t0) + " ms")
|
||||
|
||||
// Add the devices we have shared with to session.sharedWithDevices.
|
||||
// we deliberately iterate over devicesByUser (ie, the devices we
|
||||
// attempted to share with) rather than the contentMap (those we did
|
||||
// share with), because we don't want to try to claim a one-time-key
|
||||
// for dead devices on every message.
|
||||
for ((userId, devicesToShareWith) in devicesByUser) {
|
||||
for ((deviceId) in devicesToShareWith) {
|
||||
session.sharedWithDevices.setObject(userId, deviceId, chainIndex)
|
||||
}
|
||||
try {
|
||||
sendToDeviceTask.execute(sendToDeviceParams)
|
||||
Timber.v("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms")
|
||||
} catch (failure: Throwable) {
|
||||
// What to do here...
|
||||
Timber.e("## CRYPTO | shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ")
|
||||
}
|
||||
} else {
|
||||
Timber.v("## CRYPTO | shareUserDevicesKey() : no need to sharekey")
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyKeyWithHeld(targets: List<UserDevice>, sessionId: String, senderKey: String?, code: WithHeldCode) {
|
||||
val withHeldContent = RoomKeyWithHeldContent(
|
||||
roomId = roomId,
|
||||
senderKey = senderKey,
|
||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
sessionId = sessionId,
|
||||
codeString = code.value
|
||||
)
|
||||
val params = SendToDeviceTask.Params(
|
||||
EventType.ROOM_KEY_WITHHELD,
|
||||
MXUsersDevicesMap<Any>().apply {
|
||||
targets.forEach {
|
||||
setObject(it.userId, it.deviceId, withHeldContent)
|
||||
}
|
||||
}
|
||||
)
|
||||
sendToDeviceTask.configureWith(params) {
|
||||
callback = object : MatrixCallback<Unit> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.e("## CRYPTO | notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ")
|
||||
}
|
||||
}
|
||||
}.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
/**
|
||||
* process the pending encryptions
|
||||
*/
|
||||
@ -271,7 +327,7 @@ internal class MXMegolmEncryption(
|
||||
*
|
||||
* @param userIds the user ids whose devices must be checked.
|
||||
*/
|
||||
private suspend fun getDevicesInRoom(userIds: List<String>): MXUsersDevicesMap<CryptoDeviceInfo> {
|
||||
private suspend fun getDevicesInRoom(userIds: List<String>): DeviceInRoomInfo {
|
||||
// We are happy to use a cached version here: we assume that if we already
|
||||
// have a list of the user's devices, then we already share an e2e room
|
||||
// with them, which means that they will have announced any new devices via
|
||||
@ -280,7 +336,7 @@ internal class MXMegolmEncryption(
|
||||
val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices()
|
||||
|| cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId)
|
||||
|
||||
val devicesInRoom = MXUsersDevicesMap<CryptoDeviceInfo>()
|
||||
val devicesInRoom = DeviceInRoomInfo()
|
||||
val unknownDevices = MXUsersDevicesMap<CryptoDeviceInfo>()
|
||||
|
||||
for (userId in keys.userIds) {
|
||||
@ -294,10 +350,12 @@ internal class MXMegolmEncryption(
|
||||
}
|
||||
if (deviceInfo.isBlocked) {
|
||||
// Remove any blocked devices
|
||||
devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.BLACKLISTED)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) {
|
||||
devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.UNVERIFIED)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -305,7 +363,7 @@ internal class MXMegolmEncryption(
|
||||
// Don't bother sending to ourself
|
||||
continue
|
||||
}
|
||||
devicesInRoom.setObject(userId, deviceId, deviceInfo)
|
||||
devicesInRoom.allowedDevices.setObject(userId, deviceId, deviceInfo)
|
||||
}
|
||||
}
|
||||
if (unknownDevices.isEmpty) {
|
||||
@ -324,8 +382,12 @@ internal class MXMegolmEncryption(
|
||||
.also { Timber.w("Device not found") }
|
||||
|
||||
// Get the chain index of the key we previously sent this device
|
||||
val chainIndex = outboundSession?.sharedWithDevices?.getObject(userId, deviceId)?.toLong() ?: return false
|
||||
.also { Timber.w("[MXMegolmEncryption] reshareKey : ERROR : Never share megolm with this device") }
|
||||
val chainIndex = outboundSession?.sharedWithHelper?.wasSharedWith(userId, deviceId) ?: return false
|
||||
.also {
|
||||
// Send a room key with held
|
||||
notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED)
|
||||
Timber.w("[MXMegolmEncryption] reshareKey : ERROR : Never share megolm with this device")
|
||||
}
|
||||
|
||||
val devicesByUser = mapOf(userId to listOf(deviceInfo))
|
||||
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||
@ -343,7 +405,7 @@ internal class MXMegolmEncryption(
|
||||
.fold(
|
||||
{
|
||||
// TODO
|
||||
payloadJson["content"] = it.exportKeys(chainIndex) ?: ""
|
||||
payloadJson["content"] = it.exportKeys(chainIndex.toLong()) ?: ""
|
||||
},
|
||||
{
|
||||
// TODO
|
||||
@ -354,9 +416,24 @@ internal class MXMegolmEncryption(
|
||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
||||
Timber.v("## CRYPTO | CRYPTO | shareKeysWithDevice() : sending to $userId:$deviceId")
|
||||
Timber.v("## CRYPTO | CRYPTO | reshareKey() : sending to $userId:$deviceId")
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||
sendToDeviceTask.execute(sendToDeviceParams)
|
||||
return true
|
||||
return try {
|
||||
sendToDeviceTask.execute(sendToDeviceParams)
|
||||
true
|
||||
} catch (failure: Throwable) {
|
||||
Timber.v("## CRYPTO | CRYPTO | reshareKey() : fail to send <$sessionId> to $userId:$deviceId")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
data class DeviceInRoomInfo(
|
||||
val allowedDevices: MXUsersDevicesMap<CryptoDeviceInfo> = MXUsersDevicesMap(),
|
||||
val withHeldDevices: MXUsersDevicesMap<WithHeldCode> = MXUsersDevicesMap()
|
||||
)
|
||||
|
||||
data class UserDevice(
|
||||
val userId: String,
|
||||
val deviceId: String
|
||||
)
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupServ
|
||||
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class MXMegolmEncryptionFactory @Inject constructor(
|
||||
@ -36,7 +37,8 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
|
||||
private val credentials: Credentials,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository) {
|
||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
|
||||
private val taskExecutor: TaskExecutor) {
|
||||
|
||||
fun create(roomId: String): MXMegolmEncryption {
|
||||
return MXMegolmEncryption(
|
||||
@ -49,6 +51,8 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
|
||||
credentials,
|
||||
sendToDeviceTask,
|
||||
messageEncrypter,
|
||||
warnOnUnknownDevicesRepository)
|
||||
warnOnUnknownDevicesRepository,
|
||||
taskExecutor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -23,17 +23,14 @@ import timber.log.Timber
|
||||
|
||||
internal class MXOutboundSessionInfo(
|
||||
// The id of the session
|
||||
val sessionId: String) {
|
||||
val sessionId: String,
|
||||
val sharedWithHelper: SharedWithHelper) {
|
||||
// When the session was created
|
||||
private val creationTime = System.currentTimeMillis()
|
||||
|
||||
// Number of times this session has been used
|
||||
var useCount: Int = 0
|
||||
|
||||
// Devices with which we have shared the session key
|
||||
// userId -> {deviceId -> msgindex}
|
||||
val sharedWithDevices: MXUsersDevicesMap<Int> = MXUsersDevicesMap()
|
||||
|
||||
fun needsRotation(rotationPeriodMsgs: Int, rotationPeriodMs: Int): Boolean {
|
||||
var needsRotation = false
|
||||
val sessionLifetime = System.currentTimeMillis() - creationTime
|
||||
@ -53,6 +50,7 @@ internal class MXOutboundSessionInfo(
|
||||
* @return true if we have shared the session with devices which aren't in devicesInRoom.
|
||||
*/
|
||||
fun sharedWithTooManyDevices(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): Boolean {
|
||||
val sharedWithDevices = sharedWithHelper.sharedWithDevices()
|
||||
val userIds = sharedWithDevices.userIds
|
||||
|
||||
for (userId in userIds) {
|
||||
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.megolm
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
|
||||
internal class SharedWithHelper(
|
||||
private val roomId: String,
|
||||
private val sessionId: String,
|
||||
private val cryptoStore: IMXCryptoStore) {
|
||||
|
||||
fun sharedWithDevices(): MXUsersDevicesMap<Int> {
|
||||
return cryptoStore.getSharedWithInfo(roomId, sessionId)
|
||||
}
|
||||
|
||||
fun wasSharedWith(userId: String, deviceId: String): Int? {
|
||||
return cryptoStore.wasSessionSharedWithUser(roomId, sessionId, userId, deviceId).chainIndex
|
||||
}
|
||||
|
||||
fun markedSessionAsShared(userId: String, deviceId: String, chainIndex: Int) {
|
||||
cryptoStore.markedSessionAsShared(roomId, sessionId, userId, deviceId, chainIndex)
|
||||
}
|
||||
}
|
@ -212,7 +212,7 @@ internal class MXOlmDecryption(
|
||||
return res["payload"]
|
||||
}
|
||||
|
||||
override fun requestKeysForEvent(event: Event) {
|
||||
override fun requestKeysForEvent(event: Event, withHeld: Boolean) {
|
||||
// nop
|
||||
}
|
||||
}
|
||||
|
@ -119,3 +119,13 @@ class MXUsersDevicesMap<E> {
|
||||
return "MXUsersDevicesMap $map"
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> MXUsersDevicesMap<T>.forEach(action: (String, String, T) -> Unit) {
|
||||
userIds.forEach { userId ->
|
||||
getUserDeviceIds(userId)?.forEach { deviceId ->
|
||||
getObject(userId, deviceId)?.let {
|
||||
action(userId, deviceId, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
/**
|
||||
* Class representing an sharekey content
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RoomKeyWithHeldContent(
|
||||
|
||||
/**
|
||||
* Required if code is not m.no_olm. The ID of the room that the session belongs to.
|
||||
*/
|
||||
@Json(name = "room_id") val roomId: String? = null,
|
||||
|
||||
/**
|
||||
* Required. The encryption algorithm that the key is for.
|
||||
*/
|
||||
@Json(name = "algorithm") val algorithm: String? = null,
|
||||
|
||||
/**
|
||||
* Required if code is not m.no_olm. The ID of the session.
|
||||
*/
|
||||
@Json(name = "session_id") val sessionId: String? = null,
|
||||
|
||||
/**
|
||||
* Required. The key of the session creator.
|
||||
*/
|
||||
@Json(name = "sender_key") val senderKey: String? = null,
|
||||
|
||||
/**
|
||||
* Required. A machine-readable code for why the key was not sent
|
||||
*/
|
||||
@Json(name = "code") val codeString: String? = null,
|
||||
|
||||
/**
|
||||
* A human-readable reason for why the key was not sent. The receiving client should only use this string if it does not understand the code.
|
||||
*/
|
||||
@Json(name = "reason") val reason: String? = null
|
||||
|
||||
) {
|
||||
val code: WithHeldCode?
|
||||
get() {
|
||||
return WithHeldCode.fromCode(codeString)
|
||||
}
|
||||
}
|
||||
|
||||
enum class WithHeldCode(val value: String) {
|
||||
/**
|
||||
* the user/device was blacklisted
|
||||
*/
|
||||
BLACKLISTED("m.blacklisted"),
|
||||
/**
|
||||
* the user/devices is unverified
|
||||
*/
|
||||
UNVERIFIED("m.unverified"),
|
||||
/**
|
||||
* the user/device is not allowed have the key. For example, this would usually be sent in response
|
||||
* to a key request if the user was not in the room when the message was sent
|
||||
*/
|
||||
UNAUTHORISED("m.unauthorised"),
|
||||
/**
|
||||
* Sent in reply to a key request if the device that the key is requested from does not have the requested key
|
||||
*/
|
||||
UNAVAILABLE("m.unavailable"),
|
||||
/**
|
||||
* An olm session could not be established.
|
||||
* This may happen, for example, if the sender was unable to obtain a one-time key from the recipient.
|
||||
*/
|
||||
NO_OLM("m.no_olm");
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: String?): WithHeldCode? {
|
||||
return when (code) {
|
||||
BLACKLISTED.value -> BLACKLISTED
|
||||
UNVERIFIED.value -> UNVERIFIED
|
||||
UNAUTHORISED.value -> UNAUTHORISED
|
||||
UNAVAILABLE.value -> UNAVAILABLE
|
||||
NO_OLM.value -> NO_OLM
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2018 New Vector Ltd
|
||||
@ -30,8 +31,10 @@ import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest
|
||||
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.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||
@ -416,6 +419,13 @@ internal interface IMXCryptoStore {
|
||||
|
||||
fun updateUsersTrust(check: (String) -> Boolean)
|
||||
|
||||
fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent)
|
||||
fun getWithHeldMegolmSession(roomId: String, sessionId: String) : RoomKeyWithHeldContent?
|
||||
|
||||
fun markedSessionAsShared(roomId: String?, sessionId: String, userId: String, deviceId: String, chainIndex: Int)
|
||||
fun wasSessionSharedWithUser(roomId: String?, sessionId: String, userId: String, deviceId: String) : SharedSessionResult
|
||||
data class SharedSessionResult(val found: Boolean, val chainIndex: Int?)
|
||||
fun getSharedWithInfo(roomId: String?, sessionId: String) : MXUsersDevicesMap<Int>
|
||||
// Dev tools
|
||||
|
||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
||||
|
@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.crypto.GossipingRequestState
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
|
||||
import im.vector.matrix.android.internal.crypto.IncomingShareRequestCommon
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
|
||||
@ -38,8 +39,10 @@ import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
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.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import im.vector.matrix.android.internal.crypto.model.toEntity
|
||||
@ -66,10 +69,13 @@ import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.SharedSessionEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.createPrimaryKey
|
||||
import im.vector.matrix.android.internal.crypto.store.db.query.create
|
||||
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.getById
|
||||
@ -1427,4 +1433,68 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
return existing
|
||||
}
|
||||
}
|
||||
|
||||
override fun addWithHeldMegolmSession(withHeldContent: RoomKeyWithHeldContent) {
|
||||
val roomId = withHeldContent.roomId ?: return
|
||||
val sessionId = withHeldContent.sessionId ?: return
|
||||
if (withHeldContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
WithHeldSessionEntity.getOrCreate(realm, roomId, sessionId)?.let {
|
||||
it.code = withHeldContent.code
|
||||
it.senderKey = withHeldContent.senderKey
|
||||
it.reason = withHeldContent.reason
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? {
|
||||
return doWithRealm(realmConfiguration) { realm ->
|
||||
WithHeldSessionEntity.get(realm, roomId, sessionId)?.let {
|
||||
RoomKeyWithHeldContent(
|
||||
roomId = roomId,
|
||||
sessionId = sessionId,
|
||||
algorithm = it.algorithm,
|
||||
codeString = it.codeString,
|
||||
reason = it.reason,
|
||||
senderKey = it.senderKey
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun markedSessionAsShared(roomId: String?, sessionId: String, userId: String, deviceId: String, chainIndex: Int) {
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
SharedSessionEntity.create(
|
||||
realm = realm,
|
||||
roomId = roomId,
|
||||
sessionId = sessionId,
|
||||
userId = userId,
|
||||
deviceId = deviceId,
|
||||
chainIndex = chainIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun wasSessionSharedWithUser(roomId: String?, sessionId: String, userId: String, deviceId: String): IMXCryptoStore.SharedSessionResult {
|
||||
return doWithRealm(realmConfiguration) { realm ->
|
||||
SharedSessionEntity.get(realm, roomId, sessionId, userId, deviceId)?.let {
|
||||
IMXCryptoStore.SharedSessionResult(true, it.chainIndex)
|
||||
} ?: IMXCryptoStore.SharedSessionResult(false, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int> {
|
||||
return doWithRealm(realmConfiguration) { realm ->
|
||||
val result = MXUsersDevicesMap<Int>()
|
||||
SharedSessionEntity.get(realm, roomId, sessionId)
|
||||
.groupBy { it.userId }
|
||||
.forEach { (userId, shared) ->
|
||||
shared.forEach {
|
||||
result.setObject(userId, it.deviceId, it.chainIndex)
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,8 +36,10 @@ import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenI
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.SharedSessionEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntityFields
|
||||
import im.vector.matrix.android.internal.di.SerializeNulls
|
||||
import io.realm.DynamicRealm
|
||||
import io.realm.RealmMigration
|
||||
@ -52,7 +54,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||
// 0, 1, 2: legacy Riot-Android
|
||||
// 3: migrate to RiotX schema
|
||||
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
|
||||
const val CRYPTO_STORE_SCHEMA_VERSION = 9L
|
||||
const val CRYPTO_STORE_SCHEMA_VERSION = 10L
|
||||
}
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
@ -67,6 +69,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||
if (oldVersion <= 6) migrateTo7(realm)
|
||||
if (oldVersion <= 7) migrateTo8(realm)
|
||||
if (oldVersion <= 8) migrateTo9(realm)
|
||||
if (oldVersion <= 9) migrateTo10(realm)
|
||||
}
|
||||
|
||||
private fun migrateTo1Legacy(realm: DynamicRealm) {
|
||||
@ -416,4 +419,30 @@ 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)
|
||||
|
||||
realm.schema.create("SharedSessionEntity")
|
||||
.addField(SharedSessionEntityFields.ROOM_ID, String::class.java)
|
||||
.addField(SharedSessionEntityFields.ALGORITHM, String::class.java)
|
||||
.addField(SharedSessionEntityFields.SESSION_ID, String::class.java)
|
||||
.addIndex(SharedSessionEntityFields.SESSION_ID)
|
||||
.addField(SharedSessionEntityFields.USER_ID, String::class.java)
|
||||
.addIndex(SharedSessionEntityFields.USER_ID)
|
||||
.addField(SharedSessionEntityFields.DEVICE_ID, String::class.java)
|
||||
.addIndex(SharedSessionEntityFields.DEVICE_ID)
|
||||
.addField(SharedSessionEntityFields.CHAIN_INDEX, Long::class.java)
|
||||
.setNullable(SharedSessionEntityFields.CHAIN_INDEX, true)
|
||||
}
|
||||
}
|
||||
|
@ -28,8 +28,10 @@ import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenI
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
||||
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.SharedSessionEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntity
|
||||
import io.realm.annotations.RealmModule
|
||||
|
||||
/**
|
||||
@ -50,6 +52,8 @@ import io.realm.annotations.RealmModule
|
||||
GossipingEventEntity::class,
|
||||
IncomingGossipingRequestEntity::class,
|
||||
OutgoingGossipingRequestEntity::class,
|
||||
MyDeviceLastSeenInfoEntity::class
|
||||
MyDeviceLastSeenInfoEntity::class,
|
||||
WithHeldSessionEntity::class,
|
||||
SharedSessionEntity::class
|
||||
])
|
||||
internal class RealmCryptoStoreModule
|
||||
|
@ -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.matrix.android.internal.crypto.store.db.model
|
||||
|
||||
import io.realm.RealmObject
|
||||
import io.realm.annotations.Index
|
||||
|
||||
/**
|
||||
* Keep a record of to whom (user/device) a given session should have been shared.
|
||||
* It will be used to reply to keyshare requests from other users, in order to see if
|
||||
* this session was originaly shared with a given user
|
||||
*/
|
||||
internal open class SharedSessionEntity(
|
||||
var roomId: String? = null,
|
||||
var algorithm: String? = null,
|
||||
@Index var sessionId: String? = null,
|
||||
@Index var userId: String? = null,
|
||||
@Index var deviceId: String? = null,
|
||||
var chainIndex: Int? = null
|
||||
) : RealmObject() {
|
||||
|
||||
companion object
|
||||
}
|
@ -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,57 @@
|
||||
/*
|
||||
* 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.SharedSessionEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.SharedSessionEntityFields
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmResults
|
||||
import io.realm.kotlin.createObject
|
||||
import io.realm.kotlin.where
|
||||
|
||||
internal fun SharedSessionEntity.Companion.get(realm: Realm, roomId: String?, sessionId: String, userId: String, deviceId: String)
|
||||
: SharedSessionEntity? {
|
||||
return realm.where<SharedSessionEntity>()
|
||||
.equalTo(SharedSessionEntityFields.ROOM_ID, roomId)
|
||||
.equalTo(SharedSessionEntityFields.SESSION_ID, sessionId)
|
||||
.equalTo(SharedSessionEntityFields.ALGORITHM, MXCRYPTO_ALGORITHM_MEGOLM)
|
||||
.equalTo(SharedSessionEntityFields.USER_ID, userId)
|
||||
.equalTo(SharedSessionEntityFields.DEVICE_ID, deviceId)
|
||||
.findFirst()
|
||||
}
|
||||
|
||||
internal fun SharedSessionEntity.Companion.get(realm: Realm, roomId: String?, sessionId: String)
|
||||
: RealmResults<SharedSessionEntity> {
|
||||
return realm.where<SharedSessionEntity>()
|
||||
.equalTo(SharedSessionEntityFields.ROOM_ID, roomId)
|
||||
.equalTo(SharedSessionEntityFields.SESSION_ID, sessionId)
|
||||
.equalTo(SharedSessionEntityFields.ALGORITHM, MXCRYPTO_ALGORITHM_MEGOLM)
|
||||
.findAll()
|
||||
}
|
||||
|
||||
internal fun SharedSessionEntity.Companion.create(realm: Realm, roomId: String?, sessionId: String, userId: String, deviceId: String, chainIndex: Int)
|
||||
: SharedSessionEntity {
|
||||
return realm.createObject<SharedSessionEntity>().apply {
|
||||
this.roomId = roomId
|
||||
this.algorithm = MXCRYPTO_ALGORITHM_MEGOLM
|
||||
this.sessionId = sessionId
|
||||
this.userId = userId
|
||||
this.deviceId = deviceId
|
||||
this.chainIndex = chainIndex
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.store.db.query
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.WithHeldSessionEntityFields
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import io.realm.kotlin.where
|
||||
|
||||
internal fun WithHeldSessionEntity.Companion.get(realm: Realm, roomId: String, sessionId: String): WithHeldSessionEntity? {
|
||||
return realm.where<WithHeldSessionEntity>()
|
||||
.equalTo(WithHeldSessionEntityFields.ROOM_ID, roomId)
|
||||
.equalTo(WithHeldSessionEntityFields.SESSION_ID, sessionId)
|
||||
.equalTo(WithHeldSessionEntityFields.ALGORITHM, MXCRYPTO_ALGORITHM_MEGOLM)
|
||||
.findFirst()
|
||||
}
|
||||
|
||||
internal fun WithHeldSessionEntity.Companion.getOrCreate(realm: Realm, roomId: String, sessionId: String): WithHeldSessionEntity? {
|
||||
return get(realm, roomId, sessionId)
|
||||
?: realm.createObject<WithHeldSessionEntity>().apply {
|
||||
this.roomId = roomId
|
||||
this.algorithm = MXCRYPTO_ALGORITHM_MEGOLM
|
||||
this.sessionId = sessionId
|
||||
}
|
||||
}
|
@ -45,6 +45,12 @@ internal object EventMapper {
|
||||
eventEntity.redacts = event.redacts
|
||||
eventEntity.age = event.unsignedData?.age ?: event.originServerTs
|
||||
eventEntity.unsignedData = uds
|
||||
|
||||
eventEntity.decryptionResultJson = event.mxDecryptionResult?.let {
|
||||
MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(it)
|
||||
}
|
||||
eventEntity.decryptionErrorReason = event.mCryptoErrorReason
|
||||
eventEntity.decryptionErrorCode = event.mCryptoError?.name
|
||||
return eventEntity
|
||||
}
|
||||
|
||||
@ -85,6 +91,7 @@ internal object EventMapper {
|
||||
it.mCryptoError = eventEntity.decryptionErrorCode?.let { errorCode ->
|
||||
MXCryptoError.ErrorType.valueOf(errorCode)
|
||||
}
|
||||
it.mCryptoErrorReason = eventEntity.decryptionErrorReason
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ internal open class EventEntity(@Index var eventId: String = "",
|
||||
var redacts: String? = null,
|
||||
var decryptionResultJson: String? = null,
|
||||
var decryptionErrorCode: String? = null,
|
||||
var decryptionErrorReason: String? = null,
|
||||
var ageLocalTs: Long? = null
|
||||
) : RealmObject() {
|
||||
|
||||
@ -62,5 +63,6 @@ internal open class EventEntity(@Index var eventId: String = "",
|
||||
val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java)
|
||||
decryptionResultJson = adapter.toJson(decryptionResult)
|
||||
decryptionErrorCode = null
|
||||
decryptionErrorReason = null
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,8 @@ import im.vector.matrix.android.api.auth.AuthenticationService
|
||||
import im.vector.matrix.android.internal.SessionManager
|
||||
import im.vector.matrix.android.internal.auth.AuthModule
|
||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||
import im.vector.matrix.android.internal.session.MockHttpInterceptor
|
||||
import im.vector.matrix.android.internal.session.TestInterceptor
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
@ -34,7 +36,7 @@ import okhttp3.OkHttpClient
|
||||
import org.matrix.olm.OlmManager
|
||||
import java.io.File
|
||||
|
||||
@Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class])
|
||||
@Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class, NoOpTestModule::class])
|
||||
@MatrixScope
|
||||
internal interface MatrixComponent {
|
||||
|
||||
@ -45,6 +47,9 @@ internal interface MatrixComponent {
|
||||
@Unauthenticated
|
||||
fun okHttpClient(): OkHttpClient
|
||||
|
||||
@MockHttpInterceptor
|
||||
fun testInterceptor(): TestInterceptor?
|
||||
|
||||
fun authenticationService(): AuthenticationService
|
||||
|
||||
fun context(): Context
|
||||
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import im.vector.matrix.android.internal.session.MockHttpInterceptor
|
||||
import im.vector.matrix.android.internal.session.TestInterceptor
|
||||
|
||||
@Module
|
||||
internal object NoOpTestModule {
|
||||
|
||||
@Provides
|
||||
@JvmStatic
|
||||
@MockHttpInterceptor
|
||||
fun providesTestInterceptor(): TestInterceptor? = null
|
||||
}
|
@ -51,13 +51,14 @@ import im.vector.matrix.android.internal.di.Unauthenticated
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.di.UserMd5
|
||||
import im.vector.matrix.android.internal.eventbus.EventBusTimberLogger
|
||||
import im.vector.matrix.android.internal.network.AccessTokenInterceptor
|
||||
import im.vector.matrix.android.internal.network.DefaultNetworkConnectivityChecker
|
||||
import im.vector.matrix.android.internal.network.FallbackNetworkCallbackStrategy
|
||||
import im.vector.matrix.android.internal.network.NetworkCallbackStrategy
|
||||
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
|
||||
import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrategy
|
||||
import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||
import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor
|
||||
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
|
||||
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
|
||||
import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider
|
||||
import im.vector.matrix.android.internal.session.call.CallEventObserver
|
||||
@ -80,6 +81,11 @@ import org.greenrobot.eventbus.EventBus
|
||||
import retrofit2.Retrofit
|
||||
import java.io.File
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class MockHttpInterceptor
|
||||
|
||||
@Module
|
||||
internal abstract class SessionModule {
|
||||
@ -182,8 +188,26 @@ internal abstract class SessionModule {
|
||||
@SessionScope
|
||||
@Authenticated
|
||||
fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient,
|
||||
@Authenticated accessTokenProvider: AccessTokenProvider): OkHttpClient {
|
||||
return okHttpClient.addAccessTokenInterceptor(accessTokenProvider)
|
||||
@Authenticated accessTokenProvider: AccessTokenProvider,
|
||||
@SessionId sessionId: String,
|
||||
@MockHttpInterceptor testInterceptor: TestInterceptor?): OkHttpClient {
|
||||
return okHttpClient.newBuilder()
|
||||
.apply {
|
||||
// Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor
|
||||
val existingCurlInterceptors = interceptors().filterIsInstance<CurlLoggingInterceptor>()
|
||||
interceptors().removeAll(existingCurlInterceptors)
|
||||
|
||||
addInterceptor(AccessTokenInterceptor(accessTokenProvider))
|
||||
if (testInterceptor != null) {
|
||||
testInterceptor.sessionId = sessionId
|
||||
addInterceptor(testInterceptor)
|
||||
}
|
||||
// Re add eventually the curl logging interceptors
|
||||
existingCurlInterceptors.forEach {
|
||||
addInterceptor(it)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.session
|
||||
|
||||
import okhttp3.Interceptor
|
||||
|
||||
interface TestInterceptor : Interceptor {
|
||||
var sessionId: String?
|
||||
}
|
@ -777,7 +777,8 @@ internal class DefaultTimeline(
|
||||
`in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray())
|
||||
}
|
||||
if (settings.filterUseless) {
|
||||
not().equalTo(TimelineEventEntityFields.ROOT.IS_USELESS, true)
|
||||
not()
|
||||
.equalTo(TimelineEventEntityFields.ROOT.IS_USELESS, true)
|
||||
}
|
||||
if (settings.filterEdits) {
|
||||
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
|
||||
|
@ -115,9 +115,10 @@ internal class TimelineEventDecryptor @Inject constructor(
|
||||
eventEntity.setDecryptionResult(result)
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.v(e, "Failed to decrypt event $eventId")
|
||||
if (e is MXCryptoError.Base && e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
||||
if (e is MXCryptoError.Base /*&& e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID*/) {
|
||||
// Keep track of unknown sessions to automatically try to decrypt on new session
|
||||
eventEntity.decryptionErrorCode = e.errorType.name
|
||||
eventEntity.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
|
||||
event.content?.toModel<EncryptedEventContent>()?.let { content ->
|
||||
content.sessionId?.let { sessionId ->
|
||||
synchronized(unknownSessionsFailure) {
|
||||
|
@ -17,6 +17,7 @@
|
||||
package im.vector.matrix.android.internal.session.sync
|
||||
|
||||
import im.vector.matrix.android.R
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
@ -51,6 +52,7 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMemberEvent
|
||||
import im.vector.matrix.android.internal.session.room.read.FullyReadContent
|
||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor
|
||||
import im.vector.matrix.android.internal.session.room.typing.TypingEventContent
|
||||
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomSync
|
||||
@ -71,7 +73,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||
private val roomMemberEventHandler: RoomMemberEventHandler,
|
||||
private val roomTypingUsersHandler: RoomTypingUsersHandler,
|
||||
@UserId private val userId: String,
|
||||
private val eventBus: EventBus) {
|
||||
private val eventBus: EventBus,
|
||||
private val timelineEventDecryptor: TimelineEventDecryptor) {
|
||||
|
||||
sealed class HandlingStrategy {
|
||||
data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
|
||||
@ -162,7 +165,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||
roomSync.timeline.events,
|
||||
roomSync.timeline.prevToken,
|
||||
roomSync.timeline.limited,
|
||||
syncLocalTimestampMillis
|
||||
syncLocalTimestampMillis,
|
||||
isInitialSync
|
||||
)
|
||||
roomEntity.addOrUpdate(chunkEntity)
|
||||
}
|
||||
@ -259,7 +263,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||
eventList: List<Event>,
|
||||
prevToken: String? = null,
|
||||
isLimited: Boolean = true,
|
||||
syncLocalTimestampMillis: Long): ChunkEntity {
|
||||
syncLocalTimestampMillis: Long,
|
||||
isInitialSync: Boolean): ChunkEntity {
|
||||
val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
|
||||
val chunkEntity = if (!isLimited && lastChunk != null) {
|
||||
lastChunk
|
||||
@ -278,6 +283,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||
continue
|
||||
}
|
||||
eventIds.add(event.eventId)
|
||||
|
||||
if (event.isEncrypted() && !isInitialSync) {
|
||||
decryptIfNeeded(event, roomId)
|
||||
}
|
||||
|
||||
val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
|
||||
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm)
|
||||
if (event.stateKey != null) {
|
||||
@ -295,9 +305,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||
val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
|
||||
ContentMapper.map(rootStateEvent?.content).toModel()
|
||||
}
|
||||
|
||||
chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
|
||||
// Give info to crypto module
|
||||
cryptoService.onLiveEvent(roomEntity.roomId, event)
|
||||
|
||||
// Try to remove local echo
|
||||
event.unsignedData?.transactionId?.also {
|
||||
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
|
||||
@ -324,6 +336,23 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||
return chunkEntity
|
||||
}
|
||||
|
||||
private fun decryptIfNeeded(event: Event, roomId: String) {
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
if (e is MXCryptoError.Base) {
|
||||
event.mCryptoError = e.errorType
|
||||
event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class EphemeralResult(
|
||||
val typingUserIds: List<String> = emptyList()
|
||||
)
|
||||
|
@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1
|
||||
# android\.text\.TextUtils
|
||||
|
||||
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If it is ok, change the value in file forbidden_strings_in_code.txt
|
||||
enum class===72
|
||||
enum class===73
|
||||
|
||||
### Do not import temporary legacy classes
|
||||
import im.vector.matrix.android.internal.legacy.riot===3
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -75,6 +75,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
||||
data class DeclineVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction()
|
||||
data class RequestVerification(val userId: String) : RoomDetailAction()
|
||||
data class ResumeVerification(val transactionId: String, val otherUserId: String?) : RoomDetailAction()
|
||||
data class TapOnFailedToDecrypt(val eventId: String) : RoomDetailAction()
|
||||
data class ReRequestKeys(val eventId: String) : RoomDetailAction()
|
||||
|
||||
object SelectStickerAttachment : RoomDetailAction()
|
||||
|
@ -86,6 +86,8 @@ import im.vector.matrix.android.api.session.widgets.model.WidgetType
|
||||
import im.vector.matrix.android.api.util.MatrixItem
|
||||
import im.vector.matrix.android.api.util.toMatrixItem
|
||||
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.dialogs.ConfirmationDialogBuilder
|
||||
import im.vector.riotx.core.dialogs.withColoredButton
|
||||
@ -340,6 +342,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it)
|
||||
is RoomDetailViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
|
||||
is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it)
|
||||
is RoomDetailViewEvents.ShowE2EErrorMessage -> displayE2eError(it.withHeldCode)
|
||||
RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager()
|
||||
is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it)
|
||||
is RoomDetailViewEvents.DisplayEnableIntegrationsWarning -> displayDisabledIntegrationDialog()
|
||||
@ -941,6 +944,20 @@ class RoomDetailFragment @Inject constructor(
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun displayE2eError(withHeldCode: WithHeldCode?) {
|
||||
val msgId = when (withHeldCode) {
|
||||
WithHeldCode.BLACKLISTED -> R.string.crypto_error_withheld_blacklisted
|
||||
WithHeldCode.UNVERIFIED -> R.string.crypto_error_withheld_unverified
|
||||
WithHeldCode.UNAUTHORISED,
|
||||
WithHeldCode.UNAVAILABLE -> R.string.crypto_error_withheld_generic
|
||||
else -> R.string.notice_crypto_unable_to_decrypt_friendly_desc
|
||||
}
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setMessage(msgId)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun promptReasonToReportContent(action: EventSharedAction.ReportContentCustom) {
|
||||
val inflater = requireActivity().layoutInflater
|
||||
val layout = inflater.inflate(R.layout.dialog_report_content, null)
|
||||
@ -1205,13 +1222,18 @@ class RoomDetailFragment @Inject constructor(
|
||||
roomDetailViewModel.handle(RoomDetailAction.LoadMoreTimelineEvents(direction))
|
||||
}
|
||||
|
||||
override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) {
|
||||
if (messageContent is MessageVerificationRequestContent) {
|
||||
roomDetailViewModel.handle(RoomDetailAction.ResumeVerification(informationData.eventId, null))
|
||||
override fun onEventCellClicked(informationData: MessageInformationData, messageContent: Any?, view: View) {
|
||||
when (messageContent) {
|
||||
is MessageVerificationRequestContent -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.ResumeVerification(informationData.eventId, null))
|
||||
}
|
||||
is EncryptedEventContent -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View): Boolean {
|
||||
override fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
val roomId = roomDetailArgs.roomId
|
||||
|
||||
|
@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import im.vector.matrix.android.api.session.widgets.model.Widget
|
||||
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
|
||||
import im.vector.riotx.core.platform.VectorViewEvents
|
||||
import im.vector.riotx.features.command.Command
|
||||
import java.io.File
|
||||
@ -33,6 +34,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
|
||||
data class ActionFailure(val action: RoomDetailAction, val throwable: Throwable) : RoomDetailViewEvents()
|
||||
|
||||
data class ShowMessage(val message: String) : RoomDetailViewEvents()
|
||||
data class ShowE2EErrorMessage(val withHeldCode: WithHeldCode?) : RoomDetailViewEvents()
|
||||
|
||||
data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents()
|
||||
|
||||
|
@ -32,6 +32,7 @@ import im.vector.matrix.android.api.MatrixPatterns
|
||||
import im.vector.matrix.android.api.NoOpMatrixCallback
|
||||
import im.vector.matrix.android.api.query.QueryStringValue
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
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.isImageMessage
|
||||
import im.vector.matrix.android.api.session.events.model.isTextMessage
|
||||
@ -59,6 +60,7 @@ import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
|
||||
import im.vector.matrix.android.api.util.toOptional
|
||||
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.matrix.rx.unwrap
|
||||
import im.vector.riotx.R
|
||||
@ -259,6 +261,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
|
||||
is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action)
|
||||
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
|
||||
is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action)
|
||||
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
|
||||
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
|
||||
is RoomDetailAction.StartCall -> handleStartCall(action)
|
||||
@ -1034,6 +1037,19 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTapOnFailedToDecrypt(action: RoomDetailAction.TapOnFailedToDecrypt) {
|
||||
room.getTimeLineEvent(action.eventId)?.let {
|
||||
val code = when (it.root.mCryptoError) {
|
||||
MXCryptoError.ErrorType.KEYS_WITHHELD -> {
|
||||
WithHeldCode.fromCode(it.root.mCryptoErrorReason)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
_viewEvents.post(RoomDetailViewEvents.ShowE2EErrorMessage(code))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReplyToOptions(action: RoomDetailAction.ReplyToOptions) {
|
||||
room.sendOptionsReply(action.eventId, action.optionIndex, action.optionValue)
|
||||
}
|
||||
|
@ -26,7 +26,6 @@ import com.airbnb.epoxy.EpoxyController
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.VisibilityState
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
||||
@ -89,8 +88,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
}
|
||||
|
||||
interface BaseCallback {
|
||||
fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View)
|
||||
fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View): Boolean
|
||||
fun onEventCellClicked(informationData: MessageInformationData, messageContent: Any?, view: View)
|
||||
fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean
|
||||
}
|
||||
|
||||
interface AvatarCallback {
|
||||
@ -283,13 +282,16 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
private fun getModels(): List<EpoxyModel<*>> {
|
||||
buildCacheItemsIfNeeded()
|
||||
return modelCache
|
||||
.map {
|
||||
val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) {
|
||||
.map { cacheItemData ->
|
||||
val eventModel = if (cacheItemData == null || mergedHeaderItemFactory.isCollapsed(cacheItemData.localId)) {
|
||||
null
|
||||
} else {
|
||||
it.eventModel
|
||||
cacheItemData.eventModel
|
||||
}
|
||||
listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel)
|
||||
listOf(eventModel,
|
||||
cacheItemData?.mergedHeaderModel,
|
||||
cacheItemData?.formattedDayModel?.takeIf { eventModel != null || cacheItemData.mergedHeaderModel != null }
|
||||
)
|
||||
}
|
||||
.flatten()
|
||||
.filterNotNull()
|
||||
|
@ -18,10 +18,13 @@ package im.vector.riotx.features.home.room.detail.timeline.factory
|
||||
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.resources.DrawableProvider
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
@ -29,6 +32,8 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformat
|
||||
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_
|
||||
import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
||||
import im.vector.riotx.features.settings.VectorPreferences
|
||||
import me.gujun.android.span.image
|
||||
import me.gujun.android.span.span
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -37,7 +42,9 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
||||
private val colorProvider: ColorProvider,
|
||||
private val stringProvider: StringProvider,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val attributesFactory: MessageItemAttributesFactory) {
|
||||
private val drawableProvider: DrawableProvider,
|
||||
private val attributesFactory: MessageItemAttributesFactory,
|
||||
private val vectorPreferences: VectorPreferences) {
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
@ -48,25 +55,63 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
||||
return when {
|
||||
EventType.ENCRYPTED == event.root.getClearType() -> {
|
||||
val cryptoError = event.root.mCryptoError
|
||||
val errorDescription =
|
||||
if (cryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
||||
stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id)
|
||||
} else {
|
||||
// TODO i18n
|
||||
cryptoError?.name
|
||||
}
|
||||
|
||||
val 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 = if (vectorPreferences.developerMode()) {
|
||||
val errorDescription =
|
||||
if (cryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
||||
stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id)
|
||||
} else {
|
||||
// TODO i18n
|
||||
cryptoError?.name
|
||||
}
|
||||
|
||||
val message = stringProvider.getString(R.string.encrypted_message).takeIf { cryptoError == null }
|
||||
?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription)
|
||||
span(message) {
|
||||
textStyle = "italic"
|
||||
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
||||
}
|
||||
} else {
|
||||
val colorFromAttribute = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
|
||||
if (cryptoError == null) {
|
||||
span(stringProvider.getString(R.string.encrypted_message)) {
|
||||
textStyle = "italic"
|
||||
textColor = colorFromAttribute
|
||||
}
|
||||
} else {
|
||||
when (cryptoError) {
|
||||
MXCryptoError.ErrorType.KEYS_WITHHELD -> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO This is not correct format for error, change it
|
||||
|
||||
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
||||
val attributes = attributesFactory.create(null, informationData, callback)
|
||||
val attributes = attributesFactory.create(event.root.content.toModel<EncryptedEventContent>(), informationData, callback)
|
||||
return MessageTextItem_()
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.highlighted(highlight)
|
||||
|
@ -36,11 +36,16 @@ import im.vector.riotx.features.home.room.detail.timeline.item.MergedMembershipE
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MergedMembershipEventsItem_
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreationItem
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreationItem_
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MergedUTDItem
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MergedUTDItem_
|
||||
import im.vector.riotx.features.settings.VectorPreferences
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer,
|
||||
class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val activeSessionHolder: ActiveSessionHolder) {
|
||||
private val vectorPreferences: VectorPreferences) {
|
||||
|
||||
private val collapsedEventIds = linkedSetOf<Long>()
|
||||
private val mergeItemCollapseStates = HashMap<Long, Boolean>()
|
||||
@ -58,7 +63,10 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av
|
||||
callback: TimelineEventController.Callback?,
|
||||
requestModelBuild: () -> Unit)
|
||||
: BasedMergedItem<*>? {
|
||||
return if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE
|
||||
return if (shouldMergedAsCannotDecryptGroup(event, nextEvent)) {
|
||||
Timber.v("## MERGE: Candidate for merge, top event ${event.eventId}")
|
||||
buildUTDMergedSummary(currentPosition, items, event, eventIdToHighlight, /*requestModelBuild,*/ callback)
|
||||
} else if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE
|
||||
&& event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel<RoomCreateContent>()?.creator)) {
|
||||
// It's the first item before room.create
|
||||
// Collapse all room configuration events
|
||||
@ -130,6 +138,82 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av
|
||||
}
|
||||
}
|
||||
|
||||
// Event should be UTD
|
||||
// Next event should not
|
||||
private fun shouldMergedAsCannotDecryptGroup(event: TimelineEvent, nextEvent: TimelineEvent?): Boolean {
|
||||
if (!vectorPreferences.mergeUTDinTimeline()) return false
|
||||
// if event is not UTD return false
|
||||
if (!isEventUTD(event)) return false
|
||||
// At this point event cannot be decrypted
|
||||
// Let's check if older event is not UTD
|
||||
return nextEvent == null || !isEventUTD(event)
|
||||
}
|
||||
|
||||
private fun isEventUTD(event: TimelineEvent): Boolean {
|
||||
return event.root.getClearType() == EventType.ENCRYPTED && !event.root.isRedacted()
|
||||
}
|
||||
|
||||
private fun buildUTDMergedSummary(currentPosition: Int,
|
||||
items: List<TimelineEvent>,
|
||||
event: TimelineEvent,
|
||||
eventIdToHighlight: String?,
|
||||
// requestModelBuild: () -> Unit,
|
||||
callback: TimelineEventController.Callback?): MergedUTDItem_? {
|
||||
Timber.v("## MERGE: buildUTDMergedSummary from position $currentPosition")
|
||||
var prevEvent = items.prevOrNull(currentPosition)
|
||||
var tmpPos = currentPosition - 1
|
||||
val mergedEvents = ArrayList<TimelineEvent>().also { it.add(event) }
|
||||
|
||||
while (prevEvent != null && isEventUTD(prevEvent)) {
|
||||
mergedEvents.add(prevEvent)
|
||||
tmpPos--
|
||||
prevEvent = if (tmpPos >= 0) items[tmpPos] else null
|
||||
}
|
||||
|
||||
Timber.v("## MERGE: buildUTDMergedSummary merge group size ${mergedEvents.size}")
|
||||
if (mergedEvents.size < 3) return null
|
||||
|
||||
var highlighted = false
|
||||
val mergedData = ArrayList<BasedMergedItem.Data>(mergedEvents.size)
|
||||
mergedEvents.reversed()
|
||||
.forEach { mergedEvent ->
|
||||
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
|
||||
highlighted = true
|
||||
}
|
||||
val senderAvatar = mergedEvent.senderInfo.avatarUrl
|
||||
val senderName = mergedEvent.senderInfo.disambiguatedDisplayName
|
||||
val data = BasedMergedItem.Data(
|
||||
userId = mergedEvent.root.senderId ?: "",
|
||||
avatarUrl = senderAvatar,
|
||||
memberName = senderName,
|
||||
localId = mergedEvent.localId,
|
||||
eventId = mergedEvent.root.eventId ?: ""
|
||||
)
|
||||
mergedData.add(data)
|
||||
}
|
||||
val mergedEventIds = mergedEvents.map { it.localId }
|
||||
|
||||
collapsedEventIds.addAll(mergedEventIds)
|
||||
|
||||
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
|
||||
|
||||
val attributes = MergedUTDItem.Attributes(
|
||||
isCollapsed = true,
|
||||
mergeData = mergedData,
|
||||
avatarRenderer = avatarRenderer,
|
||||
onCollapsedStateChanged = {}
|
||||
)
|
||||
return MergedUTDItem_()
|
||||
.id(mergeId)
|
||||
.big(mergedEventIds.size > 5)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.highlighted(highlighted)
|
||||
.attributes(attributes)
|
||||
.also {
|
||||
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildRoomCreationMergedSummary(currentPosition: Int,
|
||||
items: List<TimelineEvent>,
|
||||
event: TimelineEvent,
|
||||
|
@ -18,7 +18,6 @@
|
||||
package im.vector.riotx.features.home.room.detail.timeline.helper
|
||||
|
||||
import android.view.View
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.riotx.EmojiCompatFontProvider
|
||||
import im.vector.riotx.core.utils.DebouncedClickListener
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
@ -34,7 +33,7 @@ class MessageItemAttributesFactory @Inject constructor(
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val emojiCompatFontProvider: EmojiCompatFontProvider) {
|
||||
|
||||
fun create(messageContent: MessageContent?,
|
||||
fun create(messageContent: Any?,
|
||||
informationData: MessageInformationData,
|
||||
callback: TimelineEventController.Callback?): AbsMessageItem.Attributes {
|
||||
return AbsMessageItem.Attributes(
|
||||
|
@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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.features.home.room.detail.timeline.item
|
||||
|
||||
import android.util.TypedValue
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
|
||||
abstract class MergedUTDItem : BasedMergedItem<MergedUTDItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
override lateinit var attributes: Attributes
|
||||
|
||||
@EpoxyAttribute
|
||||
var big: Boolean? = false
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
|
||||
holder.mergedTile.updateLayoutParams<RelativeLayout.LayoutParams> {
|
||||
this.marginEnd = leftGuideline
|
||||
if (big == true) {
|
||||
this.height = TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
800f,
|
||||
holder.view.context.resources.displayMetrics
|
||||
).toInt()
|
||||
} else {
|
||||
this.height = LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
}
|
||||
|
||||
// if (attributes.isCollapsed) {
|
||||
// // Take the oldest data
|
||||
// val data = distinctMergeData.lastOrNull()
|
||||
//
|
||||
// val summary = holder.expandView.resources.getString(R.string.room_created_summary_item,
|
||||
// data?.memberName ?: data?.userId ?: "")
|
||||
// holder.summaryView.text = summary
|
||||
// holder.summaryView.visibility = View.VISIBLE
|
||||
// holder.avatarView.visibility = View.VISIBLE
|
||||
// if (data != null) {
|
||||
// holder.avatarView.visibility = View.VISIBLE
|
||||
// attributes.avatarRenderer.render(data.toMatrixItem(), holder.avatarView)
|
||||
// } else {
|
||||
// holder.avatarView.visibility = View.GONE
|
||||
// }
|
||||
//
|
||||
// if (attributes.hasEncryptionEvent) {
|
||||
// holder.encryptionTile.isVisible = true
|
||||
// holder.encryptionTile.updateLayoutParams<RelativeLayout.LayoutParams> {
|
||||
// this.marginEnd = leftGuideline
|
||||
// }
|
||||
// if (attributes.isEncryptionAlgorithmSecure) {
|
||||
// holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
|
||||
// holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
|
||||
// holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||
// holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
// ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
|
||||
// null, null, null
|
||||
// )
|
||||
// } else {
|
||||
// holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled)
|
||||
// holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description)
|
||||
// holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
// ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning),
|
||||
// null, null, null
|
||||
// )
|
||||
// }
|
||||
// } else {
|
||||
// holder.encryptionTile.isVisible = false
|
||||
// }
|
||||
// } else {
|
||||
// holder.avatarView.visibility = View.INVISIBLE
|
||||
// holder.summaryView.visibility = View.GONE
|
||||
// holder.encryptionTile.isGone = true
|
||||
// }
|
||||
// No read receipt for this item
|
||||
holder.readReceiptsView.isVisible = false
|
||||
}
|
||||
|
||||
class Holder : BasedMergedItem.Holder(STUB_ID) {
|
||||
// val summaryView by bind<TextView>(R.id.itemNoticeTextView)
|
||||
// val avatarView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
||||
val mergedTile by bind<ViewGroup>(R.id.mergedUTDTile)
|
||||
//
|
||||
// val e2eTitleTextView by bind<TextView>(R.id.itemVerificationDoneTitleTextView)
|
||||
// val e2eTitleDescriptionView by bind<TextView>(R.id.itemVerificationDoneDetailTextView)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STUB_ID = R.id.messageContentMergedUTDStub
|
||||
}
|
||||
|
||||
data class Attributes(
|
||||
override val isCollapsed: Boolean,
|
||||
override val mergeData: List<Data>,
|
||||
override val avatarRenderer: AvatarRenderer,
|
||||
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
|
||||
override val onCollapsedStateChanged: (Boolean) -> Unit
|
||||
) : BasedMergedItem.Attributes
|
||||
}
|
@ -144,6 +144,8 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
||||
private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
|
||||
private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"
|
||||
private const val SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY"
|
||||
// SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS
|
||||
private const val SETTINGS_LABS_MERGE_E2E_ERRORS = "SETTINGS_LABS_MERGE_E2E_ERRORS"
|
||||
|
||||
// analytics
|
||||
const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY"
|
||||
@ -265,6 +267,10 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
||||
return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY, true)
|
||||
}
|
||||
|
||||
fun mergeUTDinTimeline(): Boolean {
|
||||
return defaultPrefs.getBoolean(SETTINGS_LABS_MERGE_E2E_ERRORS, false)
|
||||
}
|
||||
|
||||
fun labAllowedExtendedLogging(): Boolean {
|
||||
return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, false)
|
||||
}
|
||||
|
@ -385,8 +385,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||
// }
|
||||
// }
|
||||
|
||||
sendToUnverifiedDevicesPref.isChecked = false
|
||||
|
||||
sendToUnverifiedDevicesPref.isChecked = session.cryptoService().getGlobalBlacklistUnverifiedDevices()
|
||||
|
||||
sendToUnverifiedDevicesPref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
|
21
vector/src/main/res/drawable/ic_clock.xml
Normal file
21
vector/src/main/res/drawable/ic_clock.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="10dp"
|
||||
android:height="10dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,22C17.5228,22 22,17.5228 22,12C22,6.4771 17.5228,2 12,2C6.4771,2 2,6.4771 2,12C2,17.5228 6.4771,22 12,22Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,6V12L15,15"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
11
vector/src/main/res/drawable/ic_forbidden.xml
Normal file
11
vector/src/main/res/drawable/ic_forbidden.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<vector android:height="10dp" android:viewportHeight="22"
|
||||
android:viewportWidth="22" android:width="10dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#00000000" android:fillType="evenOdd"
|
||||
android:pathData="M11,21C16.5228,21 21,16.5228 21,11C21,5.4771 16.5228,1 11,1C5.4771,1 1,5.4771 1,11C1,16.5228 5.4771,21 11,21Z"
|
||||
android:strokeColor="#2E2F32" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
<path android:fillColor="#00000000" android:fillType="evenOdd"
|
||||
android:pathData="M17.4692,4.3983L4.8165,17.6833"
|
||||
android:strokeColor="#2E2F32" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
</vector>
|
@ -61,6 +61,13 @@
|
||||
tools:layout_marginTop="240dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentMergedUTDStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout="@layout/item_timeline_event_merged_utd_stub"
|
||||
tools:layout_marginTop="380dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView
|
||||
|
@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/mergedUTDTile"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="52dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:background="@drawable/rounded_rect_shape_8"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemVerificationDoneTitleTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="8dp"
|
||||
android:drawableStart="@drawable/ic_clock"
|
||||
android:drawablePadding="2dp"
|
||||
android:gravity="start"
|
||||
android:text="@string/notice_crypto_unable_to_decrypt_merged"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="15sp"
|
||||
app:drawableTint="?riotx_text_secondary" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemVerificationDoneDetailTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/notice_crypto_unable_to_decrypt_friendly_desc"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="italic" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemMergedExpandTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="2dp"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingRight="8dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:text="@string/merged_events_expand"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="italic"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/itemMergedSeparatorView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_below="@+id/mergedUTDTile"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="?attr/riotx_header_panel_background" />
|
||||
|
||||
</RelativeLayout>
|
@ -1734,6 +1734,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
||||
<string name="room_directory_search_hint">Name or ID (#example:matrix.org)</string>
|
||||
|
||||
<string name="labs_swipe_to_reply_in_timeline">Enable swipe to reply in timeline</string>
|
||||
<string name="labs_merge_e2e_in_timeline">Merge failed to decrypt message in timeline</string>
|
||||
|
||||
<string name="link_copied_to_clipboard">Link copied to clipboard</string>
|
||||
|
||||
@ -2501,4 +2502,12 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
||||
<string name="room_settings_topic_hint">Topic</string>
|
||||
<string name="room_settings_save_success">You changed room settings successfully</string>
|
||||
|
||||
<string name="notice_crypto_unable_to_decrypt_final">You cannot access this message</string>
|
||||
<string name="notice_crypto_unable_to_decrypt_friendly">Waiting for this message, this may take a while</string>
|
||||
<string name="crypto_utd">Cannot Decrypt</string>
|
||||
<string name="notice_crypto_unable_to_decrypt_friendly_desc">Due to end-to-end encryption, you might need to wait for someone\'s message to arrive because the encryption keys were not properly sent to you.</string>
|
||||
<string name="crypto_error_withheld_blacklisted">You cannot access this message because you have been blocked by the sender</string>
|
||||
<string name="crypto_error_withheld_unverified">You cannot access this message because your session is not trusted by the sender</string>
|
||||
<string name="crypto_error_withheld_generic">You cannot access this message because the sender purposely did not send the keys</string>
|
||||
<string name="notice_crypto_unable_to_decrypt_merged">Waiting for encryption history</string>
|
||||
</resources>
|
||||
|
@ -40,6 +40,10 @@
|
||||
android:title="@string/labs_swipe_to_reply_in_timeline" />
|
||||
|
||||
|
||||
<im.vector.riotx.core.preference.VectorSwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="SETTINGS_LABS_MERGE_E2E_ERRORS"
|
||||
android:title="@string/labs_merge_e2e_in_timeline" />
|
||||
<!--</im.vector.riotx.core.preference.VectorPreferenceCategory>-->
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
@ -29,7 +29,7 @@
|
||||
<!-- android:title="@string/encryption_information_device_key" />-->
|
||||
|
||||
<im.vector.riotx.core.preference.VectorSwitchPreference
|
||||
android:enabled="false"
|
||||
android:enabled="true"
|
||||
android:key="SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY"
|
||||
android:summary="@string/encryption_never_send_to_unverified_devices_summary"
|
||||
android:title="@string/encryption_never_send_to_unverified_devices_title" />
|
||||
|
Loading…
x
Reference in New Issue
Block a user