Gossiping refactoring

This commit is contained in:
Valere 2020-03-06 18:29:15 +01:00
parent 3639007985
commit fc6225a7ac
62 changed files with 2811 additions and 984 deletions

View File

@ -97,6 +97,7 @@ dependencies {
def coroutines_version = "1.3.2" def coroutines_version = "1.3.2"
def markwon_version = '3.1.0' def markwon_version = '3.1.0'
def daggerVersion = '2.25.4' def daggerVersion = '2.25.4'
def work_version = '2.3.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
@ -126,7 +127,7 @@ dependencies {
kapt 'dk.ilios:realmfieldnameshelper:1.1.1' kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
// Work // Work
implementation "androidx.work:work-runtime-ktx:2.3.3" implementation "androidx.work:work-runtime-ktx:$work_version"
// FP // FP
implementation "io.arrow-kt:arrow-core:$arrow_version" implementation "io.arrow-kt:arrow-core:$arrow_version"

View File

@ -0,0 +1,210 @@
/*
* 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.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.SessionTestParams
import im.vector.matrix.android.common.TestConstants
import im.vector.matrix.android.internal.crypto.GossipingRequestState
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import junit.framework.TestCase.fail
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import java.util.concurrent.CountDownLatch
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class KeyShareTests : InstrumentedTest {
private val mTestHelper = CommonTestHelper(context())
// @Before
// fun setup() {
// mockkStatic(Log::class)
// every { Log.v(any(), any()) } returns 0
// every { Log.d(any(), any()) } returns 0
// every { Log.i(any(), any()) } returns 0
// every { Log.e(any(), any()) } returns 0
//// every { Log.println(any(), any(), any()) } returns 0
//// every { Log.wtf(any(), any(), any()) } returns 0
// }
@Test
fun test_DoNotSelfShareIfNotTrusted() {
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
// Create an encrypted room and add a message
val roomId = mTestHelper.doSync<String> {
aliceSession.createRoom(
CreateRoomParams(RoomDirectoryVisibility.PRIVATE).enableEncryptionWithAlgorithm(true),
it
)
}
val room = aliceSession.getRoom(roomId)
assertNotNull(room)
Thread.sleep(4_000)
assertTrue(room?.isEncrypted() == true)
val sentEventId = mTestHelper.sendTextMessage(room!!, "My Message", 1).first().eventId
// Open a new sessionx
val aliceSession2 = mTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
val roomSecondSessionPOV = aliceSession2.getRoom(roomId)
val receivedEvent = roomSecondSessionPOV?.getTimeLineEvent(sentEventId)
assertNotNull(receivedEvent)
assert(receivedEvent!!.isEncrypted())
try {
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
fail("should fail")
} catch (failure: Throwable) {
}
val outgoingRequestBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequest()
// Try to request
aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root)
val waitLatch = CountDownLatch(1)
val eventMegolmSessionId = receivedEvent.root.content.toModel<EncryptedEventContent>()?.sessionId
var outGoingRequestId: String? = null
retryPeriodicallyWithLatch(waitLatch) {
aliceSession2.cryptoService().getOutgoingRoomKeyRequest()
.filter { req ->
// filter out request that was known before
!outgoingRequestBefore.any { req.requestId == it.requestId }
}
.let {
val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
outGoingRequestId = outgoing?.requestId
outgoing != null
}
}
mTestHelper.await(waitLatch)
Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId")
val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequest()
//We should have a new request
Assert.assertTrue(outgoingRequestAfter.size > outgoingRequestBefore.size)
Assert.assertNotNull(outgoingRequestAfter.first { it.sessionId == eventMegolmSessionId })
// The first session should see an incoming request
// the request should be refused, because the device is not trusted
waitWithLatch { latch ->
retryPeriodicallyWithLatch(latch) {
// DEBUG LOGS
aliceSession.cryptoService().getIncomingRoomKeyRequest().let {
Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
Log.v("TEST", "=========================")
it.forEach { keyRequest ->
Log.v("TEST", "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId} is ${keyRequest.state}")
}
Log.v("TEST", "=========================")
}
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequest().firstOrNull { it.requestId == outGoingRequestId }
incoming?.state == GossipingRequestState.REJECTED
}
}
try {
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
fail("should fail")
} catch (failure: Throwable) {
}
// Mark the device as trusted
aliceSession.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId,
aliceSession2.sessionParams.credentials.deviceId ?: "")
// Re request
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
waitWithLatch { latch ->
retryPeriodicallyWithLatch(latch) {
aliceSession.cryptoService().getIncomingRoomKeyRequest().let {
Log.v("TEST", "Incoming request Session 1")
Log.v("TEST", "=========================")
it.forEach {
Log.v("TEST", "requestId ${it.requestId}, for sessionId ${it.requestBody?.sessionId} is ${it.state}")
}
Log.v("TEST", "=========================")
it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == GossipingRequestState.ACCEPTED }
}
}
}
Thread.sleep(6_000)
waitWithLatch { latch ->
retryPeriodicallyWithLatch(latch) {
aliceSession2.cryptoService().getOutgoingRoomKeyRequest().let {
it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == OutgoingGossipingRequestState.CANCELLED }
}
}
}
try {
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
} catch (failure: Throwable) {
fail("should have been able to decrypt")
}
mTestHelper.signOutAndClose(aliceSession)
mTestHelper.signOutAndClose(aliceSession2)
}
fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
GlobalScope.launch {
while (true) {
delay(1000)
if (condition()) {
latch.countDown()
return@launch
}
}
}
}
fun waitWithLatch(block: (CountDownLatch) -> Unit) {
val latch = CountDownLatch(1)
block(latch)
mTestHelper.await(latch)
}
}

View File

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.crypto.keysbackup package im.vector.matrix.android.internal.crypto.keysbackup
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.api.listeners.ProgressListener
@ -34,6 +35,7 @@ import im.vector.matrix.android.common.assertDictEquals
import im.vector.matrix.android.common.assertListEquals import im.vector.matrix.android.common.assertListEquals
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import im.vector.matrix.android.internal.crypto.MegolmSessionData import im.vector.matrix.android.internal.crypto.MegolmSessionData
import im.vector.matrix.android.internal.crypto.ShareRequestState
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
@ -41,12 +43,15 @@ import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersio
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
import io.mockk.every
import io.mockk.mockkStatic
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Assert.fail import org.junit.Assert.fail
import org.junit.BeforeClass
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -325,46 +330,46 @@ class KeysBackupTest : InstrumentedTest {
* - Restore must be successful * - Restore must be successful
* - *** There must be no more pending key share requests * - *** There must be no more pending key share requests
*/ */
@Test // @Test
fun restoreKeysBackupAndKeyShareRequestTest() { // fun restoreKeysBackupAndKeyShareRequestTest() {
fail("Check with Valere for this test. I think we do not send key share request") // fail("Check with Valere for this test. I think we do not send key share request")
//
val testData = createKeysBackupScenarioWithPassword(null) // val testData = createKeysBackupScenarioWithPassword(null)
//
// - Check the SDK sent key share requests // // - Check the SDK sent key share requests
val cryptoStore2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store // val cryptoStore2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
val unsentRequest = cryptoStore2 // val unsentRequest = cryptoStore2
.getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT)) // .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT))
val sentRequest = cryptoStore2 // val sentRequest = cryptoStore2
.getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT)) // .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT))
//
// Request is either sent or unsent // // Request is either sent or unsent
assertTrue(unsentRequest != null || sentRequest != null) // assertTrue(unsentRequest != null || sentRequest != null)
//
// - Restore the e2e backup from the homeserver // // - Restore the e2e backup from the homeserver
val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> { // val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> {
testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, // testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, // testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
null, // null,
null, // null,
null, // null,
it // it
) // )
} // }
//
checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) // checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
//
// - There must be no more pending key share requests // // - There must be no more pending key share requests
val unsentRequestAfterRestoration = cryptoStore2 // val unsentRequestAfterRestoration = cryptoStore2
.getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT)) // .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT))
val sentRequestAfterRestoration = cryptoStore2 // val sentRequestAfterRestoration = cryptoStore2
.getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT)) // .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT))
//
// Request is either sent or unsent // // Request is either sent or unsent
assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null) // assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null)
//
testData.cleanUp(mTestHelper) // testData.cleanUp(mTestHelper)
} // }
/** /**
* - Do an e2e backup to the homeserver with a recovery key * - Do an e2e backup to the homeserver with a recovery key

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.verification.qrcode
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64NoPadding
import org.amshove.kluent.shouldBeNull import org.amshove.kluent.shouldBeNull
import org.amshove.kluent.shouldEqual import org.amshove.kluent.shouldEqual
import org.amshove.kluent.shouldEqualTo import org.amshove.kluent.shouldEqualTo

View File

@ -23,5 +23,14 @@ data class MXCryptoConfig(
// Tell whether the encryption of the event content is enabled for the invited members. // Tell whether the encryption of the event content is enabled for the invited members.
// SDK clients can disable this by settings it to false. // SDK clients can disable this by settings it to false.
// Note that the encryption for the invited members will be blocked if the history visibility is "joined". // Note that the encryption for the invited members will be blocked if the history visibility is "joined".
var enableEncryptionForInvitedMembers: Boolean = true var enableEncryptionForInvitedMembers: Boolean = true,
/**
* If set to true, the SDK will automatically ignore room key request (gossiping)
* coming from your other untrusted sessions (or blocked).
* If set to false, the request will be forwarded to the application layer; in this
* case the application can decide to prompt the user.
*/
var discardRoomKeyRequestsFromUntrustedDevices : Boolean = true
) )

View File

@ -0,0 +1,25 @@
/*
* 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.extensions
inline fun <A> tryThis(operation: () -> A): A? {
return try {
operation()
} catch (any: Throwable) {
null
}
}

View File

@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestList
import im.vector.matrix.android.api.session.crypto.verification.VerificationService import im.vector.matrix.android.api.session.crypto.verification.VerificationService
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.crypto.NewSessionListener import im.vector.matrix.android.internal.crypto.NewSessionListener
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
@ -87,6 +88,8 @@ interface CryptoService {
fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo?
fun requestRoomKeyForEvent(event: Event)
fun reRequestRoomKeyForEvent(event: Event) fun reRequestRoomKeyForEvent(event: Event)
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody)
@ -132,4 +135,6 @@ interface CryptoService {
fun removeSessionListener(listener: NewSessionListener) fun removeSessionListener(listener: NewSessionListener)
fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest> fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest>
fun getIncomingRoomKeyRequest(): List<IncomingRoomKeyRequest>
fun getGossipingEventsTrail(): List<Event>
} }

View File

@ -68,4 +68,7 @@ interface CrossSigningService {
fun checkDeviceTrust(otherUserId: String, fun checkDeviceTrust(otherUserId: String,
otherDeviceId: String, otherDeviceId: String,
locallyTrusted: Boolean?): DeviceTrustResult locallyTrusted: Boolean?): DeviceTrustResult
fun onSecretSSKGossip(sskPrivateKey: String): Boolean
fun onSecretUSKGossip(uskPrivateKey: String): Boolean
} }

View File

@ -111,6 +111,9 @@ interface SharedSecretStorageService {
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?) : IntegrityResult fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?) : IntegrityResult
fun requestSecret(name: String, myOtherDeviceId: String)
data class KeyRef( data class KeyRef(
val keyId: String?, val keyId: String?,
val keySpec: SsssKeySpec? val keySpec: SsssKeySpec?

View File

@ -0,0 +1,119 @@
/*
* 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
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.failure.shouldBeRetried
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.LocalEcho
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.ShareRequestCancellation
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import im.vector.matrix.android.internal.worker.getSessionComponent
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject
internal class CancelGossipRequestWorker(context: Context,
params: WorkerParameters)
: CoroutineWorker(context, params) {
@JsonClass(generateAdapter = true)
internal data class Params(
val sessionId: String,
val requestId: String,
val recipients: Map<String, List<String>>
) {
companion object {
fun fromRequest(sessionId: String, request: OutgoingGossipingRequest): Params {
return Params(
sessionId = sessionId,
requestId = request.requestId,
recipients = request.recipients
)
}
}
}
@Inject lateinit var sendToDeviceTask: SendToDeviceTask
@Inject lateinit var cryptoStore: IMXCryptoStore
@Inject lateinit var eventBus: EventBus
@Inject lateinit var credentials: Credentials
override suspend fun doWork(): Result {
val errorOutputData = Data.Builder().putBoolean("failed", true).build()
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.success(errorOutputData)
val sessionComponent = getSessionComponent(params.sessionId)
?: return Result.success(errorOutputData).also {
// TODO, can this happen? should I update local echo?
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
}
sessionComponent.inject(this)
val localId = LocalEcho.createLocalEchoId()
val contentMap = MXUsersDevicesMap<Any>()
val toDeviceContent = ShareRequestCancellation(
requestingDeviceId = credentials.deviceId,
requestId = params.requestId
)
cryptoStore.saveGossipingEvent(Event(
type = EventType.ROOM_KEY_REQUEST,
content = toDeviceContent.toContent(),
senderId = credentials.userId
).also {
it.ageLocalTs = System.currentTimeMillis()
})
params.recipients.forEach { userToDeviceMap ->
userToDeviceMap.value.forEach { deviceId ->
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
}
}
try {
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLING)
sendToDeviceTask.execute(
SendToDeviceTask.Params(
eventType = EventType.ROOM_KEY_REQUEST,
contentMap = contentMap,
transactionId = localId
)
)
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLED)
return Result.success()
} catch (exception: Throwable) {
return if (exception.shouldBeRetried()) {
Result.retry()
} else {
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.FAILED_TO_CANCEL)
Result.success(errorOutputData)
}
}
}
}

View File

@ -33,6 +33,8 @@ import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
@ -47,6 +49,7 @@ import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAct
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService
@ -55,7 +58,9 @@ 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.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult 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.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.RoomKeyContent
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
@ -80,6 +85,7 @@ import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembers
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
import im.vector.matrix.android.internal.session.sync.model.SyncResponse import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
@ -137,7 +143,7 @@ internal class DefaultCryptoService @Inject constructor(
// //
private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager, private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager,
// //
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
// Actions // Actions
private val setDeviceVerificationAction: SetDeviceVerificationAction, private val setDeviceVerificationAction: SetDeviceVerificationAction,
private val megolmSessionDataImporter: MegolmSessionDataImporter, private val megolmSessionDataImporter: MegolmSessionDataImporter,
@ -189,6 +195,7 @@ internal class DefaultCryptoService @Inject constructor(
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) { override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
setDeviceNameTask setDeviceNameTask
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) { .configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) {
this.executionThread = TaskThread.CRYPTO
this.callback = object : MatrixCallback<Unit> { this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
// bg refresh of crypto device // bg refresh of crypto device
@ -207,6 +214,7 @@ internal class DefaultCryptoService @Inject constructor(
override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) { override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) {
deleteDeviceTask deleteDeviceTask
.configureWith(DeleteDeviceTask.Params(deviceId)) { .configureWith(DeleteDeviceTask.Params(deviceId)) {
this.executionThread = TaskThread.CRYPTO
this.callback = callback this.callback = callback
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
@ -215,6 +223,7 @@ internal class DefaultCryptoService @Inject constructor(
override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>) { override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>) {
deleteDeviceWithUserPasswordTask deleteDeviceWithUserPasswordTask
.configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password)) { .configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password)) {
this.executionThread = TaskThread.CRYPTO
this.callback = callback this.callback = callback
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
@ -231,6 +240,7 @@ internal class DefaultCryptoService @Inject constructor(
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) { override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
getDevicesTask getDevicesTask
.configureWith { .configureWith {
this.executionThread = TaskThread.CRYPTO
this.callback = callback this.callback = callback
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
@ -239,6 +249,7 @@ internal class DefaultCryptoService @Inject constructor(
override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) { override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
getDeviceInfoTask getDeviceInfoTask
.configureWith(GetDeviceInfoTask.Params(deviceId)) { .configureWith(GetDeviceInfoTask.Params(deviceId)) {
this.executionThread = TaskThread.CRYPTO
this.callback = callback this.callback = callback
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
@ -301,7 +312,6 @@ internal class DefaultCryptoService @Inject constructor(
runCatching { runCatching {
uploadDeviceKeys() uploadDeviceKeys()
oneTimeKeysUploader.maybeUploadOneTimeKeys() oneTimeKeysUploader.maybeUploadOneTimeKeys()
outgoingRoomKeyRequestManager.start()
keysBackupService.checkAndStartKeysBackup() keysBackupService.checkAndStartKeysBackup()
if (isInitialSync) { if (isInitialSync) {
// refresh the devices list for each known room members // refresh the devices list for each known room members
@ -329,8 +339,6 @@ internal class DefaultCryptoService @Inject constructor(
fun close() = runBlocking(coroutineDispatchers.crypto) { fun close() = runBlocking(coroutineDispatchers.crypto) {
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
outgoingRoomKeyRequestManager.stop()
olmDevice.release() olmDevice.release()
cryptoStore.close() cryptoStore.close()
} }
@ -689,18 +697,24 @@ internal class DefaultCryptoService @Inject constructor(
* @param event the event * @param event the event
*/ */
fun onToDeviceEvent(event: Event) { fun onToDeviceEvent(event: Event) {
// event have already been decrypted
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
when (event.getClearType()) { when (event.getClearType()) {
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> { EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
cryptoStore.saveGossipingEvent(event)
// Keys are imported directly, not waiting for end of sync
onRoomKeyEvent(event) onRoomKeyEvent(event)
} }
EventType.REQUEST_SECRET, EventType.REQUEST_SECRET,
EventType.ROOM_KEY_REQUEST -> { EventType.ROOM_KEY_REQUEST -> {
// save audit trail
cryptoStore.saveGossipingEvent(event)
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
incomingRoomKeyRequestManager.onGossipingRequestEvent(event) incomingRoomKeyRequestManager.onGossipingRequestEvent(event)
} }
EventType.SEND_SECRET -> { EventType.SEND_SECRET -> {
// incomingRoomKeyRequestManager.onGossipingRequestEvent(event) cryptoStore.saveGossipingEvent(event)
onSecretSendReceived(event)
} }
else -> { else -> {
// ignore // ignore
@ -716,6 +730,7 @@ internal class DefaultCryptoService @Inject constructor(
*/ */
private fun onRoomKeyEvent(event: Event) { private fun onRoomKeyEvent(event: Event) {
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
Timber.v("## onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>")
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
Timber.e("## onRoomKeyEvent() : missing fields") Timber.e("## onRoomKeyEvent() : missing fields")
return return
@ -728,6 +743,78 @@ internal class DefaultCryptoService @Inject constructor(
alg.onRoomKeyEvent(event, keysBackupService) alg.onRoomKeyEvent(event, keysBackupService)
} }
private fun onSecretSendReceived(event: Event) {
if (!event.isEncrypted()) {
// secret send messages must be encrypted
Timber.e("## onSecretSend() :Received unencrypted secret send event")
return
}
// Was that sent by us?
if (event.senderId != credentials.userId) {
Timber.e("## onSecretSend() : Ignore secret from other user ${event.senderId}")
return
}
val encryptedEventContent = event.getClearContent().toModel<EncryptedEventContent>()
?: return Unit.also {
Timber.e("## onSecretSend() :Received malformed secret send event")
}
val senderDevice = encryptedEventContent.senderKey
val device = senderDevice?.let { cryptoStore.getUserDevice(event.senderId, it) }
?: return Unit.also {
Timber.e("## processIncomingSecretShareRequest() : Received secret share request from unknown device ${senderDevice}")
}
try {
val result = decryptEvent(event, "gossip")
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (failure: Throwable) {
Timber.i("## onSecretSend() :Failed to decrypt secret share: $device")
}
val secretContent = event.getClearContent().toModel<SecretSendEventContent>() ?: return
val existingRequest = cryptoStore
.getPendingIncomingGossipingRequests()
.firstOrNull { it.requestId == secretContent.requestId } as? IncomingSecretShareRequest
if (existingRequest == null) {
Timber.i("## onSecretSend() :Received secret from unknown request id: ${secretContent.requestId} from device ")
return
}
if (device.isBlocked || !device.isVerified) {
// Ignore secrets from this
Timber.i("## onSecretSend() :Received secret from untrusted/blocked device: ${device}")
return
}
when (existingRequest.secretName) {
SELF_SIGNING_KEY_SSSS_NAME -> {
if (device.trustLevel?.isLocallyVerified() == true) {
crossSigningService.onSecretSSKGossip(secretContent.secretValue)
return
}
}
USER_SIGNING_KEY_SSSS_NAME -> {
if (device.trustLevel?.isLocallyVerified() == true) {
cryptoStore.storePrivateKeysInfo(null, null, secretContent.secretValue)
}
}
else -> {
// Ask to application layer?
}
}
}
/** /**
* Handle an m.room.encryption event. * Handle an m.room.encryption event.
* *
@ -1003,14 +1090,14 @@ internal class DefaultCryptoService @Inject constructor(
setRoomBlacklistUnverifiedDevices(roomId, false) setRoomBlacklistUnverifiedDevices(roomId, false)
} }
// TODO Check if this method is still necessary // TODO Check if this method is still necessary
/** /**
* Cancel any earlier room key request * Cancel any earlier room key request
* *
* @param requestBody requestBody * @param requestBody requestBody
*/ */
override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) { override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody) outgoingGossipingRequestManager.cancelRoomKeyRequest(requestBody)
} }
/** /**
@ -1019,20 +1106,36 @@ internal class DefaultCryptoService @Inject constructor(
* @param event the event to decrypt again. * @param event the event to decrypt again.
*/ */
override fun reRequestRoomKeyForEvent(event: Event) { override fun reRequestRoomKeyForEvent(event: Event) {
val wireContent = event.content val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
if (wireContent == null) {
Timber.e("## reRequestRoomKeyForEvent Failed to re-request key, null content") Timber.e("## reRequestRoomKeyForEvent Failed to re-request key, null content")
return
} }
val requestBody = RoomKeyRequestBody( val requestBody = RoomKeyRequestBody(
algorithm = wireContent["algorithm"]?.toString(), algorithm = wireContent.algorithm,
roomId = event.roomId, roomId = event.roomId,
senderKey = wireContent["sender_key"]?.toString(), senderKey = wireContent.senderKey,
sessionId = wireContent["session_id"]?.toString() sessionId = wireContent.sessionId
) )
outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody) outgoingGossipingRequestManager.resendRoomKeyRequest(requestBody)
}
override fun requestRoomKeyForEvent(event: Event) {
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
Timber.e("## requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}")
}
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
if (!isStarted()) {
Timber.v("## requestRoomKeyForEvent() : wait after e2e init")
internalStart(false)
}
roomDecryptorProvider
.getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
?.requestKeysForEvent(event) ?: run {
Timber.v("## requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
}
}
} }
/** /**
@ -1090,7 +1193,7 @@ internal class DefaultCryptoService @Inject constructor(
override fun removeSessionListener(listener: NewSessionListener) { override fun removeSessionListener(listener: NewSessionListener) {
roomDecryptorProvider.removeSessionListener(listener) roomDecryptorProvider.removeSessionListener(listener)
} }
/* ========================================================================================== /* ==========================================================================================
* DEBUG INFO * DEBUG INFO
* ========================================================================================== */ * ========================================================================================== */
@ -1101,4 +1204,12 @@ internal class DefaultCryptoService @Inject constructor(
override fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest> { override fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest> {
return cryptoStore.getOutgoingRoomKeyRequests() return cryptoStore.getOutgoingRoomKeyRequests()
} }
override fun getIncomingRoomKeyRequest(): List<IncomingRoomKeyRequest> {
return cryptoStore.getIncomingRoomKeyRequests()
}
override fun getGossipingEventsTrail(): List<Event> {
return cryptoStore.getGossipingEventsTrail()
}
} }

View File

@ -0,0 +1,43 @@
/*
* 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
enum class GossipRequestType {
KEY,
SECRET
}
enum class GossipingRequestState {
NONE,
PENDING,
REJECTED,
ACCEPTED,
// USER_REJECTED,
UNABLE_TO_PROCESS,
CANCELLED_BY_REQUESTER,
RE_REQUESTED
}
enum class OutgoingGossipingRequestState {
UNSENT,
SENDING,
SENT,
CANCELLING,
CANCELLED,
FAILED_TO_SEND,
FAILED_TO_CANCEL
}

View File

@ -37,7 +37,8 @@ data class IncomingRequestCancellation(
/** /**
* The request id * The request id
*/ */
override val requestId: String? = null override val requestId: String? = null,
override val localCreationTimestamp: Long?
) : IncomingShareRequestCommon { ) : IncomingShareRequestCommon {
companion object { companion object {
/** /**
@ -52,7 +53,8 @@ data class IncomingRequestCancellation(
IncomingRequestCancellation( IncomingRequestCancellation(
userId = event.senderId, userId = event.senderId,
deviceId = it.requestingDeviceId, deviceId = it.requestingDeviceId,
requestId = it.requestId requestId = it.requestId,
localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis()
) )
} }
} }

View File

@ -46,6 +46,8 @@ data class IncomingRoomKeyRequest(
*/ */
val requestBody: RoomKeyRequestBody? = null, val requestBody: RoomKeyRequestBody? = null,
val state: GossipingRequestState = GossipingRequestState.NONE,
/** /**
* The runnable to call to accept to share the keys * The runnable to call to accept to share the keys
*/ */
@ -56,7 +58,8 @@ data class IncomingRoomKeyRequest(
* The runnable to call to ignore the key share request. * The runnable to call to ignore the key share request.
*/ */
@Transient @Transient
var ignore: Runnable? = null var ignore: Runnable? = null,
override val localCreationTimestamp: Long?
) : IncomingShareRequestCommon { ) : IncomingShareRequestCommon {
companion object { companion object {
/** /**
@ -72,7 +75,8 @@ data class IncomingRoomKeyRequest(
userId = event.senderId, userId = event.senderId,
deviceId = it.requestingDeviceId, deviceId = it.requestingDeviceId,
requestId = it.requestId, requestId = it.requestId,
requestBody = it.body ?: RoomKeyRequestBody() requestBody = it.body ?: RoomKeyRequestBody(),
localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis()
) )
} }
} }

View File

@ -17,7 +17,7 @@
package im.vector.matrix.android.internal.crypto package im.vector.matrix.android.internal.crypto
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService import im.vector.matrix.android.api.crypto.MXCryptoConfig
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener
@ -35,8 +35,8 @@ import javax.inject.Inject
internal class IncomingRoomKeyRequestManager @Inject constructor( internal class IncomingRoomKeyRequestManager @Inject constructor(
private val credentials: Credentials, private val credentials: Credentials,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
private val crossSigningService: CrossSigningService, private val cryptoConfig: MXCryptoConfig,
private val secrSecretCryptoProvider: ShareSecretCryptoProvider, private val secretSecretCryptoProvider: ShareSecretCryptoProvider,
private val roomDecryptorProvider: RoomDecryptorProvider) { private val roomDecryptorProvider: RoomDecryptorProvider) {
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
@ -48,8 +48,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet() private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet()
init { init {
receiveGossipingRequests.addAll(cryptoStore.getPendingIncomingSecretShareRequests()) receiveGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
receiveGossipingRequests.addAll(cryptoStore.getPendingIncomingRoomKeyRequests())
} }
/** /**
@ -59,21 +58,37 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
* @param event the announcement event. * @param event the announcement event.
*/ */
fun onGossipingRequestEvent(event: Event) { fun onGossipingRequestEvent(event: Event) {
Timber.v("## onGossipingRequestEvent type ${event.type} from user ${event.senderId}")
val roomKeyShare = event.getClearContent().toModel<GossipingDefaultContent>() val roomKeyShare = event.getClearContent().toModel<GossipingDefaultContent>()
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
when (roomKeyShare?.action) { when (roomKeyShare?.action) {
GossipingToDeviceObject.ACTION_SHARE_REQUEST -> { GossipingToDeviceObject.ACTION_SHARE_REQUEST -> {
if (event.getClearType() == EventType.REQUEST_SECRET) { if (event.getClearType() == EventType.REQUEST_SECRET) {
IncomingSecretShareRequest.fromEvent(event)?.let { IncomingSecretShareRequest.fromEvent(event)?.let {
// save in DB
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
receiveGossipingRequests.add(it) receiveGossipingRequests.add(it)
} }
} else if (event.getClearType() == EventType.ROOM_KEY_REQUEST) { } else if (event.getClearType() == EventType.ROOM_KEY_REQUEST) {
IncomingRoomKeyRequest.fromEvent(event)?.let { IncomingRoomKeyRequest.fromEvent(event)?.let {
if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) {
// ignore, it was sent by me as *
Timber.v("## onGossipingRequestEvent type ${event.type} ignore remote echo")
} else {
cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
receiveGossipingRequests.add(it) receiveGossipingRequests.add(it)
} }
} }
} }
GossipingToDeviceObject.ACTION_SHARE_CANCELLATION -> IncomingRequestCancellation.fromEvent(event)?.let { receivedRequestCancellations.add(it) } }
else -> Timber.e("## onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}") GossipingToDeviceObject.ACTION_SHARE_CANCELLATION -> {
IncomingRequestCancellation.fromEvent(event)?.let {
receivedRequestCancellations.add(it)
}
}
else -> {
Timber.e("## onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}")
}
} }
} }
@ -83,6 +98,8 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
* It must be called on CryptoThread * It must be called on CryptoThread
*/ */
fun processReceivedGossipingRequests() { fun processReceivedGossipingRequests() {
Timber.v("## processReceivedGossipingRequests()")
val roomKeyRequestsToProcess = receiveGossipingRequests.toList() val roomKeyRequestsToProcess = receiveGossipingRequests.toList()
receiveGossipingRequests.clear() receiveGossipingRequests.clear()
for (request in roomKeyRequestsToProcess) { for (request in roomKeyRequestsToProcess) {
@ -102,16 +119,25 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
} }
} }
if (null != receivedRequestCancellations) { receivedRequestCancellations?.forEach { request ->
for (request in receivedRequestCancellations!!) { Timber.v("## processReceivedGossipingRequests() : m.room_key_request cancellation $request")
Timber.v("## ## processReceivedGossipingRequests() : m.room_key_request cancellation for " + request.userId
+ ":" + request.deviceId + " id " + request.requestId)
// we should probably only notify the app of cancellations we told it // we should probably only notify the app of cancellations we told it
// about, but we don't currently have a record of that, so we just pass // about, but we don't currently have a record of that, so we just pass
// everything through. // everything through.
if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) {
// ignore remote echo
return@forEach
}
val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "")
if (matchingIncoming == null) {
// ignore that?
return@forEach
} else {
// If it was accepted from this device, keep the information, do not mark as cancelled
if (matchingIncoming.state != GossipingRequestState.ACCEPTED) {
onRoomKeyRequestCancellation(request) onRoomKeyRequestCancellation(request)
cryptoStore.deleteIncomingRoomKeyRequest(request) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER)
}
} }
} }
} }
@ -123,11 +149,11 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
val roomId = body!!.roomId val roomId = body!!.roomId
val alg = body.algorithm val alg = body.algorithm
Timber.v("m.room_key_request from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}") Timber.v("## processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
if (userId == null || credentials.userId != userId) { if (userId == null || credentials.userId != userId) {
// TODO: determine if we sent this device the keys already: in // TODO: determine if we sent this device the keys already: in
Timber.w("## processReceivedGossipingRequests() : Ignoring room key request from other user for now") Timber.w("## processReceivedGossipingRequests() : Ignoring room key request from other user for now")
cryptoStore.deleteIncomingRoomKeyRequest(request) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return return
} }
// TODO: should we queue up requests we don't yet have keys for, in case they turn up later? // TODO: should we queue up requests we don't yet have keys for, in case they turn up later?
@ -136,89 +162,110 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg) val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg)
if (null == decryptor) { if (null == decryptor) {
Timber.w("## processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId") Timber.w("## processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return return
} }
if (!decryptor.hasKeysForKeyRequest(request)) { if (!decryptor.hasKeysForKeyRequest(request)) {
Timber.w("## processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}") Timber.w("## processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}")
cryptoStore.deleteIncomingRoomKeyRequest(request) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return return
} }
if (credentials.deviceId == deviceId && credentials.userId == userId) { if (credentials.deviceId == deviceId && credentials.userId == userId) {
Timber.v("## processReceivedGossipingRequests() : oneself device - ignored") Timber.v("## processReceivedGossipingRequests() : oneself device - ignored")
cryptoStore.deleteIncomingRoomKeyRequest(request) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return return
} }
request.share = Runnable { request.share = Runnable {
decryptor.shareKeysWithDevice(request) decryptor.shareKeysWithDevice(request)
cryptoStore.deleteIncomingRoomKeyRequest(request) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
} }
request.ignore = Runnable { request.ignore = Runnable {
cryptoStore.deleteIncomingRoomKeyRequest(request) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
} }
// if the device is verified already, share the keys // if the device is verified already, share the keys
val device = cryptoStore.getUserDevice(userId, deviceId!!) val device = cryptoStore.getUserDevice(userId, deviceId!!)
if (device != null) { if (device != null) {
if (device.isVerified) { if (device.isVerified) {
Timber.v("## processReceivedGossipingRequests() : device is already verified: sharing keys") Timber.v("## processReceivedGossipingRequests() : device is already verified: sharing keys")
cryptoStore.deleteIncomingRoomKeyRequest(request)
request.share?.run() request.share?.run()
return return
} }
if (device.isBlocked) { if (device.isBlocked) {
Timber.v("## processReceivedGossipingRequests() : device is blocked -> ignored") Timber.v("## processReceivedGossipingRequests() : device is blocked -> ignored")
cryptoStore.deleteIncomingRoomKeyRequest(request) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return return
} }
} }
// If cross signing is available on account we automatically discard untrust devices request // As per config we automatically discard untrusted devices request
if (cryptoStore.getMyCrossSigningInfo() != null) { if (cryptoConfig.discardRoomKeyRequestsFromUntrustedDevices) {
Timber.v("## processReceivedGossipingRequests() : discardRoomKeyRequestsFromUntrustedDevices")
// At this point the device is unknown, we don't want to bother user with that // At this point the device is unknown, we don't want to bother user with that
cryptoStore.deleteIncomingRoomKeyRequest(request) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return return
} }
cryptoStore.storeIncomingRoomKeyRequest(request) // Pass to application layer to decide what to do
// Legacy, pass to application layer to decide what to do
onRoomKeyRequest(request) onRoomKeyRequest(request)
} }
private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) { private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) {
val secretName = request.secretName ?: return Unit.also { val secretName = request.secretName ?: return Unit.also {
cryptoStore.deleteIncomingSecretRequest(request) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
Timber.v("## processIncomingSecretShareRequest() : Missing secret name") Timber.v("## processIncomingSecretShareRequest() : Missing secret name")
} }
val userId = request.userId val userId = request.userId
if (userId == null || credentials.userId != userId) { if (userId == null || credentials.userId != userId) {
Timber.e("## processIncomingSecretShareRequest() : Ignoring secret share request from other user") Timber.e("## processIncomingSecretShareRequest() : Ignoring secret share request from other users")
cryptoStore.deleteIncomingRoomKeyRequest(request) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return return
} }
val deviceId = request.deviceId
?: return Unit.also {
Timber.e("## processIncomingSecretShareRequest() : Malformed request, no ")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
}
val device = cryptoStore.getUserDevice(userId, deviceId)
?: return Unit.also {
Timber.e("## processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
}
if (!device.isVerified || device.isBlocked) {
Timber.v("## processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
val isDeviceLocallyVerified = cryptoStore.getUserDevice(userId, deviceId)?.trustLevel?.isLocallyVerified()
//Should SDK always Silently reject any request for the master key?
when (secretName) { when (secretName) {
SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned
USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user
else -> null else -> null
}?.let { secretValue -> }?.let { secretValue ->
// TODO check if locally trusted and not outdated // TODO check if locally trusted and not outdated
if (cryptoStore.getUserDevice(userId, request.deviceId ?: "")?.trustLevel?.isLocallyVerified() == true) { Timber.i("## processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted")
secrSecretCryptoProvider.shareSecretWithDevice(request, secretValue) if (isDeviceLocallyVerified == true) {
cryptoStore.deleteIncomingRoomKeyRequest(request) secretSecretCryptoProvider.shareSecretWithDevice(request, secretValue)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
} }
return return
} }
request.ignore = Runnable { request.ignore = Runnable {
cryptoStore.deleteIncomingRoomKeyRequest(request) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
} }
request.share = { secretValue -> request.share = { secretValue ->
secrSecretCryptoProvider.shareSecretWithDevice(request, secretValue) secretSecretCryptoProvider.shareSecretWithDevice(request, secretValue)
cryptoStore.deleteIncomingRoomKeyRequest(request) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
} }
onShareRequest(request) onShareRequest(request)

View File

@ -54,7 +54,9 @@ data class IncomingSecretShareRequest(
* The runnable to call to ignore the key share request. * The runnable to call to ignore the key share request.
*/ */
@Transient @Transient
var ignore: Runnable? = null var ignore: Runnable? = null,
override val localCreationTimestamp: Long?
) : IncomingShareRequestCommon { ) : IncomingShareRequestCommon {
companion object { companion object {
@ -71,7 +73,8 @@ data class IncomingSecretShareRequest(
userId = event.senderId, userId = event.senderId,
deviceId = it.requestingDeviceId, deviceId = it.requestingDeviceId,
requestId = it.requestId, requestId = it.requestId,
secretName = it.secretName secretName = it.secretName,
localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis()
) )
} }
} }

View File

@ -31,4 +31,6 @@ interface IncomingShareRequestCommon {
* The request id * The request id
*/ */
val requestId: String? val requestId: String?
val localCreationTimestamp: Long?
} }

View File

@ -16,10 +16,10 @@
package im.vector.matrix.android.internal.crypto package im.vector.matrix.android.internal.crypto
interface OutgoingShareRequest { interface OutgoingGossipingRequest {
var recipients: List<Map<String, String>> var recipients: Map<String, List<String>>
var requestId: String var requestId: String
var state: ShareRequestState var state: OutgoingGossipingRequestState
// transaction id for the cancellation, if any // transaction id for the cancellation, if any
var cancellationTxnId: String? //var cancellationTxnId: String?
} }

View File

@ -0,0 +1,185 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2018 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
import androidx.work.BackoffPolicy
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.ListenableWorker
import androidx.work.OneTimeWorkRequest
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.WorkManagerProvider
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.util.CancelableWork
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import im.vector.matrix.android.internal.worker.startChain
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@SessionScope
internal class OutgoingGossipingRequestManager @Inject constructor(
@SessionId private val sessionId: String,
private val cryptoStore: IMXCryptoStore,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope,
private val workManagerProvider: WorkManagerProvider) {
/**
* Send off a room key request, if we haven't already done so.
*
*
* The `requestBody` is compared (with a deep-equality check) against
* previous queued or sent requests and if it matches, no change is made.
* Otherwise, a request is added to the pending list, and a job is started
* in the background to send it.
*
* @param requestBody requestBody
* @param recipients recipients
*/
fun sendRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients)?.let {
// Don't resend if it's already done, you need to cancel first (reRequest)
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
Timber.v("## sendOutgoingRoomKeyRequest() : we already request for that session: $it")
return@launch
}
sendOutgoingGossipingRequest(it)
}
}
}
fun sendSecretShareRequest(secretName: String, recipients: Map<String, List<String>>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let {
// TODO check if there is already one that is being sent?
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
Timber.v("## sendOutgoingRoomKeyRequest() : we already request for that session: $it")
return@launch
}
sendOutgoingGossipingRequest(it)
}
}
}
/**
* Cancel room key requests, if any match the given details
*
* @param requestBody requestBody
*/
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cancelRoomKeyRequest(requestBody, false)
}
}
/**
* Cancel room key requests, if any match the given details, and resend
*
* @param requestBody requestBody
*/
fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cancelRoomKeyRequest(requestBody, true)
}
}
/**
* Cancel room key requests, if any match the given details, and resend
*
* @param requestBody requestBody
* @param andResend true to resend the key request
*/
private fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody, andResend: Boolean) {
val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody)
?: // no request was made for this key
return Unit.also {
Timber.v("## cancelRoomKeyRequest() Unknown request")
}
sendOutgoingRoomKeyRequestCancellation(req, andResend)
}
/**
* Send the outgoing key request.
*
* @param request the request
*/
private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) {
Timber.v("## sendOutgoingRoomKeyRequest() : Requesting keys ${request}")
val params = SendGossipRequestWorker.Params(
sessionId = sessionId,
keyShareRequest = request as? OutgoingRoomKeyRequest,
secretShareRequest = request as? OutgoingSecretRequest
)
cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.SENDING)
val workRequest = createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
postWork(workRequest)
}
/**
* Given a OutgoingRoomKeyRequest, cancel it and delete the request record
*
* @param request the request
*/
private fun sendOutgoingRoomKeyRequestCancellation(request: OutgoingRoomKeyRequest, resend: Boolean = false) {
Timber.v("$request")
val params = CancelGossipRequestWorker.Params.fromRequest(sessionId, request)
cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.CANCELLING)
val workRequest = createWork<CancelGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
postWork(workRequest)
if (resend) {
val reSendParams = SendGossipRequestWorker.Params(
sessionId = sessionId,
keyShareRequest = request.copy(requestId = LocalEcho.createLocalEchoId())
)
val reSendWorkRequest = createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(reSendParams), true)
postWork(reSendWorkRequest)
}
}
private inline fun <reified W : ListenableWorker> createWork(data: Data, startChain: Boolean): OneTimeWorkRequest {
return workManagerProvider.matrixOneTimeWorkRequestBuilder<W>()
.setConstraints(WorkManagerProvider.workConstraints)
.startChain(startChain)
.setInputData(data)
.setBackoffCriteria(BackoffPolicy.LINEAR, 10_000L, TimeUnit.MILLISECONDS)
.build()
}
private fun postWork(workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable {
workManagerProvider.workManager
.beginUniqueWork(this::class.java.name, policy, workRequest)
.enqueue()
return CancelableWork(workManagerProvider.workManager, workRequest.id)
}
}

View File

@ -17,22 +17,27 @@
package im.vector.matrix.android.internal.crypto package im.vector.matrix.android.internal.crypto
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
/** /**
* Represents an outgoing room key request * Represents an outgoing room key request
*/ */
class OutgoingRoomKeyRequest( @JsonClass(generateAdapter = true)
data class OutgoingRoomKeyRequest(
// RequestBody // RequestBody
var requestBody: RoomKeyRequestBody?, // list of recipients for the request var requestBody: RoomKeyRequestBody?,
override var recipients: List<Map<String, String>>, // Unique id for this request. Used for both // list of recipients for the request
override var recipients: Map<String, List<String>>,
// Unique id for this request. Used for both
// an id within the request for later pairing with a cancellation, and for // an id within the request for later pairing with a cancellation, and for
// the transaction id when sending the to_device messages to our local // the transaction id when sending the to_device messages to our local
override var requestId: String, // current state of this request override var requestId: String, // current state of this request
override var state: ShareRequestState) : OutgoingShareRequest { override var state: OutgoingGossipingRequestState
// transaction id for the cancellation, if any // transaction id for the cancellation, if any
override var cancellationTxnId: String? = null // override var cancellationTxnId: String? = null
) : OutgoingGossipingRequest {
/** /**
* Used only for log. * Used only for log.

View File

@ -1,320 +0,0 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2018 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
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.model.rest.ShareRequestCancellation
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareRequest
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.createBackgroundHandler
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
@SessionScope
internal class OutgoingRoomKeyRequestManager @Inject constructor(
private val cryptoStore: IMXCryptoStore,
private val sendToDeviceTask: SendToDeviceTask,
private val taskExecutor: TaskExecutor) {
// running
private var isClientRunning: Boolean = false
// transaction counter
private var txnCtr: Int = 0
// sanity check to ensure that we don't end up with two concurrent runs
// of sendOutgoingRoomKeyRequestsTimer
private val sendOutgoingRoomKeyRequestsRunning = AtomicBoolean(false)
/**
* Called when the client is started. Sets background processes running.
*/
fun start() {
isClientRunning = true
startTimer()
}
/**
* Called when the client is stopped. Stops any running background processes.
*/
fun stop() {
isClientRunning = false
stopTimer()
}
/**
* Make up a new transaction id
*
* @return {string} a new, unique, transaction id
*/
private fun makeTxnId(): String {
return "m" + System.currentTimeMillis() + "." + txnCtr++
}
/**
* Send off a room key request, if we haven't already done so.
*
*
* The `requestBody` is compared (with a deep-equality check) against
* previous queued or sent requests and if it matches, no change is made.
* Otherwise, a request is added to the pending list, and a job is started
* in the background to send it.
*
* @param requestBody requestBody
* @param recipients recipients
*/
fun sendRoomKeyRequest(requestBody: RoomKeyRequestBody?, recipients: List<Map<String, String>>) {
val req = cryptoStore.getOrAddOutgoingRoomKeyRequest(
OutgoingRoomKeyRequest(requestBody, recipients, makeTxnId(), ShareRequestState.UNSENT))
if (req?.state == ShareRequestState.UNSENT) {
startTimer()
}
}
/**
* Cancel room key requests, if any match the given details
*
* @param requestBody requestBody
*/
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
BACKGROUND_HANDLER.post {
cancelRoomKeyRequest(requestBody, false)
}
}
/**
* Cancel room key requests, if any match the given details, and resend
*
* @param requestBody requestBody
*/
fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) {
BACKGROUND_HANDLER.post {
cancelRoomKeyRequest(requestBody, true)
}
}
/**
* Cancel room key requests, if any match the given details, and resend
*
* @param requestBody requestBody
* @param andResend true to resend the key request
*/
private fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody, andResend: Boolean) {
val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody)
?: // no request was made for this key
return
Timber.v("cancelRoomKeyRequest: requestId: " + req.requestId + " state: " + req.state + " andResend: " + andResend)
when (req.state) {
ShareRequestState.CANCELLATION_PENDING,
ShareRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> {
// nothing to do here
}
ShareRequestState.UNSENT,
ShareRequestState.FAILED -> {
Timber.v("## cancelRoomKeyRequest() : deleting unnecessary room key request for $requestBody")
cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId)
}
ShareRequestState.SENT -> {
if (andResend) {
req.state = ShareRequestState.CANCELLATION_PENDING_AND_WILL_RESEND
} else {
req.state = ShareRequestState.CANCELLATION_PENDING
}
req.cancellationTxnId = makeTxnId()
cryptoStore.updateOutgoingRoomKeyRequest(req)
sendOutgoingRoomKeyRequestCancellation(req)
}
}
}
/**
* Start the background timer to send queued requests, if the timer isn't already running.
*/
private fun startTimer() {
if (sendOutgoingRoomKeyRequestsRunning.get()) {
return
}
BACKGROUND_HANDLER.postDelayed(Runnable {
if (sendOutgoingRoomKeyRequestsRunning.get()) {
Timber.v("## startTimer() : RoomKeyRequestSend already in progress!")
return@Runnable
}
sendOutgoingRoomKeyRequestsRunning.set(true)
sendOutgoingRoomKeyRequests()
}, SEND_KEY_REQUESTS_DELAY_MS.toLong())
}
private fun stopTimer() {
BACKGROUND_HANDLER.removeCallbacksAndMessages(null)
}
// look for and send any queued requests. Runs itself recursively until
// there are no more requests, or there is an error (in which case, the
// timer will be restarted before the promise resolves).
private fun sendOutgoingRoomKeyRequests() {
if (!isClientRunning) {
sendOutgoingRoomKeyRequestsRunning.set(false)
return
}
Timber.v("## sendOutgoingRoomKeyRequests() : Looking for queued outgoing room key requests")
val outgoingRoomKeyRequest = cryptoStore.getOutgoingRoomKeyRequestByState(
setOf(ShareRequestState.UNSENT,
ShareRequestState.CANCELLATION_PENDING,
ShareRequestState.CANCELLATION_PENDING_AND_WILL_RESEND))
if (null == outgoingRoomKeyRequest) {
Timber.v("## sendOutgoingRoomKeyRequests() : No more outgoing room key requests")
sendOutgoingRoomKeyRequestsRunning.set(false)
return
}
if (ShareRequestState.UNSENT === outgoingRoomKeyRequest.state) {
sendOutgoingRoomKeyRequest(outgoingRoomKeyRequest)
} else {
sendOutgoingRoomKeyRequestCancellation(outgoingRoomKeyRequest)
}
}
/**
* Send the outgoing key request.
*
* @param request the request
*/
private fun sendOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) {
Timber.v("## sendOutgoingRoomKeyRequest() : Requesting keys " + request.requestBody
+ " from " + request.recipients + " id " + request.requestId)
val requestMessage = RoomKeyShareRequest(
requestingDeviceId = cryptoStore.getDeviceId(),
requestId = request.requestId,
body = request.requestBody
)
sendMessageToDevices(requestMessage, request.recipients, request.requestId, object : MatrixCallback<Unit> {
private fun onDone(state: ShareRequestState) {
if (request.state !== ShareRequestState.UNSENT) {
Timber.v("## sendOutgoingRoomKeyRequest() : Cannot update room key request from UNSENT as it was already updated to ${request.state}")
} else {
request.state = state
cryptoStore.updateOutgoingRoomKeyRequest(request)
}
sendOutgoingRoomKeyRequestsRunning.set(false)
startTimer()
}
override fun onSuccess(data: Unit) {
Timber.v("## sendOutgoingRoomKeyRequest succeed")
onDone(ShareRequestState.SENT)
}
override fun onFailure(failure: Throwable) {
Timber.e("## sendOutgoingRoomKeyRequest failed")
onDone(ShareRequestState.FAILED)
}
})
}
/**
* Given a OutgoingRoomKeyRequest, cancel it and delete the request record
*
* @param request the request
*/
private fun sendOutgoingRoomKeyRequestCancellation(request: OutgoingRoomKeyRequest) {
Timber.v("## sendOutgoingRoomKeyRequestCancellation() : Sending cancellation for key request for " + request.requestBody
+ " to " + request.recipients
+ " cancellation id " + request.cancellationTxnId)
val roomKeyShareCancellation = ShareRequestCancellation(
requestingDeviceId = cryptoStore.getDeviceId(),
requestId = request.cancellationTxnId
)
sendMessageToDevices(roomKeyShareCancellation, request.recipients, request.cancellationTxnId, object : MatrixCallback<Unit> {
private fun onDone() {
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
sendOutgoingRoomKeyRequestsRunning.set(false)
startTimer()
}
override fun onSuccess(data: Unit) {
Timber.v("## sendOutgoingRoomKeyRequestCancellation() : done")
val resend = request.state === ShareRequestState.CANCELLATION_PENDING_AND_WILL_RESEND
onDone()
// Resend the request with a new ID
if (resend) {
sendRoomKeyRequest(request.requestBody, request.recipients)
}
}
override fun onFailure(failure: Throwable) {
Timber.e("## sendOutgoingRoomKeyRequestCancellation failed")
onDone()
}
})
}
/**
* Send a SendToDeviceObject to a list of recipients
*
* @param message the message
* @param recipients the recipients.
* @param transactionId the transaction id
* @param callback the asynchronous callback.
*/
private fun sendMessageToDevices(message: Any,
recipients: List<Map<String, String>>,
transactionId: String?,
callback: MatrixCallback<Unit>) {
val contentMap = MXUsersDevicesMap<Any>()
for (recipient in recipients) {
// TODO Change this two hard coded key to something better
contentMap.setObject(recipient["userId"], recipient["deviceId"], message)
}
sendToDeviceTask
.configureWith(SendToDeviceTask.Params(EventType.ROOM_KEY_REQUEST, contentMap, transactionId)) {
this.callback = callback
this.callbackThread = TaskThread.CALLER
this.executionThread = TaskThread.CALLER
}
.executeBy(taskExecutor)
}
companion object {
private const val SEND_KEY_REQUESTS_DELAY_MS = 500
private val BACKGROUND_HANDLER = createBackgroundHandler("OutgoingRoomKeyRequest")
}
}

View File

@ -16,21 +16,23 @@
package im.vector.matrix.android.internal.crypto package im.vector.matrix.android.internal.crypto
import com.squareup.moshi.JsonClass
/** /**
* Represents an outgoing room key request * Represents an outgoing room key request
*/ */
@JsonClass(generateAdapter = true)
class OutgoingSecretRequest( class OutgoingSecretRequest(
// Secret Name // Secret Name
var secretName: String?, val secretName: String?,
// list of recipients for the request // list of recipients for the request
override var recipients: List<Map<String, String>>, override var recipients: Map<String, List<String>>,
// Unique id for this request. Used for both // Unique id for this request. Used for both
// an id within the request for later pairing with a cancellation, and for // an id within the request for later pairing with a cancellation, and for
// the transaction id when sending the to_device messages to our local // the transaction id when sending the to_device messages to our local
override var requestId: String, override var requestId: String,
// current state of this request // current state of this request
override var state: ShareRequestState) : OutgoingShareRequest { override var state: OutgoingGossipingRequestState) : OutgoingGossipingRequest {
// transaction id for the cancellation, if any // transaction id for the cancellation, if any
override var cancellationTxnId: String? = null
} }

View File

@ -0,0 +1,150 @@
/*
* 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
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.failure.shouldBeRetried
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.LocalEcho
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObject
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareRequest
import im.vector.matrix.android.internal.crypto.model.rest.SecretShareRequest
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import im.vector.matrix.android.internal.worker.getSessionComponent
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject
internal class SendGossipRequestWorker(context: Context,
params: WorkerParameters)
: CoroutineWorker(context, params) {
@JsonClass(generateAdapter = true)
internal data class Params(
val sessionId: String,
val keyShareRequest: OutgoingRoomKeyRequest? = null,
val secretShareRequest: OutgoingSecretRequest? = null
)
@Inject lateinit var sendToDeviceTask: SendToDeviceTask
@Inject lateinit var cryptoStore: IMXCryptoStore
@Inject lateinit var eventBus: EventBus
@Inject lateinit var credentials: Credentials
override suspend fun doWork(): Result {
val errorOutputData = Data.Builder().putBoolean("failed", true).build()
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.success(errorOutputData)
val sessionComponent = getSessionComponent(params.sessionId)
?: return Result.success(errorOutputData).also {
// TODO, can this happen? should I update local echo?
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
}
sessionComponent.inject(this)
val localId = LocalEcho.createLocalEchoId()
val contentMap = MXUsersDevicesMap<Any>()
val eventType: String
val requestId: String
when {
params.keyShareRequest != null -> {
eventType = EventType.ROOM_KEY_REQUEST
requestId = params.keyShareRequest.requestId
val toDeviceContent = RoomKeyShareRequest(
requestingDeviceId = credentials.deviceId,
requestId = params.keyShareRequest.requestId,
action = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
body = params.keyShareRequest.requestBody
)
cryptoStore.saveGossipingEvent(Event(
type = eventType,
content = toDeviceContent.toContent(),
senderId = credentials.userId
).also {
it.ageLocalTs = System.currentTimeMillis()
})
params.keyShareRequest.recipients.forEach { userToDeviceMap ->
userToDeviceMap.value.forEach { deviceId ->
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
}
}
}
params.secretShareRequest != null -> {
eventType = EventType.REQUEST_SECRET
requestId = params.secretShareRequest.requestId
val toDeviceContent = SecretShareRequest(
requestingDeviceId = credentials.deviceId,
requestId = params.secretShareRequest.requestId,
action = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
secretName = params.secretShareRequest.secretName
)
cryptoStore.saveGossipingEvent(Event(
type = eventType,
content = toDeviceContent.toContent(),
senderId = credentials.userId
).also {
it.ageLocalTs = System.currentTimeMillis()
})
params.secretShareRequest.recipients.forEach { userToDeviceMap ->
userToDeviceMap.value.forEach { deviceId ->
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
}
}
}
else -> {
return Result.success(errorOutputData).also {
Timber.e("Unknown empty gossiping request: ${params}")
}
}
}
try {
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENDING)
sendToDeviceTask.execute(
SendToDeviceTask.Params(
eventType = eventType,
contentMap = contentMap,
transactionId = localId
)
)
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENT)
return Result.success()
} catch (exception: Throwable) {
return if (exception.shouldBeRetried()) {
Result.retry()
} else {
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.FAILED_TO_SEND)
Result.success(errorOutputData)
}
}
}
}

View File

@ -16,8 +16,10 @@
package im.vector.matrix.android.internal.crypto package im.vector.matrix.android.internal.crypto
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmDecryptionFactory
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
@ -25,6 +27,7 @@ import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -32,6 +35,7 @@ internal class ShareSecretCryptoProvider @Inject constructor(
val messageEncrypter: MessageEncrypter, val messageEncrypter: MessageEncrypter,
val sendToDeviceTask: SendToDeviceTask, val sendToDeviceTask: SendToDeviceTask,
val deviceListManager: DeviceListManager, val deviceListManager: DeviceListManager,
private val olmDecryptionFactory: MXOlmDecryptionFactory,
val cryptoCoroutineScope: CoroutineScope, val cryptoCoroutineScope: CoroutineScope,
val cryptoStore: IMXCryptoStore, val cryptoStore: IMXCryptoStore,
val coroutineDispatchers: MatrixCoroutineDispatchers val coroutineDispatchers: MatrixCoroutineDispatchers
@ -61,4 +65,10 @@ internal class ShareSecretCryptoProvider @Inject constructor(
} }
} }
} }
fun decryptEvent(event: Event): MXEventDecryptionResult {
return runBlocking(coroutineDispatchers.crypto) {
olmDecryptionFactory.create().decryptEvent(event, ShareSecretCryptoProvider::class.java.name ?: "")
}
}
} }

View File

@ -20,7 +20,7 @@ import androidx.annotation.WorkerThread
import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.MegolmSessionData import im.vector.matrix.android.internal.crypto.MegolmSessionData
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.RoomDecryptorProvider import im.vector.matrix.android.internal.crypto.RoomDecryptorProvider
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
@ -30,7 +30,7 @@ import javax.inject.Inject
internal class MegolmSessionDataImporter @Inject constructor(private val olmDevice: MXOlmDevice, internal class MegolmSessionDataImporter @Inject constructor(private val olmDevice: MXOlmDevice,
private val roomDecryptorProvider: RoomDecryptorProvider, private val roomDecryptorProvider: RoomDecryptorProvider,
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
private val cryptoStore: IMXCryptoStore) { private val cryptoStore: IMXCryptoStore) {
/** /**
@ -73,7 +73,7 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi
sessionId = megolmSessionData.sessionId sessionId = megolmSessionData.sessionId
) )
outgoingRoomKeyRequestManager.cancelRoomKeyRequest(roomKeyRequestBody) outgoingGossipingRequestManager.cancelRoomKeyRequest(roomKeyRequestBody)
// Have another go at decrypting events sent with this session // Have another go at decrypting events sent with this session
decrypting.onNewSession(megolmSessionData.senderKey!!, sessionId!!) decrypting.onNewSession(megolmSessionData.senderKey!!, sessionId!!)

View File

@ -68,4 +68,6 @@ internal interface IMXDecrypting {
fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {} fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {}
fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue : String) {} fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue : String) {}
fun requestKeysForEvent(event: Event)
} }

View File

@ -26,7 +26,7 @@ import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.NewSessionListener import im.vector.matrix.android.internal.crypto.NewSessionListener
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting
@ -46,7 +46,7 @@ import timber.log.Timber
internal class MXMegolmDecryption(private val userId: String, internal class MXMegolmDecryption(private val userId: String,
private val olmDevice: MXOlmDevice, private val olmDevice: MXOlmDevice,
private val deviceListManager: DeviceListManager, private val deviceListManager: DeviceListManager,
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
private val messageEncrypter: MessageEncrypter, private val messageEncrypter: MessageEncrypter,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
@ -144,25 +144,26 @@ internal class MXMegolmDecryption(private val userId: String,
* *
* @param event the event * @param event the event
*/ */
private fun requestKeysForEvent(event: Event) { override fun requestKeysForEvent(event: Event) {
val sender = event.senderId!! val sender = event.senderId ?: return
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()!! val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
val senderDevice = encryptedEventContent?.deviceId ?: return
val recipients = ArrayList<Map<String, String>>() val recipients = if (event.senderId != userId) {
mapOf(
val selfMap = HashMap<String, String>() userId to listOf("*")
// TODO Replace this hard coded keys (see OutgoingRoomKeyRequestManager) )
selfMap["userId"] = userId } else {
selfMap["deviceId"] = "*" // for the case where you share the key with a device that has a broken olm session
recipients.add(selfMap) // The other user might Re-shares a megolm session key with devices if the key has already been
// sent to them.
if (sender != userId) { mapOf(
val senderMap = HashMap<String, String>() userId to listOf("*"),
senderMap["userId"] = sender sender to listOf(senderDevice)
senderMap["deviceId"] = encryptedEventContent.deviceId!! )
recipients.add(senderMap)
} }
val requestBody = RoomKeyRequestBody( val requestBody = RoomKeyRequestBody(
roomId = event.roomId, roomId = event.roomId,
algorithm = encryptedEventContent.algorithm, algorithm = encryptedEventContent.algorithm,
@ -170,7 +171,7 @@ internal class MXMegolmDecryption(private val userId: String,
sessionId = encryptedEventContent.sessionId sessionId = encryptedEventContent.sessionId
) )
outgoingRoomKeyRequestManager.sendRoomKeyRequest(requestBody, recipients) outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients)
} }
/** /**
@ -271,7 +272,7 @@ internal class MXMegolmDecryption(private val userId: String,
senderKey = senderKey senderKey = senderKey
) )
outgoingRoomKeyRequestManager.cancelRoomKeyRequest(content) outgoingGossipingRequestManager.cancelRoomKeyRequest(content)
onNewSession(senderKey, roomKeyContent.sessionId) onNewSession(senderKey, roomKeyContent.sessionId)
} }

View File

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.crypto.algorithms.megolm
import im.vector.matrix.android.internal.crypto.DeviceListManager import im.vector.matrix.android.internal.crypto.DeviceListManager
import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
@ -32,7 +32,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
@UserId private val userId: String, @UserId private val userId: String,
private val olmDevice: MXOlmDevice, private val olmDevice: MXOlmDevice,
private val deviceListManager: DeviceListManager, private val deviceListManager: DeviceListManager,
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
private val messageEncrypter: MessageEncrypter, private val messageEncrypter: MessageEncrypter,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
@ -46,7 +46,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
userId, userId,
olmDevice, olmDevice,
deviceListManager, deviceListManager,
outgoingRoomKeyRequestManager, outgoingGossipingRequestManager,
messageEncrypter, messageEncrypter,
ensureOlmSessionsForDevicesAction, ensureOlmSessionsForDevicesAction,
cryptoStore, cryptoStore,

View File

@ -210,4 +210,8 @@ internal class MXOlmDecryption(
return res["payload"] return res["payload"]
} }
override fun requestKeysForEvent(event: Event) {
// nop
}
} }

View File

@ -25,7 +25,7 @@ import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.crypto.DeviceListManager import im.vector.matrix.android.internal.crypto.DeviceListManager
import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
import im.vector.matrix.android.internal.crypto.model.KeyUsage import im.vector.matrix.android.internal.crypto.model.KeyUsage
import im.vector.matrix.android.internal.crypto.model.rest.UploadSignatureQueryBuilder import im.vector.matrix.android.internal.crypto.model.rest.UploadSignatureQueryBuilder
@ -43,6 +43,7 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.withoutPrefix import im.vector.matrix.android.internal.util.withoutPrefix
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.matrix.olm.OlmPkSigning import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility import org.matrix.olm.OlmUtility
@ -62,7 +63,7 @@ internal class DefaultCrossSigningService @Inject constructor(
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope, private val cryptoCoroutineScope: CoroutineScope,
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener { private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
private var olmUtility: OlmUtility? = null private var olmUtility: OlmUtility? = null
@ -299,6 +300,58 @@ internal class DefaultCrossSigningService @Inject constructor(
cryptoStore.clearOtherUserTrust() cryptoStore.clearOtherUserTrust()
} }
override fun onSecretSSKGossip(sskPrivateKey: String): Boolean {
Timber.i("## CrossSigning - onSecretSSKGossip")
return runBlocking(coroutineDispatchers.crypto) {
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return@runBlocking false
sskPrivateKey.fromBase64NoPadding()
.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
selfSigningPkSigning?.releaseSigning()
selfSigningPkSigning = pkSigning
Timber.i("## CrossSigning - Loading SSK success")
cryptoStore.storePrivateKeysInfo(null, null, sskPrivateKey)
return@runBlocking true
} else {
pkSigning.releaseSigning()
}
} catch (failure: Throwable) {
pkSigning.releaseSigning()
}
}
return@runBlocking false
}
}
override fun onSecretUSKGossip(uskPrivateKey: String): Boolean {
Timber.i("## CrossSigning - onSecretUSKGossip")
return runBlocking(coroutineDispatchers.crypto) {
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return@runBlocking false
uskPrivateKey.fromBase64NoPadding()
.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
userPkSigning?.releaseSigning()
userPkSigning = pkSigning
Timber.i("## CrossSigning - Loading USK success")
cryptoStore.storePrivateKeysInfo(null, uskPrivateKey, null)
return@runBlocking true
} else {
pkSigning.releaseSigning()
}
} catch (failure: Throwable) {
pkSigning.releaseSigning()
}
}
return@runBlocking false
}
}
override fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?, override fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?,
uskKeyPrivateKey: String?, uskKeyPrivateKey: String?,
sskPrivateKey: String? sskPrivateKey: String?
@ -549,22 +602,23 @@ internal class DefaultCrossSigningService @Inject constructor(
} }
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) { override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
Timber.d("## CrossSigning - Mark user $userId as trusted ") Timber.d("## CrossSigning - Mark user $userId as trusted ")
// We should have this user keys // We should have this user keys
val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey() val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey()
if (otherMasterKeys == null) { if (otherMasterKeys == null) {
callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known")) callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known"))
return return@launch
} }
val myKeys = getUserCrossSigningKeys(userId) val myKeys = getUserCrossSigningKeys(userId)
if (myKeys == null) { if (myKeys == null) {
callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account")) callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account"))
return return@launch
} }
val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey
if (userPubKey == null || userPkSigning == null) { if (userPubKey == null || userPkSigning == null) {
callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey")) callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey"))
return return@launch
} }
// Sign the other MasterKey with our UserSigning key // Sign the other MasterKey with our UserSigning key
@ -574,7 +628,7 @@ internal class DefaultCrossSigningService @Inject constructor(
if (newSignature == null) { if (newSignature == null) {
// race?? // race??
callback.onFailure(Throwable("## CrossSigning - Failed to sign")) callback.onFailure(Throwable("## CrossSigning - Failed to sign"))
return return@launch
} }
cryptoStore.setUserKeysAsTrusted(otherUserId, true) cryptoStore.setUserKeysAsTrusted(otherUserId, true)
@ -589,30 +643,34 @@ internal class DefaultCrossSigningService @Inject constructor(
this.callback = callback this.callback = callback
}.executeBy(taskExecutor) }.executeBy(taskExecutor)
} }
}
override fun markMyMasterKeyAsTrusted() { override fun markMyMasterKeyAsTrusted() {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoStore.markMyMasterKeyAsLocallyTrusted(true) cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
checkSelfTrust() checkSelfTrust()
} }
}
override fun trustDevice(deviceId: String, callback: MatrixCallback<Unit>) { override fun trustDevice(deviceId: String, callback: MatrixCallback<Unit>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
// This device should be yours // This device should be yours
val device = cryptoStore.getUserDevice(userId, deviceId) val device = cryptoStore.getUserDevice(userId, deviceId)
if (device == null) { if (device == null) {
callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours")) callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours"))
return return@launch
} }
val myKeys = getUserCrossSigningKeys(userId) val myKeys = getUserCrossSigningKeys(userId)
if (myKeys == null) { if (myKeys == null) {
callback.onFailure(Throwable("CrossSigning is not setup for this account")) callback.onFailure(Throwable("CrossSigning is not setup for this account"))
return return@launch
} }
val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey
if (ssPubKey == null || selfSigningPkSigning == null) { if (ssPubKey == null || selfSigningPkSigning == null) {
callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey")) callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey"))
return return@launch
} }
// Sign with self signing // Sign with self signing
@ -621,7 +679,7 @@ internal class DefaultCrossSigningService @Inject constructor(
if (newSignature == null) { if (newSignature == null) {
// race?? // race??
callback.onFailure(Throwable("Failed to sign")) callback.onFailure(Throwable("Failed to sign"))
return return@launch
} }
val toUpload = device.copy( val toUpload = device.copy(
signatures = mapOf( signatures = mapOf(
@ -641,6 +699,7 @@ internal class DefaultCrossSigningService @Inject constructor(
this.callback = callback this.callback = callback
}.executeBy(taskExecutor) }.executeBy(taskExecutor)
} }
}
override fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult { override fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult {
val otherDevice = cryptoStore.getUserDevice(otherUserId, otherDeviceId) val otherDevice = cryptoStore.getUserDevice(otherUserId, otherDeviceId)
@ -738,7 +797,7 @@ internal class DefaultCrossSigningService @Inject constructor(
// If it's me, recheck trust of all users and devices? // If it's me, recheck trust of all users and devices?
val users = ArrayList<String>() val users = ArrayList<String>()
if (otherUserId == userId && currentTrust != trusted) { if (otherUserId == userId && currentTrust != trusted) {
reRequestAllPendingRoomKeyRequest() // reRequestAllPendingRoomKeyRequest()
cryptoStore.updateUsersTrust { cryptoStore.updateUsersTrust {
users.add(it) users.add(it)
checkUserTrust(it).isVerified() checkUserTrust(it).isVerified()
@ -755,16 +814,18 @@ internal class DefaultCrossSigningService @Inject constructor(
} }
} }
private fun reRequestAllPendingRoomKeyRequest() { // private fun reRequestAllPendingRoomKeyRequest() {
Timber.d("## CrossSigning - reRequest pending outgoing room key requests") // cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoStore.getOutgoingRoomKeyRequests().forEach { // Timber.d("## CrossSigning - reRequest pending outgoing room key requests")
it.requestBody?.let { requestBody -> // cryptoStore.getOutgoingRoomKeyRequests().forEach {
if (cryptoStore.getInboundGroupSession(requestBody.sessionId ?: "", requestBody.senderKey ?: "") == null) { // it.requestBody?.let { requestBody ->
outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody) // if (cryptoStore.getInboundGroupSession(requestBody.sessionId ?: "", requestBody.senderKey ?: "") == null) {
} else { // outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody)
outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody) // } else {
} // outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody)
} // }
} // }
} // }
// }
// }
} }

View File

@ -33,7 +33,7 @@ data class CryptoDeviceInfo(
) : CryptoInfo { ) : CryptoInfo {
val isVerified: Boolean val isVerified: Boolean
get() = trustLevel?.isVerified() ?: false get() = trustLevel?.isVerified() == true
val isUnknown: Boolean val isUnknown: Boolean
get() = trustLevel == null get() = trustLevel == null

View File

@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass
* Interface representing an room key action request * Interface representing an room key action request
* Note: this class cannot be abstract because of [org.matrix.androidsdk.core.JsonUtils.toRoomKeyShare] * Note: this class cannot be abstract because of [org.matrix.androidsdk.core.JsonUtils.toRoomKeyShare]
*/ */
internal interface GossipingToDeviceObject : SendToDeviceObject { interface GossipingToDeviceObject : SendToDeviceObject {
val action: String? val action: String?

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.di.MoshiProvider
/** /**
* Class representing an room key request body content * Class representing an room key request body content
@ -35,4 +36,16 @@ data class RoomKeyRequestBody(
@Json(name = "session_id") @Json(name = "session_id")
val sessionId: String? = null val sessionId: String? = null
) ) {
fun toJson(): String {
return MoshiProvider.providesMoshi().adapter(RoomKeyRequestBody::class.java).toJson(this)
}
companion object {
fun fromJson(json: String?): RoomKeyRequestBody? {
return json?.let { MoshiProvider.providesMoshi().adapter(RoomKeyRequestBody::class.java).fromJson(it) }
}
}
}

View File

@ -23,7 +23,7 @@ import com.squareup.moshi.JsonClass
* Class representing a room key request content * Class representing a room key request content
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class RoomKeyShareRequest( data class RoomKeyShareRequest(
@Json(name = "action") @Json(name = "action")
override val action: String? = GossipingToDeviceObject.ACTION_SHARE_REQUEST, override val action: String? = GossipingToDeviceObject.ACTION_SHARE_REQUEST,

View File

@ -33,6 +33,7 @@ import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageSer
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
import im.vector.matrix.android.api.session.securestorage.SsssKeySpec import im.vector.matrix.android.api.session.securestorage.SsssKeySpec
import im.vector.matrix.android.api.session.securestorage.SsssPassphrase import im.vector.matrix.android.api.session.securestorage.SsssPassphrase
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2 import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2 import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64 import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
@ -41,6 +42,7 @@ import im.vector.matrix.android.internal.crypto.keysbackup.generatePrivateKeyWit
import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey
import im.vector.matrix.android.internal.crypto.tools.HkdfSha256 import im.vector.matrix.android.internal.crypto.tools.HkdfSha256
import im.vector.matrix.android.internal.crypto.tools.withOlmDecryption import im.vector.matrix.android.internal.crypto.tools.withOlmDecryption
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -55,7 +57,9 @@ import javax.inject.Inject
import kotlin.experimental.and import kotlin.experimental.and
internal class DefaultSharedSecretStorageService @Inject constructor( internal class DefaultSharedSecretStorageService @Inject constructor(
@UserId private val userId: String,
private val accountDataService: AccountDataService, private val accountDataService: AccountDataService,
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope private val cryptoCoroutineScope: CoroutineScope
) : SharedSecretStorageService { ) : SharedSecretStorageService {
@ -429,4 +433,11 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
return IntegrityResult.Success(keyInfo.content.passphrase != null) return IntegrityResult.Success(keyInfo.content.passphrase != null)
} }
override fun requestSecret(name: String, myOtherDeviceId: String) {
outgoingGossipingRequestManager.sendSecretShareRequest(
name,
mapOf(userId to listOf(myOtherDeviceId))
)
}
} }

View File

@ -19,13 +19,15 @@ package im.vector.matrix.android.internal.crypto.store
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.crypto.IncomingShareRequestCommon import im.vector.matrix.android.internal.crypto.GossipingRequestState
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest import im.vector.matrix.android.internal.crypto.IncomingShareRequestCommon
import im.vector.matrix.android.internal.crypto.NewSessionListener import im.vector.matrix.android.internal.crypto.NewSessionListener
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.ShareRequestState 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.CryptoCrossSigningKey
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
@ -118,7 +120,10 @@ internal interface IMXCryptoStore {
* @return the pending IncomingRoomKeyRequest requests * @return the pending IncomingRoomKeyRequest requests
*/ */
fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest>
fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon>
fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?)
// fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest>
/** /**
* Indicate if the store contains data for the passed account. * Indicate if the store contains data for the passed account.
@ -209,6 +214,7 @@ internal interface IMXCryptoStore {
// TODO temp // TODO temp
fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>> fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>>
/** /**
* Store the crypto algorithm for a room. * Store the crypto algorithm for a room.
* *
@ -350,45 +356,49 @@ internal interface IMXCryptoStore {
* @param request the request * @param request the request
* @return either the same instance as passed in, or the existing one. * @return either the same instance as passed in, or the existing one.
*/ */
fun getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): OutgoingRoomKeyRequest? fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>): OutgoingRoomKeyRequest?
fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest?
fun saveGossipingEvent(event: Event)
/** /**
* Look for room key requests by state. * Look for room key requests by state.
* *
* @param states the states * @param states the states
* @return an OutgoingRoomKeyRequest or null * @return an OutgoingRoomKeyRequest or null
*/ */
fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest? // fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest?
// fun getOutgoingSecretShareRequestByState(states: Set<ShareRequestState>): OutgoingSecretRequest?
/** /**
* Update an existing outgoing request. * Update an existing outgoing request.
* *
* @param request the request * @param request the request
*/ */
fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) // fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest)
/** /**
* Delete an outgoing room key request. * Delete an outgoing room key request.
* *
* @param transactionId the transaction id. * @param transactionId the transaction id.
*/ */
fun deleteOutgoingRoomKeyRequest(transactionId: String) // fun deleteOutgoingRoomKeyRequest(transactionId: String)
/** /**
* Store an incomingRoomKeyRequest instance * Store an incomingRoomKeyRequest instance
* *
* @param incomingRoomKeyRequest the incoming key request * @param incomingRoomKeyRequest the incoming key request
*/ */
fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) // fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?)
/** /**
* Delete an incomingRoomKeyRequest instance * Delete an incomingRoomKeyRequest instance
* *
* @param incomingRoomKeyRequest the incoming key request * @param incomingRoomKeyRequest the incoming key request
*/ */
fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingShareRequestCommon) // fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingShareRequestCommon)
fun deleteIncomingSecretRequest(request: IncomingSecretShareRequest) fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState)
/** /**
* Search an IncomingRoomKeyRequest * Search an IncomingRoomKeyRequest
@ -400,6 +410,8 @@ internal interface IMXCryptoStore {
*/ */
fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest?
fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState)
fun addNewSessionListener(listener: NewSessionListener) fun addNewSessionListener(listener: NewSessionListener)
fun removeSessionListener(listener: NewSessionListener) fun removeSessionListener(listener: NewSessionListener)
@ -411,20 +423,21 @@ internal interface IMXCryptoStore {
/** /**
* Gets the current crosssigning info * Gets the current crosssigning info
*/ */
fun getMyCrossSigningInfo() : MXCrossSigningInfo? fun getMyCrossSigningInfo(): MXCrossSigningInfo?
fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) fun setMyCrossSigningInfo(info: MXCrossSigningInfo?)
fun getCrossSigningInfo(userId: String) : MXCrossSigningInfo? fun getCrossSigningInfo(userId: String): MXCrossSigningInfo?
fun getLiveCrossSigningInfo(userId: String) : LiveData<Optional<MXCrossSigningInfo>> fun getLiveCrossSigningInfo(userId: String): LiveData<Optional<MXCrossSigningInfo>>
fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?)
fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean)
fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?)
fun getCrossSigningPrivateKeys() : PrivateKeysInfo? fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true) fun setUserKeysAsTrusted(userId: String, trusted: Boolean = true)
fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified : Boolean) fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean)
fun clearOtherUserTrust() fun clearOtherUserTrust()
@ -432,5 +445,8 @@ internal interface IMXCryptoStore {
// Dev tools // Dev tools
fun getOutgoingRoomKeyRequests() : List<OutgoingRoomKeyRequest> fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
fun getGossipingEventsTrail(): List<Event>
} }

View File

@ -59,7 +59,12 @@ fun <T : RealmObject> doRealmQueryAndCopyList(realmConfiguration: RealmConfigura
*/ */
fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) { fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
Realm.getInstance(realmConfiguration).use { realm -> Realm.getInstance(realmConfiguration).use { realm ->
realm.executeTransaction { action.invoke(realm) } realm.executeTransaction { action.invoke(it) }
}
}
fun doRealmTransactionAsync(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
Realm.getInstance(realmConfiguration).use { realm ->
realm.executeTransactionAsync { action.invoke(it) }
} }
} }

View File

@ -21,14 +21,21 @@ import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.GossipRequestType
import im.vector.matrix.android.internal.crypto.GossipingRequestState
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
import im.vector.matrix.android.internal.crypto.IncomingShareRequestCommon import im.vector.matrix.android.internal.crypto.IncomingShareRequestCommon
import im.vector.matrix.android.internal.crypto.NewSessionListener import im.vector.matrix.android.internal.crypto.NewSessionListener
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.ShareRequestState 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.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
@ -46,18 +53,17 @@ import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingRoomKeyRequestEntity import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntity
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingRoomKeyRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntity
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingSecretRequestEntity import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingSecretRequestEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity 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.OlmSessionEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingRoomKeyRequestEntity import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingRoomKeyRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
@ -66,7 +72,9 @@ import im.vector.matrix.android.internal.crypto.store.db.query.delete
import im.vector.matrix.android.internal.crypto.store.db.query.get import im.vector.matrix.android.internal.crypto.store.db.query.get
import im.vector.matrix.android.internal.crypto.store.db.query.getById import im.vector.matrix.android.internal.crypto.store.db.query.getById
import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.di.CryptoDatabase import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
@ -801,152 +809,334 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? { override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? {
return doRealmQueryAndCopy(realmConfiguration) { return monarchy.fetchAllCopiedSync { realm ->
it.where<OutgoingRoomKeyRequestEntity>() realm.where<OutgoingGossipingRequestEntity>()
.equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_ALGORITHM, requestBody.algorithm) .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
.equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_ROOM_ID, requestBody.roomId) }.mapNotNull {
.equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_SENDER_KEY, requestBody.senderKey) it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
.equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_SESSION_ID, requestBody.sessionId) }.firstOrNull {
.findFirst() it.requestBody?.algorithm == requestBody.algorithm
it.requestBody?.roomId == requestBody.roomId
it.requestBody?.senderKey == requestBody.senderKey
it.requestBody?.sessionId == requestBody.sessionId
} }
?.toOutgoingRoomKeyRequest()
} }
override fun getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): OutgoingRoomKeyRequest? { override fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? {
if (request.requestBody == null) { // return monarchy.fetchAllCopiedSync { realm ->
return null //// realm.where<OutgoingGossipingRequestEntity>()
//// .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
//// .equalTo(GossipingEventEntityFields.SENDER, credentials.userId)
//// }.mapNotNull {
//// ContentMapper.map(it.content)?.toModel<OutgoingSecretRequest>()
//// }.firstOrNull {
//// it.secretName == secretName
//// }
TODO("not implemented")
} }
val existingOne = getOutgoingRoomKeyRequest(request.requestBody!!) override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
return monarchy.fetchAllCopiedSync { realm ->
if (existingOne != null) { realm.where<IncomingGossipingRequestEntity>()
return existingOne .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
}.mapNotNull {
it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
} }
}
override fun getGossipingEventsTrail(): List<Event> {
return monarchy.fetchAllCopiedSync { realm ->
realm.where<GossipingEventEntity>()
}.map {
it.toModel()
}
}
override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>): OutgoingRoomKeyRequest? {
// Insert the request and return the one passed in parameter
var request: OutgoingRoomKeyRequest? = null
doRealmTransaction(realmConfiguration) { realm ->
val existing = realm.where<OutgoingGossipingRequestEntity>()
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
.findAll()
.mapNotNull {
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
}.firstOrNull {
it.requestBody?.algorithm == requestBody.algorithm
&& it.requestBody?.sessionId == requestBody.sessionId
&& it.requestBody?.senderKey == requestBody.senderKey
&& it.requestBody?.roomId == requestBody.roomId
}
if (existing == null) {
request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply {
this.requestId = LocalEcho.createLocalEchoId()
this.setRecipients(recipients)
this.requestState = OutgoingGossipingRequestState.UNSENT
this.type = GossipRequestType.KEY
this.requestedInfoStr = requestBody.toJson()
}.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
} else {
request = existing
}
}
return request
}
override fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest? {
var request: OutgoingSecretRequest? = null
// Insert the request and return the one passed in parameter // Insert the request and return the one passed in parameter
doRealmTransaction(realmConfiguration) { doRealmTransaction(realmConfiguration) { realm ->
it.createObject(OutgoingRoomKeyRequestEntity::class.java, request.requestId).apply { val existing = realm.where<OutgoingGossipingRequestEntity>()
putRequestBody(request.requestBody) .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
putRecipients(request.recipients) .equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName)
cancellationTxnId = request.cancellationTxnId .findAll()
state = request.state.ordinal .mapNotNull {
it.toOutgoingGossipingRequest() as? OutgoingSecretRequest
}.firstOrNull()
if (existing == null) {
request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply {
this.type = GossipRequestType.SECRET
setRecipients(recipients)
this.requestState = OutgoingGossipingRequestState.UNSENT
this.requestId = LocalEcho.createLocalEchoId()
this.requestedInfoStr = secretName
}.toOutgoingGossipingRequest() as? OutgoingSecretRequest
} else {
request = existing
} }
} }
return request return request
} }
override fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest? { override fun saveGossipingEvent(event: Event) {
val statesIndex = states.map { it.ordinal }.toTypedArray() val now = System.currentTimeMillis()
return doRealmQueryAndCopy(realmConfiguration) { val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
it.where<OutgoingRoomKeyRequestEntity>() val entity = GossipingEventEntity(
.`in`(OutgoingRoomKeyRequestEntityFields.STATE, statesIndex) type = event.type,
.findFirst() sender = event.senderId,
ageLocalTs = ageLocalTs,
content = ContentMapper.map(event.content)
).apply {
sendState = SendState.SYNCED
decryptionResultJson = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
decryptionErrorCode = event.mCryptoError?.name
} }
?.toOutgoingRoomKeyRequest() doRealmTransaction(realmConfiguration) { realm ->
} realm.insertOrUpdate(entity)
override fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) {
doRealmTransaction(realmConfiguration) {
val obj = OutgoingRoomKeyRequestEntity().apply {
requestId = request.requestId
cancellationTxnId = request.cancellationTxnId
state = request.state.ordinal
putRecipients(request.recipients)
putRequestBody(request.requestBody)
}
it.insertOrUpdate(obj)
} }
} }
override fun deleteOutgoingRoomKeyRequest(transactionId: String) { // override fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest? {
doRealmTransaction(realmConfiguration) { // val statesIndex = states.map { it.ordinal }.toTypedArray()
it.where<OutgoingRoomKeyRequestEntity>() // return doRealmQueryAndCopy(realmConfiguration) { realm ->
.equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_ID, transactionId) // realm.where<GossipingEventEntity>()
.findFirst() // .equalTo(GossipingEventEntityFields.SENDER, credentials.userId)
?.deleteFromRealm() // .findAll()
} // .filter {entity ->
} // states.any { it == entity.requestState}
// }
// }.mapNotNull {
// ContentMapper.map(it.content)?.toModel<OutgoingSecretRequest>()
// }
// ?.toOutgoingRoomKeyRequest()
// }
//
// override fun getOutgoingSecretShareRequestByState(states: Set<ShareRequestState>): OutgoingSecretRequest? {
// val statesIndex = states.map { it.ordinal }.toTypedArray()
// return doRealmQueryAndCopy(realmConfiguration) {
// it.where<OutgoingSecretRequestEntity>()
// .`in`(OutgoingSecretRequestEntityFields.STATE, statesIndex)
// .findFirst()
// }
// ?.toOutgoingSecretRequest()
// }
override fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) { // override fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) {
if (incomingRoomKeyRequest == null) { // doRealmTransaction(realmConfiguration) {
return // val obj = OutgoingRoomKeyRequestEntity().apply {
} // requestId = request.requestId
// cancellationTxnId = request.cancellationTxnId
// state = request.state.ordinal
// putRecipients(request.recipients)
// putRequestBody(request.requestBody)
// }
//
// it.insertOrUpdate(obj)
// }
// }
doRealmTransaction(realmConfiguration) { // override fun deleteOutgoingRoomKeyRequest(transactionId: String) {
// Delete any previous store request with the same parameters // doRealmTransaction(realmConfiguration) {
it.where<IncomingRoomKeyRequestEntity>() // it.where<OutgoingRoomKeyRequestEntity>()
.equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId) // .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_ID, transactionId)
.equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId) // .findFirst()
.equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId) // ?.deleteFromRealm()
.findAll() // }
.deleteAllFromRealm() // }
// Then store it // override fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) {
it.createObject(IncomingRoomKeyRequestEntity::class.java).apply { // if (incomingRoomKeyRequest == null) {
userId = incomingRoomKeyRequest.userId // return
deviceId = incomingRoomKeyRequest.deviceId // }
requestId = incomingRoomKeyRequest.requestId //
putRequestBody(incomingRoomKeyRequest.requestBody) // doRealmTransaction(realmConfiguration) {
// // Delete any previous store request with the same parameters
// it.where<IncomingRoomKeyRequestEntity>()
// .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId)
// .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId)
// .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId)
// .findAll()
// .deleteAllFromRealm()
//
// // Then store it
// it.createObject(IncomingRoomKeyRequestEntity::class.java).apply {
// userId = incomingRoomKeyRequest.userId
// deviceId = incomingRoomKeyRequest.deviceId
// requestId = incomingRoomKeyRequest.requestId
// putRequestBody(incomingRoomKeyRequest.requestBody)
// }
// }
// }
// override fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingShareRequestCommon) {
// doRealmTransaction(realmConfiguration) {
// it.where<GossipingEventEntity>()
// .equalTo(GossipingEventEntityFields.TYPE, EventType.ROOM_KEY_REQUEST)
// .notEqualTo(GossipingEventEntityFields.SENDER, credentials.userId)
// .findAll()
// .filter {
// ContentMapper.map(it.content).toModel<IncomingRoomKeyRequest>()?.let {
//
// }
// }
//// .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId)
//// .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId)
//// .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId)
//// .findAll()
//// .deleteAllFromRealm()
// }
// }
override fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) {
doRealmTransaction(realmConfiguration) { realm ->
realm.where<IncomingGossipingRequestEntity>()
.equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, request.userId)
.equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, request.deviceId)
.equalTo(IncomingGossipingRequestEntityFields.REQUEST_ID, request.requestId)
.findAll().forEach {
it.requestState = state
} }
} }
} }
override fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingShareRequestCommon) { override fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) {
doRealmTransaction(realmConfiguration) { doRealmTransaction(realmConfiguration) { realm ->
it.where<IncomingRoomKeyRequestEntity>() realm.where<OutgoingGossipingRequestEntity>()
.equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId) .equalTo(OutgoingGossipingRequestEntityFields.REQUEST_ID, requestId)
.equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId) .findAll().forEach {
.equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId) it.requestState = state
.findAll()
.deleteAllFromRealm()
} }
} }
override fun deleteIncomingSecretRequest(request: IncomingSecretShareRequest) {
doRealmTransaction(realmConfiguration) {
it.where<IncomingSecretRequestEntity>()
.equalTo(IncomingSecretRequestEntityFields.USER_ID, request.userId)
.equalTo(IncomingSecretRequestEntityFields.DEVICE_ID, request.deviceId)
.equalTo(IncomingSecretRequestEntityFields.REQUEST_ID, request.requestId)
.equalTo(IncomingSecretRequestEntityFields.SECRET_NAME, request.secretName)
.findAll()
.deleteAllFromRealm()
}
} }
override fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? { override fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? {
return doRealmQueryAndCopy(realmConfiguration) { return doRealmQueryAndCopyList(realmConfiguration) { realm ->
it.where<IncomingRoomKeyRequestEntity>() realm.where<IncomingGossipingRequestEntity>()
.equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, userId) .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
.equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, deviceId) .equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, deviceId)
.equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, requestId) .equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, userId)
.findFirst() .findAll()
} }.mapNotNull { entity ->
?.toIncomingRoomKeyRequest() entity.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
}.firstOrNull()
} }
override fun getPendingIncomingRoomKeyRequests(): MutableList<IncomingRoomKeyRequest> { override fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
return doRealmQueryAndCopyList(realmConfiguration) { return doRealmQueryAndCopyList(realmConfiguration) {
it.where<IncomingRoomKeyRequestEntity>() it.where<IncomingGossipingRequestEntity>()
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
.equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name)
.findAll() .findAll()
} }
.map { .map { entity ->
it.toIncomingRoomKeyRequest() IncomingRoomKeyRequest(
userId = entity.otherUserId,
deviceId = entity.otherDeviceId,
requestId = entity.requestId,
requestBody = entity.getRequestedKeyInfo(),
localCreationTimestamp = entity.localCreationTimestamp
)
} }
.toMutableList()
} }
override fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest> { override fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon> {
return doRealmQueryAndCopyList(realmConfiguration) { return doRealmQueryAndCopyList(realmConfiguration) {
it.where<IncomingSecretRequestEntity>() it.where<IncomingGossipingRequestEntity>()
.equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name)
.findAll() .findAll()
}.map { }
it.toIncomingSecretShareRequest() .mapNotNull { entity ->
when (entity.type) {
GossipRequestType.KEY -> {
IncomingRoomKeyRequest(
userId = entity.otherUserId,
deviceId = entity.otherDeviceId,
requestId = entity.requestId,
requestBody = entity.getRequestedKeyInfo(),
localCreationTimestamp = entity.localCreationTimestamp
)
}
GossipRequestType.SECRET -> {
IncomingSecretShareRequest(
userId = entity.otherUserId,
deviceId = entity.otherDeviceId,
requestId = entity.requestId,
secretName = entity.getRequestedSecretName(),
localCreationTimestamp = entity.localCreationTimestamp
)
} }
} }
}
}
override fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) {
doRealmTransactionAsync(realmConfiguration) { realm ->
// After a clear cache, we might have a
realm.createObject(IncomingGossipingRequestEntity::class.java).let {
it.otherDeviceId = request.deviceId
it.otherUserId = request.userId
it.requestId = request.requestId ?: ""
it.requestState = GossipingRequestState.PENDING
it.localCreationTimestamp = ageLocalTS ?: System.currentTimeMillis()
if (request is IncomingSecretShareRequest) {
it.type = GossipRequestType.SECRET
it.requestedInfoStr = request.secretName
} else if (request is IncomingRoomKeyRequest) {
it.type = GossipRequestType.KEY
it.requestedInfoStr = request.requestBody?.toJson()
}
}
}
}
// override fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest> {
// return doRealmQueryAndCopyList(realmConfiguration) {
// it.where<GossipingEventEntity>()
// .findAll()
// }.map {
// it.toIncomingSecretShareRequest()
// }
// }
/* ========================================================================================== /* ==========================================================================================
* Cross Signing * Cross Signing
* ========================================================================================== */ * ========================================================================================== */
@ -1051,10 +1241,12 @@ internal class RealmCryptoStore @Inject constructor(
override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> { override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> {
return monarchy.fetchAllMappedSync({ realm -> return monarchy.fetchAllMappedSync({ realm ->
realm.where(OutgoingRoomKeyRequestEntity::class.java) realm
}, { .where(OutgoingGossipingRequestEntity::class.java)
it.toOutgoingRoomKeyRequest() .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
}) }, { entity ->
entity.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
}).filterNotNull()
} }
override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? { override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? {

View File

@ -23,9 +23,10 @@ import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingSecretRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingSecretRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
import im.vector.matrix.android.internal.di.SerializeNulls import im.vector.matrix.android.internal.di.SerializeNulls
@ -140,21 +141,38 @@ internal object RealmCryptoStoreMigration : RealmMigration {
private fun migrateTo2(realm: DynamicRealm) { private fun migrateTo2(realm: DynamicRealm) {
Timber.d("Step 1 -> 2") Timber.d("Step 1 -> 2")
realm.schema.remove("OutgoingRoomKeyRequestEntity")
realm.schema.remove("IncomingRoomKeyRequestEntity")
realm.schema.create("IncomingSecretRequestEntity") //Not need to migrate existing request, just start fresh?
.addField(IncomingSecretRequestEntityFields.DEVICE_ID, String::class.java)
.addField(IncomingSecretRequestEntityFields.SECRET_NAME, String::class.java) realm.schema.create("GossipingEventEntity")
.addField(IncomingSecretRequestEntityFields.REQUEST_ID, String::class.java) .addField(GossipingEventEntityFields.TYPE, String::class.java)
.addField(IncomingSecretRequestEntityFields.USER_ID, String::class.java) .addIndex(GossipingEventEntityFields.TYPE)
.addField(GossipingEventEntityFields.CONTENT, String::class.java)
.addField(GossipingEventEntityFields.SENDER, String::class.java)
.addField(GossipingEventEntityFields.DECRYPTION_RESULT_JSON, String::class.java)
.addField(GossipingEventEntityFields.DECRYPTION_ERROR_CODE, String::class.java)
.addField(GossipingEventEntityFields.AGE_LOCAL_TS, Long::class.java)
.addField(GossipingEventEntityFields.SEND_STATE_STR, Long::class.java)
realm.schema.create("OutgoingSecretRequestEntity") realm.schema.create("IncomingGossipingRequestEntity")
.addField(OutgoingSecretRequestEntityFields.REQUEST_ID, String::class.java) .addField(IncomingGossipingRequestEntityFields.REQUEST_ID, String::class.java)
.addPrimaryKey(OutgoingSecretRequestEntityFields.REQUEST_ID) .addField(IncomingGossipingRequestEntityFields.TYPE_STR, String::class.java)
.addField(OutgoingSecretRequestEntityFields.SECRET_NAME, String::class.java) .addField(IncomingGossipingRequestEntityFields.OTHER_USER_ID, String::class.java)
.addField(OutgoingSecretRequestEntityFields.CANCELLATION_TXN_ID, String::class.java) .addField(IncomingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java)
.addField(OutgoingSecretRequestEntityFields.RECIPIENTS_DATA, String::class.java) .addField(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, String::class.java)
.addField(OutgoingSecretRequestEntityFields.STATE, Int::class.java) .addField(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java)
.addField(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Long::class.java)
.setNullable(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, true)
realm.schema.create("OutgoingGossipingRequestEntity")
.addField(OutgoingGossipingRequestEntityFields.REQUEST_ID, String::class.java)
.addField(OutgoingGossipingRequestEntityFields.TYPE_STR, String::class.java)
.addField(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java)
.addField(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java)
.addField(OutgoingGossipingRequestEntityFields.RECIPIENTS_DATA, String::class.java)
} }
} }

View File

@ -20,14 +20,13 @@ import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoE
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntity import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntity
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingRoomKeyRequestEntity import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntity
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingSecretRequestEntity import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntity
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity 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.OlmSessionEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingRoomKeyRequestEntity import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingSecretRequestEntity
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
import io.realm.annotations.RealmModule import io.realm.annotations.RealmModule
@ -40,16 +39,19 @@ import io.realm.annotations.RealmModule
CryptoMetadataEntity::class, CryptoMetadataEntity::class,
CryptoRoomEntity::class, CryptoRoomEntity::class,
DeviceInfoEntity::class, DeviceInfoEntity::class,
IncomingRoomKeyRequestEntity::class, // IncomingRoomKeyRequestEntity::class,
KeysBackupDataEntity::class, KeysBackupDataEntity::class,
OlmInboundGroupSessionEntity::class, OlmInboundGroupSessionEntity::class,
OlmSessionEntity::class, OlmSessionEntity::class,
OutgoingRoomKeyRequestEntity::class, // OutgoingRoomKeyRequestEntity::class,
UserEntity::class, UserEntity::class,
KeyInfoEntity::class, KeyInfoEntity::class,
CrossSigningInfoEntity::class, CrossSigningInfoEntity::class,
TrustLevelEntity::class, TrustLevelEntity::class,
IncomingSecretRequestEntity::class, // IncomingSecretRequestEntity::class,
OutgoingSecretRequestEntity::class // OutgoingSecretRequestEntity::class,
GossipingEventEntity::class,
IncomingGossipingRequestEntity::class,
OutgoingGossipingRequestEntity::class
]) ])
internal class RealmCryptoStoreModule internal class RealmCryptoStoreModule

View File

@ -0,0 +1,88 @@
/*
* 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 com.squareup.moshi.JsonDataException
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.room.send.SendState
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.di.MoshiProvider
import io.realm.RealmObject
import io.realm.annotations.Index
import timber.log.Timber
/**
* Keep track of gossiping event received in toDevice messages
* (room key request, or sss secret sharing, as well as cancellations)
*
*/
internal open class GossipingEventEntity(@Index var type: String = "",
var content: String? = null,
@Index var sender: String? = null,
var decryptionResultJson: String? = null,
var decryptionErrorCode: String? = null,
var ageLocalTs: Long? = null) : RealmObject() {
private var sendStateStr: String = SendState.UNKNOWN.name
var sendState: SendState
get() {
return SendState.valueOf(sendStateStr)
}
set(value) {
sendStateStr = value.name
}
companion object
fun setDecryptionResult(result: MXEventDecryptionResult) {
val decryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java)
decryptionResultJson = adapter.toJson(decryptionResult)
decryptionErrorCode = null
}
fun toModel(): Event {
return Event(
type = this.type,
content = ContentMapper.map(this.content),
senderId = this.sender
).also {
it.ageLocalTs = this.ageLocalTs
it.sendState = this.sendState
this.decryptionResultJson?.let { json ->
try {
it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json)
} catch (t: JsonDataException) {
Timber.e(t, "Failed to parse decryption result")
}
}
// TODO get the full crypto error object
it.mCryptoError = this.decryptionErrorCode?.let { errorCode ->
MXCryptoError.ErrorType.valueOf(errorCode)
}
}
}
}

View File

@ -0,0 +1,90 @@
/*
* 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.api.extensions.tryThis
import im.vector.matrix.android.internal.crypto.GossipRequestType
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.model.rest.RoomKeyRequestBody
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.Index
internal open class IncomingGossipingRequestEntity(@Index var requestId: String = "",
@Index var typeStr: String? = null,
var otherUserId: String? = null,
var requestedInfoStr: String? = null,
var otherDeviceId: String? = null,
var localCreationTimestamp: Long? = null
) : RealmObject() {
fun getRequestedSecretName(): String? = if (type == GossipRequestType.SECRET) {
requestedInfoStr
} else null
fun getRequestedKeyInfo(): RoomKeyRequestBody? = if (type == GossipRequestType.KEY) {
RoomKeyRequestBody.fromJson(requestedInfoStr)
} else null
var type: GossipRequestType
get() {
return tryThis { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY
}
set(value) {
typeStr = value.name
}
private var requestStateStr: String = GossipingRequestState.NONE.name
var requestState: GossipingRequestState
get() {
return tryThis { GossipingRequestState.valueOf(requestStateStr) }
?: GossipingRequestState.NONE
}
set(value) {
requestStateStr = value.name
}
companion object
fun toIncomingGossipingRequest(): IncomingShareRequestCommon {
return when (type) {
GossipRequestType.KEY -> {
IncomingRoomKeyRequest(
requestBody = getRequestedKeyInfo(),
deviceId = otherDeviceId,
userId = otherUserId,
requestId = requestId,
state = requestState,
localCreationTimestamp = localCreationTimestamp
)
}
GossipRequestType.SECRET -> {
IncomingSecretShareRequest(
secretName = getRequestedSecretName(),
deviceId = otherDeviceId,
userId = otherUserId,
requestId = requestId,
localCreationTimestamp = localCreationTimestamp
)
}
}
}
}

View File

@ -1,56 +1,56 @@
/* ///*
* Copyright 2018 New Vector Ltd // * Copyright 2018 New Vector Ltd
* // *
* Licensed under the Apache License, Version 2.0 (the "License"); // * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. // * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at // * You may obtain a copy of the License at
* // *
* http://www.apache.org/licenses/LICENSE-2.0 // * http://www.apache.org/licenses/LICENSE-2.0
* // *
* Unless required by applicable law or agreed to in writing, software // * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, // * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and // * See the License for the specific language governing permissions and
* limitations under the License. // * limitations under the License.
*/ // */
//
package im.vector.matrix.android.internal.crypto.store.db.model //package im.vector.matrix.android.internal.crypto.store.db.model
//
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest //import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody //import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import io.realm.RealmObject //import io.realm.RealmObject
//
internal open class IncomingRoomKeyRequestEntity( //internal open class IncomingRoomKeyRequestEntity(
var requestId: String? = null, // var requestId: String? = null,
var userId: String? = null, // var userId: String? = null,
var deviceId: String? = null, // var deviceId: String? = null,
// RoomKeyRequestBody fields // // RoomKeyRequestBody fields
var requestBodyAlgorithm: String? = null, // var requestBodyAlgorithm: String? = null,
var requestBodyRoomId: String? = null, // var requestBodyRoomId: String? = null,
var requestBodySenderKey: String? = null, // var requestBodySenderKey: String? = null,
var requestBodySessionId: String? = null // var requestBodySessionId: String? = null
) : RealmObject() { //) : RealmObject() {
//
fun toIncomingRoomKeyRequest(): IncomingRoomKeyRequest { // fun toIncomingRoomKeyRequest(): IncomingRoomKeyRequest {
return IncomingRoomKeyRequest( // return IncomingRoomKeyRequest(
requestId = requestId, // requestId = requestId,
userId = userId, // userId = userId,
deviceId = deviceId, // deviceId = deviceId,
requestBody = RoomKeyRequestBody( // requestBody = RoomKeyRequestBody(
algorithm = requestBodyAlgorithm, // algorithm = requestBodyAlgorithm,
roomId = requestBodyRoomId, // roomId = requestBodyRoomId,
senderKey = requestBodySenderKey, // senderKey = requestBodySenderKey,
sessionId = requestBodySessionId // sessionId = requestBodySessionId
) // )
) // )
} // }
//
fun putRequestBody(requestBody: RoomKeyRequestBody?) { // fun putRequestBody(requestBody: RoomKeyRequestBody?) {
requestBody?.let { // requestBody?.let {
requestBodyAlgorithm = it.algorithm // requestBodyAlgorithm = it.algorithm
requestBodyRoomId = it.roomId // requestBodyRoomId = it.roomId
requestBodySenderKey = it.senderKey // requestBodySenderKey = it.senderKey
requestBodySessionId = it.sessionId // requestBodySessionId = it.sessionId
} // }
} // }
} //}

View File

@ -1,37 +1,37 @@
/* ///*
* Copyright (c) 2020 New Vector Ltd // * Copyright (c) 2020 New Vector Ltd
* // *
* Licensed under the Apache License, Version 2.0 (the "License"); // * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. // * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at // * You may obtain a copy of the License at
* // *
* http://www.apache.org/licenses/LICENSE-2.0 // * http://www.apache.org/licenses/LICENSE-2.0
* // *
* Unless required by applicable law or agreed to in writing, software // * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, // * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and // * See the License for the specific language governing permissions and
* limitations under the License. // * limitations under the License.
*/ // */
//
package im.vector.matrix.android.internal.crypto.store.db.model //package im.vector.matrix.android.internal.crypto.store.db.model
//
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest //import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
import io.realm.RealmObject //import io.realm.RealmObject
//
internal open class IncomingSecretRequestEntity( //internal open class IncomingSecretRequestEntity(
var requestId: String? = null, // var requestId: String? = null,
var userId: String? = null, // var userId: String? = null,
var deviceId: String? = null, // var deviceId: String? = null,
var secretName: String? = null // var secretName: String? = null
) : RealmObject() { //) : RealmObject() {
//
fun toIncomingSecretShareRequest(): IncomingSecretShareRequest { // fun toIncomingSecretShareRequest(): IncomingSecretShareRequest {
return IncomingSecretShareRequest( // return IncomingSecretShareRequest(
requestId = requestId, // requestId = requestId,
userId = userId, // userId = userId,
deviceId = deviceId, // deviceId = deviceId,
secretName = secretName // secretName = secretName
) // )
} // }
} //}

View File

@ -0,0 +1,107 @@
/*
* 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 com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Types
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.internal.crypto.GossipRequestType
import im.vector.matrix.android.internal.crypto.GossipingRequestState
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequest
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestState
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.di.MoshiProvider
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.Index
internal open class OutgoingGossipingRequestEntity(
@Index var requestId: String? = null,
// var cancellationTxnId: String? = null,
// Serialized Json
var recipientsData: String? = null,
var requestedInfoStr: String? = null,
@Index var typeStr: String? = null,
var sourceEvents: RealmList<String> = RealmList()
) : RealmObject() {
fun getRequestedSecretName(): String? = if (type == GossipRequestType.SECRET) {
requestedInfoStr
} else null
fun getRequestedKeyInfo(): RoomKeyRequestBody? = if (type == GossipRequestType.KEY) {
RoomKeyRequestBody.fromJson(requestedInfoStr)
} else null
var type: GossipRequestType
get() {
return tryThis { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY
}
set(value) {
typeStr = value.name
}
private var requestStateStr: String = OutgoingGossipingRequestState.UNSENT.name
var requestState: OutgoingGossipingRequestState
get() {
return tryThis { OutgoingGossipingRequestState.valueOf(requestStateStr) }
?: OutgoingGossipingRequestState.UNSENT
}
set(value) {
requestStateStr = value.name
}
companion object {
private val recipientsDataMapper: JsonAdapter<Map<String, List<String>>> =
MoshiProvider.providesMoshi().adapter<Map<String, List<String>>>(Types.newParameterizedType(Map::class.java, String::class.java, List::class.java))
}
fun toOutgoingGossipingRequest(): OutgoingGossipingRequest {
return when (type) {
GossipRequestType.KEY -> {
OutgoingRoomKeyRequest(
requestBody = getRequestedKeyInfo(),
recipients = getRecipients() ?: emptyMap(),
requestId = requestId ?: "",
state = requestState
)
}
GossipRequestType.SECRET -> {
OutgoingSecretRequest(
secretName = getRequestedSecretName(),
recipients = getRecipients() ?: emptyMap(),
requestId = requestId ?: "",
state = requestState
)
}
}
}
private fun getRecipients(): Map<String, List<String>>? {
return this.recipientsData?.let { recipientsDataMapper.fromJson(it) }
}
fun setRecipients(recipients: Map<String, List<String>>) {
this.recipientsData = recipientsDataMapper.toJson(recipients)
}
}

View File

@ -1,77 +1,77 @@
/* ///*
* Copyright 2018 New Vector Ltd // * Copyright 2018 New Vector Ltd
* // *
* Licensed under the Apache License, Version 2.0 (the "License"); // * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. // * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at // * You may obtain a copy of the License at
* // *
* http://www.apache.org/licenses/LICENSE-2.0 // * http://www.apache.org/licenses/LICENSE-2.0
* // *
* Unless required by applicable law or agreed to in writing, software // * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, // * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and // * See the License for the specific language governing permissions and
* limitations under the License. // * limitations under the License.
*/ // */
//
package im.vector.matrix.android.internal.crypto.store.db.model //package im.vector.matrix.android.internal.crypto.store.db.model
//
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest //import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.ShareRequestState //import im.vector.matrix.android.internal.crypto.ShareRequestState
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody //import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm //import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm
import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm //import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm
import io.realm.RealmObject //import io.realm.RealmObject
import io.realm.annotations.PrimaryKey //import io.realm.annotations.PrimaryKey
//
internal open class OutgoingRoomKeyRequestEntity( //internal open class OutgoingRoomKeyRequestEntity(
@PrimaryKey var requestId: String? = null, // @PrimaryKey var requestId: String? = null,
var cancellationTxnId: String? = null, // var cancellationTxnId: String? = null,
// Serialized Json // // Serialized Json
var recipientsData: String? = null, // var recipientsData: String? = null,
// RoomKeyRequestBody fields // // RoomKeyRequestBody fields
var requestBodyAlgorithm: String? = null, // var requestBodyAlgorithm: String? = null,
var requestBodyRoomId: String? = null, // var requestBodyRoomId: String? = null,
var requestBodySenderKey: String? = null, // var requestBodySenderKey: String? = null,
var requestBodySessionId: String? = null, // var requestBodySessionId: String? = null,
// State // // State
var state: Int = 0 // var state: Int = 0
) : RealmObject() { //) : RealmObject() {
//
/** // /**
* Convert to OutgoingRoomKeyRequest // * Convert to OutgoingRoomKeyRequest
*/ // */
fun toOutgoingRoomKeyRequest(): OutgoingRoomKeyRequest { // fun toOutgoingRoomKeyRequest(): OutgoingRoomKeyRequest {
val cancellationTxnId = this.cancellationTxnId // val cancellationTxnId = this.cancellationTxnId
return OutgoingRoomKeyRequest( // return OutgoingRoomKeyRequest(
RoomKeyRequestBody( // RoomKeyRequestBody(
algorithm = requestBodyAlgorithm, // algorithm = requestBodyAlgorithm,
roomId = requestBodyRoomId, // roomId = requestBodyRoomId,
senderKey = requestBodySenderKey, // senderKey = requestBodySenderKey,
sessionId = requestBodySessionId // sessionId = requestBodySessionId
), // ),
getRecipients()!!, // getRecipients()!!,
requestId!!, // requestId!!,
ShareRequestState.from(state) // ShareRequestState.from(state)
).apply { // ).apply {
this.cancellationTxnId = cancellationTxnId // this.cancellationTxnId = cancellationTxnId
} // }
} // }
//
private fun getRecipients(): List<Map<String, String>>? { // private fun getRecipients(): List<Map<String, String>>? {
return deserializeFromRealm(recipientsData) // return deserializeFromRealm(recipientsData)
} // }
//
fun putRecipients(recipients: List<Map<String, String>>?) { // fun putRecipients(recipients: List<Map<String, String>>?) {
recipientsData = serializeForRealm(recipients) // recipientsData = serializeForRealm(recipients)
} // }
//
fun putRequestBody(requestBody: RoomKeyRequestBody?) { // fun putRequestBody(requestBody: RoomKeyRequestBody?) {
requestBody?.let { // requestBody?.let {
requestBodyAlgorithm = it.algorithm // requestBodyAlgorithm = it.algorithm
requestBodyRoomId = it.roomId // requestBodyRoomId = it.roomId
requestBodySenderKey = it.senderKey // requestBodySenderKey = it.senderKey
requestBodySessionId = it.sessionId // requestBodySessionId = it.sessionId
} // }
} // }
} //}

View File

@ -1,63 +1,63 @@
/* ///*
* Copyright (c) 2020 New Vector Ltd // * Copyright (c) 2020 New Vector Ltd
* // *
* Licensed under the Apache License, Version 2.0 (the "License"); // * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. // * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at // * You may obtain a copy of the License at
* // *
* http://www.apache.org/licenses/LICENSE-2.0 // * http://www.apache.org/licenses/LICENSE-2.0
* // *
* Unless required by applicable law or agreed to in writing, software // * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, // * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and // * See the License for the specific language governing permissions and
* limitations under the License. // * limitations under the License.
*/ // */
//
package im.vector.matrix.android.internal.crypto.store.db.model //package im.vector.matrix.android.internal.crypto.store.db.model
//
import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest //import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest
import im.vector.matrix.android.internal.crypto.ShareRequestState //import im.vector.matrix.android.internal.crypto.ShareRequestState
import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm //import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm
import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm //import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm
import io.realm.RealmObject //import io.realm.RealmObject
import io.realm.annotations.PrimaryKey //import io.realm.annotations.PrimaryKey
//
internal open class OutgoingSecretRequestEntity( //internal open class OutgoingSecretRequestEntity(
@PrimaryKey var requestId: String? = null, // @PrimaryKey var requestId: String? = null,
var cancellationTxnId: String? = null, // var cancellationTxnId: String? = null,
// Serialized Json // // Serialized Json
var recipientsData: String? = null, // var recipientsData: String? = null,
// RoomKeyRequestBody fields // // RoomKeyRequestBody fields
var secretName: String? = null, // var secretName: String? = null,
// State // // State
var state: Int = 0 // var state: Int = 0
) : RealmObject() { //) : RealmObject() {
//
/** // /**
* Convert to OutgoingRoomKeyRequest // * Convert to OutgoingRoomKeyRequest
*/ // */
fun toOutgoingSecretRequest(): OutgoingSecretRequest { // fun toOutgoingSecretRequest(): OutgoingSecretRequest {
val cancellationTxnId = this.cancellationTxnId // val cancellationTxnId = this.cancellationTxnId
return OutgoingSecretRequest( // return OutgoingSecretRequest(
secretName, // secretName,
getRecipients() ?: emptyList(), // getRecipients() ?: emptyList(),
requestId!!, // requestId!!,
ShareRequestState.from(state) // ShareRequestState.from(state)
).apply { // ).apply {
this.cancellationTxnId = cancellationTxnId // this.cancellationTxnId = cancellationTxnId
} // }
} // }
//
private fun getRecipients(): List<Map<String, String>>? { // private fun getRecipients(): List<Map<String, String>>? {
return try { // return try {
deserializeFromRealm(recipientsData) // deserializeFromRealm(recipientsData)
} catch (failure: Throwable) { // } catch (failure: Throwable) {
null // null
} // }
} // }
//
fun putRecipients(recipients: List<Map<String, String>>?) { // fun putRecipients(recipients: List<Map<String, String>>?) {
recipientsData = serializeForRealm(recipients) // recipientsData = serializeForRealm(recipients)
} // }
} //}

View File

@ -61,7 +61,6 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
params.events.forEach { event -> params.events.forEach { event ->
Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
Timber.v("## SAS Verification live observer: received msgId: $event")
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past, // If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
// the message should be ignored by the receiver. // the message should be ignored by the receiver.

View File

@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificati
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64 import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64Safe import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64Safe
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore

View File

@ -20,7 +20,9 @@ import dagger.BindsInstance
import dagger.Component import dagger.Component
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.crypto.CancelGossipRequestWorker
import im.vector.matrix.android.internal.crypto.CryptoModule import im.vector.matrix.android.internal.crypto.CryptoModule
import im.vector.matrix.android.internal.crypto.SendGossipRequestWorker
import im.vector.matrix.android.internal.crypto.verification.SendVerificationMessageWorker import im.vector.matrix.android.internal.crypto.verification.SendVerificationMessageWorker
import im.vector.matrix.android.internal.di.MatrixComponent import im.vector.matrix.android.internal.di.MatrixComponent
import im.vector.matrix.android.internal.di.SessionAssistedInjectModule import im.vector.matrix.android.internal.di.SessionAssistedInjectModule
@ -106,6 +108,9 @@ internal interface SessionComponent {
fun inject(worker: SendVerificationMessageWorker) fun inject(worker: SendVerificationMessageWorker)
fun inject(worker: SendGossipRequestWorker)
fun inject(worker: CancelGossipRequestWorker)
@Component.Factory @Component.Factory
interface Factory { interface Factory {
fun create( fun create(

View File

@ -249,6 +249,7 @@ dependencies {
def moshi_version = '1.8.0' def moshi_version = '1.8.0'
def daggerVersion = '2.25.4' def daggerVersion = '2.25.4'
def autofill_version = "1.0.0" def autofill_version = "1.0.0"
def work_version = '2.3.2'
implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android")
implementation project(":matrix-sdk-android-rx") implementation project(":matrix-sdk-android-rx")
@ -296,7 +297,7 @@ dependencies {
implementation 'com.airbnb.android:mvrx:1.3.0' implementation 'com.airbnb.android:mvrx:1.3.0'
// Work // Work
implementation "androidx.work:work-runtime-ktx:2.3.3" implementation "androidx.work:work-runtime-ktx:$work_version"
// Paging // Paging
implementation "androidx.paging:paging-runtime-ktx:2.1.1" implementation "androidx.paging:paging-runtime-ktx:2.1.1"

View File

@ -74,7 +74,9 @@ import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment
import im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment import im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
import im.vector.riotx.features.settings.devtools.AccountDataFragment import im.vector.riotx.features.settings.devtools.AccountDataFragment
import im.vector.riotx.features.settings.devtools.KeyRequestListFragment import im.vector.riotx.features.settings.devtools.GossipingEventsPaperTrailFragment
import im.vector.riotx.features.settings.devtools.IncomingKeyRequestListFragment
import im.vector.riotx.features.settings.devtools.OutgoingKeyRequestListFragment
import im.vector.riotx.features.settings.devtools.KeyRequestsFragment import im.vector.riotx.features.settings.devtools.KeyRequestsFragment
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
import im.vector.riotx.features.settings.push.PushGatewaysFragment import im.vector.riotx.features.settings.push.PushGatewaysFragment
@ -371,11 +373,24 @@ interface FragmentModule {
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(KeyRequestListFragment::class) @FragmentKey(OutgoingKeyRequestListFragment::class)
fun bindKeyRequestListFragment(fragment: KeyRequestListFragment): Fragment fun bindOutgoingKeyRequestListFragment(fragment: OutgoingKeyRequestListFragment): Fragment
@Binds
@IntoMap
@FragmentKey(IncomingKeyRequestListFragment::class)
fun bindIncomingKeyRequestListFragment(fragment: IncomingKeyRequestListFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(KeyRequestsFragment::class) @FragmentKey(KeyRequestsFragment::class)
fun bindKeyRequestsFragment(fragment: KeyRequestsFragment): Fragment fun bindKeyRequestsFragment(fragment: KeyRequestsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(GossipingEventsPaperTrailFragment::class)
fun bindGossipingEventsPaperTrailFragment(fragment: GossipingEventsPaperTrailFragment): Fragment
} }

View File

@ -0,0 +1,203 @@
/*
* 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.settings.devtools
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
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
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.rest.ForwardedRoomKeyContent
import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObject
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareRequest
import im.vector.riotx.R
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.DateProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.genericFooterItem
import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.core.ui.list.genericItemHeader
import me.gujun.android.span.span
import javax.inject.Inject
class GossipingEventsEpoxyController @Inject constructor(
private val stringProvider: StringProvider,
private val vectorDateFormatter: VectorDateFormatter,
private val colorProvider: ColorProvider
) : TypedEpoxyController<GossipingEventsPaperTrailState>() {
interface InteractionListener {
fun didTap(event: Event)
}
var interactionListener: InteractionListener? = null
override fun buildModels(data: GossipingEventsPaperTrailState?) {
when (val async = data?.events) {
is Uninitialized,
is Loading -> {
loadingItem {
id("loadingOutgoing")
loadingText(stringProvider.getString(R.string.loading))
}
}
is Fail -> {
genericItem {
id("failOutgoing")
title(async.error.localizedMessage)
}
}
is Success -> {
val eventList = async.invoke()
if (eventList.isEmpty()) {
genericFooterItem {
id("empty")
text(stringProvider.getString(R.string.no_result_placeholder))
}
return
}
eventList.forEachIndexed { _, event ->
genericItem {
id(event.hashCode())
title(
if (event.isEncrypted()) {
"${event.getClearType()} [encrypted]"
} else {
event.type
}
)
description(
span {
+vectorDateFormatter.formatMessageDay(DateProvider.toLocalDateTime(event.ageLocalTs))
+" ${vectorDateFormatter.formatMessageHour(DateProvider.toLocalDateTime(event.ageLocalTs))}"
span("\nfrom: ") {
textStyle = "bold"
}
+"${event.senderId}"
apply {
if (event.getClearType() == EventType.ROOM_KEY_REQUEST) {
val content = event.getClearContent().toModel<RoomKeyShareRequest>()
span("\nreqId:") {
textStyle = "bold"
}
+" ${content?.requestId}"
span("\naction:") {
textStyle = "bold"
}
+" ${content?.action}"
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
span("\nsessionId:") {
textStyle = "bold"
}
+" ${content.body?.sessionId}"
}
span("\nrequestedBy: ") {
textStyle = "bold"
}
+"${content?.requestingDeviceId}"
} else if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
val encryptedContent = event.content.toModel<OlmEventContent>()
val content = event.getClearContent().toModel<ForwardedRoomKeyContent>()
if (event.mxDecryptionResult == null) {
span("**Failed to Decrypt** ${event.mCryptoError}") {
textColor = colorProvider.getColor(R.color.vector_error_color)
}
}
span("\nsessionId:") {
textStyle = "bold"
}
+" ${content?.sessionId}"
span("\nFrom Device (sender key):") {
textStyle = "bold"
}
+" ${encryptedContent?.senderKey}"
}
}
}
)
}
}
}
}
}
private fun buildOutgoing(data: KeyRequestListViewState?) {
data?.outgoingRoomKeyRequest?.let { async ->
when (async) {
is Uninitialized,
is Loading -> {
loadingItem {
id("loadingOutgoing")
loadingText(stringProvider.getString(R.string.loading))
}
}
is Fail -> {
genericItem {
id("failOutgoing")
title(async.error.localizedMessage)
}
}
is Success -> {
if (async.invoke().isEmpty()) {
genericFooterItem {
id("empty")
text(stringProvider.getString(R.string.no_result_placeholder))
}
return
}
val requestList = async.invoke().groupBy { it.roomId }
requestList.forEach {
genericItemHeader {
id(it.key)
text("roomId: ${it.key}")
}
it.value.forEach { roomKeyRequest ->
genericItem {
id(roomKeyRequest.requestId)
title(roomKeyRequest.requestId)
description(
span {
span("sessionId:\n") {
textStyle = "bold"
}
+"${roomKeyRequest.sessionId}"
span("\nstate:") {
textStyle = "bold"
}
+"\n${roomKeyRequest.state.name}"
}
)
}
}
}
}
}.exhaustive
}
}
}

View File

@ -0,0 +1,68 @@
/*
* 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.settings.devtools
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.resources.ColorProvider
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import javax.inject.Inject
class GossipingEventsPaperTrailFragment @Inject constructor(
val viewModelFactory: GossipingEventsPaperTrailViewModel.Factory,
private val epoxyController: GossipingEventsEpoxyController
) : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_generic_recycler
override fun getMenuRes(): Int = R.menu.menu_common_gossiping
private val viewModel: GossipingEventsPaperTrailViewModel by fragmentViewModel(GossipingEventsPaperTrailViewModel::class)
override fun invalidate() = withState(viewModel) { state ->
epoxyController.setData(state)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.configureWith(epoxyController, showDivider = true)
// epoxyController.interactionListener = this
}
override fun onDestroyView() {
super.onDestroyView()
recyclerView.cleanup()
// epoxyController.interactionListener = null
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.refresh) {
viewModel.refresh()
return true
} else {
return super.onOptionsItemSelected(item)
}
}
}

View File

@ -0,0 +1,79 @@
/*
* 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.settings.devtools
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
data class GossipingEventsPaperTrailState(
val events: Async<List<Event>> = Uninitialized
) : MvRxState
class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted initialState: GossipingEventsPaperTrailState,
private val session: Session)
: VectorViewModel<GossipingEventsPaperTrailState, EmptyAction, EmptyViewEvents>(initialState) {
init {
refresh()
}
fun refresh() {
setState {
copy(events = Loading())
}
GlobalScope.launch {
session.cryptoService().getGossipingEventsTrail().let {
val sorted = it.sortedByDescending { it.ageLocalTs }
setState {
copy(events = Success(sorted))
}
}
}
}
override fun handle(action: EmptyAction) {}
@AssistedInject.Factory
interface Factory {
fun create(initialState: GossipingEventsPaperTrailState): GossipingEventsPaperTrailViewModel
}
companion object : MvRxViewModelFactory<GossipingEventsPaperTrailViewModel, GossipingEventsPaperTrailState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: GossipingEventsPaperTrailState): GossipingEventsPaperTrailViewModel? {
val fragment: GossipingEventsPaperTrailFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.viewModelFactory.create(state)
}
}
}

View File

@ -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.riotx.features.settings.devtools
import android.os.Bundle
import android.view.View
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.resources.ColorProvider
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import javax.inject.Inject
class IncomingKeyRequestListFragment @Inject constructor(
val viewModelFactory: KeyRequestListViewModel.Factory,
private val epoxyController: KeyRequestEpoxyController,
private val colorProvider: ColorProvider
) : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_generic_recycler
private val viewModel: KeyRequestListViewModel by fragmentViewModel(KeyRequestListViewModel::class)
override fun invalidate() = withState(viewModel) { state ->
epoxyController.outgoing = false
epoxyController.setData(state)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.configureWith(epoxyController, showDivider = true)
// epoxyController.interactionListener = this
}
override fun onDestroyView() {
super.onDestroyView()
recyclerView.cleanup()
// epoxyController.interactionListener = null
}
}

View File

@ -25,6 +25,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.genericFooterItem
import im.vector.riotx.core.ui.list.genericItem import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.core.ui.list.genericItemHeader import im.vector.riotx.core.ui.list.genericItemHeader
import me.gujun.android.span.span import me.gujun.android.span.span
@ -38,9 +39,75 @@ class KeyRequestEpoxyController @Inject constructor(
// fun didTap(data: UserAccountData) // fun didTap(data: UserAccountData)
} }
var outgoing = true
var interactionListener: InteractionListener? = null var interactionListener: InteractionListener? = null
override fun buildModels(data: KeyRequestListViewState?) { override fun buildModels(data: KeyRequestListViewState?) {
if (outgoing) {
buildOutgoing(data)
} else {
buildIncoming(data)
}
}
private fun buildIncoming(data: KeyRequestListViewState?) {
data?.incomingRequests?.let { async ->
when (async) {
is Uninitialized,
is Loading -> {
loadingItem {
id("loadingOutgoing")
loadingText(stringProvider.getString(R.string.loading))
}
}
is Fail -> {
genericItem {
id("failOutgoing")
title(async.error.localizedMessage)
}
}
is Success -> {
if (async.invoke().isEmpty()) {
genericFooterItem {
id("empty")
text(stringProvider.getString(R.string.no_result_placeholder))
}
return
}
val requestList = async.invoke().groupBy { it.userId }
requestList.forEach {
genericItemHeader {
id(it.key)
text("From user: ${it.key}")
}
it.value.forEach { roomKeyRequest ->
genericItem {
id(roomKeyRequest.requestId)
title(roomKeyRequest.requestId)
description(
span {
span("sessionId:") {
textStyle = "bold"
}
span("\nFrom device:") {
textStyle = "bold"
}
+"${roomKeyRequest.deviceId}"
+"\n${roomKeyRequest.state.name}"
}
)
}
}
}
}
}.exhaustive
}
}
private fun buildOutgoing(data: KeyRequestListViewState?) {
data?.outgoingRoomKeyRequest?.let { async -> data?.outgoingRoomKeyRequest?.let { async ->
when (async) { when (async) {
is Uninitialized, is Uninitialized,
@ -57,6 +124,15 @@ class KeyRequestEpoxyController @Inject constructor(
} }
} }
is Success -> { is Success -> {
if (async.invoke().isEmpty()) {
genericFooterItem {
id("empty")
text(stringProvider.getString(R.string.no_result_placeholder))
}
return
}
val requestList = async.invoke().groupBy { it.roomId } val requestList = async.invoke().groupBy { it.roomId }
requestList.forEach { requestList.forEach {
@ -70,7 +146,7 @@ class KeyRequestEpoxyController @Inject constructor(
title(roomKeyRequest.requestId) title(roomKeyRequest.requestId)
description( description(
span { span {
span("sessionId:") { span("sessionId:\n") {
textStyle = "bold" textStyle = "bold"
} }
+"${roomKeyRequest.sessionId}" +"${roomKeyRequest.sessionId}"

View File

@ -31,6 +31,8 @@ import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest
import im.vector.riotx.core.platform.EmptyAction import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.EmptyViewEvents import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
data class KeyRequestListViewState( data class KeyRequestListViewState(
val incomingRequests: Async<List<IncomingRoomKeyRequest>> = Uninitialized, val incomingRequests: Async<List<IncomingRoomKeyRequest>> = Uninitialized,
@ -42,6 +44,11 @@ class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState
: VectorViewModel<KeyRequestListViewState, EmptyAction, EmptyViewEvents>(initialState) { : VectorViewModel<KeyRequestListViewState, EmptyAction, EmptyViewEvents>(initialState) {
init { init {
refresh()
}
fun refresh() {
GlobalScope.launch {
session.cryptoService().getOutgoingRoomKeyRequest().let { session.cryptoService().getOutgoingRoomKeyRequest().let {
setState { setState {
copy( copy(
@ -49,6 +56,14 @@ class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState
) )
} }
} }
session.cryptoService().getIncomingRoomKeyRequest().let {
setState {
copy(
incomingRequests = Success(it)
)
}
}
}
} }
override fun handle(action: EmptyAction) {} override fun handle(action: EmptyAction) {}
@ -58,12 +73,16 @@ class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState
fun create(initialState: KeyRequestListViewState): KeyRequestListViewModel fun create(initialState: KeyRequestListViewState): KeyRequestListViewModel
} }
companion object : MvRxViewModelFactory<KeyRequestListViewModel, KeyRequestListViewState> { companion object : MvRxViewModelFactory<KeyRequestListViewModel, KeyRequestListViewState> {
@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: KeyRequestListViewState): KeyRequestListViewModel? { override fun create(viewModelContext: ViewModelContext, state: KeyRequestListViewState): KeyRequestListViewModel? {
val fragment: KeyRequestListFragment = (viewModelContext as FragmentViewModelContext).fragment() val context = viewModelContext as FragmentViewModelContext
return fragment.viewModelFactory.create(state) val factory = (context.fragment as? IncomingKeyRequestListFragment)?.viewModelFactory
?: (context.fragment as? OutgoingKeyRequestListFragment)?.viewModelFactory
return factory?.create(state)
} }
} }
} }

View File

@ -16,11 +16,14 @@
package im.vector.riotx.features.settings.devtools package im.vector.riotx.features.settings.devtools
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_IDLE
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
@ -31,26 +34,79 @@ import javax.inject.Inject
class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() { class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() {
override fun getLayoutResId(): Int = R.layout.fragment_devtool_keyrequests override fun getLayoutResId(): Int = R.layout.fragment_devtool_keyrequests
override fun getMenuRes(): Int = R.menu.menu_common_gossiping
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.key_share_request) (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.key_share_request)
} }
private var mPagerAdapter: KeyReqPagerAdapter? = null
private val pageAdapterListener = object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
invalidateOptionsMenu()
}
override fun onPageScrollStateChanged(state: Int) {
childFragmentManager.fragments.forEach {
setHasOptionsMenu(state == SCROLL_STATE_IDLE)
}
invalidateOptionsMenu()
}
}
override fun onDestroy() {
invalidateOptionsMenu()
super.onDestroy()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
devToolKeyRequestPager.adapter = KeyReqPagerAdapter(requireActivity()) mPagerAdapter = KeyReqPagerAdapter(this)
devToolKeyRequestPager.adapter = mPagerAdapter
devToolKeyRequestPager.registerOnPageChangeCallback(pageAdapterListener)
TabLayoutMediator(devToolKeyRequestTabs, devToolKeyRequestPager) { tab, _ -> TabLayoutMediator(devToolKeyRequestTabs, devToolKeyRequestPager) { tab, position ->
when (position) {
0 -> {
tab.text = "Outgoing" tab.text = "Outgoing"
}
1 -> {
tab.text = "Incoming"
}
2 -> {
tab.text = "Audit Trail"
}
}
}.attach() }.attach()
} }
private inner class KeyReqPagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { override fun onDestroyView() {
override fun getItemCount(): Int = 1 devToolKeyRequestPager.unregisterOnPageChangeCallback(pageAdapterListener)
mPagerAdapter = null
super.onDestroyView()
}
private inner class KeyReqPagerAdapter(fa: Fragment) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment { override fun createFragment(position: Int): Fragment {
return childFragmentManager.fragmentFactory.instantiate(requireContext().classLoader, KeyRequestListFragment::class.java.name) return when (position) {
0 -> {
childFragmentManager.fragmentFactory.instantiate(requireContext().classLoader, OutgoingKeyRequestListFragment::class.java.name)
}
1 -> {
childFragmentManager.fragmentFactory.instantiate(requireContext().classLoader, IncomingKeyRequestListFragment::class.java.name)
}
else -> {
childFragmentManager.fragmentFactory.instantiate(requireContext().classLoader, GossipingEventsPaperTrailFragment::class.java.name)
} }
} }
}
}
} }

View File

@ -17,6 +17,7 @@
package im.vector.riotx.features.settings.devtools package im.vector.riotx.features.settings.devtools
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import android.view.View import android.view.View
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
@ -28,14 +29,13 @@ import im.vector.riotx.core.resources.ColorProvider
import kotlinx.android.synthetic.main.fragment_generic_recycler.* import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import javax.inject.Inject import javax.inject.Inject
class KeyRequestListFragment @Inject constructor( class OutgoingKeyRequestListFragment @Inject constructor(
val viewModelFactory: KeyRequestListViewModel.Factory, val viewModelFactory: KeyRequestListViewModel.Factory,
private val epoxyController: KeyRequestEpoxyController, private val epoxyController: KeyRequestEpoxyController,
private val colorProvider: ColorProvider private val colorProvider: ColorProvider
) : VectorBaseFragment() { ) : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_generic_recycler override fun getLayoutResId() = R.layout.fragment_generic_recycler
private val viewModel: KeyRequestListViewModel by fragmentViewModel(KeyRequestListViewModel::class) private val viewModel: KeyRequestListViewModel by fragmentViewModel(KeyRequestListViewModel::class)
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
@ -53,4 +53,5 @@ class KeyRequestListFragment @Inject constructor(
recyclerView.cleanup() recyclerView.cleanup()
// epoxyController.interactionListener = null // epoxyController.interactionListener = null
} }
} }

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/refresh"
android:icon="@drawable/ic_refresh_cw"
app:iconTint="?riotx_text_primary"
android:title="@string/refresh"
app:showAsAction="collapseActionView|ifRoom" />
</menu>

View File

@ -12,6 +12,8 @@
<string name="e2e_use_keybackup">Unlock encrypted messages history</string> <string name="e2e_use_keybackup">Unlock encrypted messages history</string>
<string name="refresh">Refresh</string>
<!-- END Strings added by Valere --> <!-- END Strings added by Valere -->