From fc6225a7acfeb790f48f23822b725afa6cfa2a65 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 6 Mar 2020 18:29:15 +0100 Subject: [PATCH] Gossiping refactoring --- matrix-sdk-android/build.gradle | 3 +- .../crypto/gossiping/KeyShareTests.kt | 210 +++++++++ .../crypto/keysbackup/KeysBackupTest.kt | 85 ++-- .../crypto/verification/qrcode/QrCodeTest.kt | 1 + .../android/api/crypto/MXCryptoConfig.kt | 11 +- .../matrix/android/api/extensions/Try.kt | 25 + .../api/session/crypto/CryptoService.kt | 5 + .../crosssigning/CrossSigningService.kt | 3 + .../SharedSecretStorageService.kt | 3 + .../crypto/CancelGossipRequestWorker.kt | 119 +++++ .../internal/crypto/DefaultCryptoService.kt | 147 +++++- .../internal/crypto/GossipingRequestState.kt | 43 ++ .../crypto/IncomingRequestCancellation.kt | 6 +- .../internal/crypto/IncomingRoomKeyRequest.kt | 8 +- .../crypto/IncomingRoomKeyRequestManager.kt | 129 ++++-- .../crypto/IncomingSecretShareRequest.kt | 7 +- .../crypto/IncomingShareRequestCommon.kt | 2 + ...Request.kt => OutgoingGossipingRequest.kt} | 8 +- .../crypto/OutgoingGossipingRequestManager.kt | 185 ++++++++ .../internal/crypto/OutgoingRoomKeyRequest.kt | 17 +- .../crypto/OutgoingRoomKeyRequestManager.kt | 320 ------------- .../internal/crypto/OutgoingSecretRequest.kt | 10 +- .../crypto/SendGossipRequestWorker.kt | 150 ++++++ .../crypto/ShareSecretCryptoProvider.kt | 10 + .../actions/MegolmSessionDataImporter.kt | 6 +- .../crypto/algorithms/IMXDecrypting.kt | 2 + .../algorithms/megolm/MXMegolmDecryption.kt | 41 +- .../megolm/MXMegolmDecryptionFactory.kt | 6 +- .../crypto/algorithms/olm/MXOlmDecryption.kt | 4 + .../DefaultCrossSigningService.kt | 253 ++++++---- .../internal/crypto/model/CryptoDeviceInfo.kt | 2 +- .../model/rest/GossipingToDeviceObject.kt | 2 +- .../crypto/model/rest/RoomKeyRequestBody.kt | 15 +- .../crypto/model/rest/RoomKeyShareRequest.kt | 2 +- .../DefaultSharedSecretStorageService.kt | 11 + .../internal/crypto/store/IMXCryptoStore.kt | 54 ++- .../internal/crypto/store/db/Helper.kt | 7 +- .../crypto/store/db/RealmCryptoStore.kt | 432 +++++++++++++----- .../store/db/RealmCryptoStoreMigration.kt | 46 +- .../crypto/store/db/RealmCryptoStoreModule.kt | 18 +- .../store/db/model/GossipingEventEntity.kt | 88 ++++ .../model/IncomingGossipingRequestEntity.kt | 90 ++++ .../db/model/IncomingRoomKeyRequestEntity.kt | 112 ++--- .../db/model/IncomingSecretRequestEntity.kt | 74 +-- .../model/OutgoingGossipingRequestEntity.kt | 107 +++++ .../db/model/OutgoingRoomKeyRequestEntity.kt | 154 +++---- .../db/model/OutgoingSecretRequestEntity.kt | 126 ++--- .../tasks/RoomVerificationUpdateTask.kt | 1 - .../DefaultQrCodeVerificationTransaction.kt | 1 + .../internal/session/SessionComponent.kt | 5 + vector/build.gradle | 3 +- .../im/vector/riotx/core/di/FragmentModule.kt | 21 +- .../GossipingEventsEpoxyController.kt | 203 ++++++++ .../GossipingEventsPaperTrailFragment.kt | 68 +++ .../GossipingEventsPaperTrailViewModel.kt | 79 ++++ .../IncomingKeyRequestListFragment.kt | 57 +++ .../devtools/KeyRequestEpoxyController.kt | 78 +++- .../devtools/KeyRequestListViewModel.kt | 33 +- .../settings/devtools/KeyRequestsFragment.kt | 70 ++- ...t.kt => OutgoingKeyRequestListFragment.kt} | 5 +- .../main/res/menu/menu_common_gossiping.xml | 10 + vector/src/main/res/values/strings_riotX.xml | 2 + 62 files changed, 2811 insertions(+), 984 deletions(-) create mode 100644 matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/Try.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CancelGossipRequestWorker.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/GossipingRequestState.kt rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/{OutgoingShareRequest.kt => OutgoingGossipingRequest.kt} (81%) create mode 100755 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt delete mode 100755 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/SendGossipRequestWorker.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/GossipingEventEntity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsEpoxyController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsPaperTrailFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/devtools/IncomingKeyRequestListFragment.kt rename vector/src/main/java/im/vector/riotx/features/settings/devtools/{KeyRequestListFragment.kt => OutgoingKeyRequestListFragment.kt} (95%) create mode 100644 vector/src/main/res/menu/menu_common_gossiping.xml diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index f980279a8d..97a1d977cc 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -97,6 +97,7 @@ dependencies { def coroutines_version = "1.3.2" def markwon_version = '3.1.0' def daggerVersion = '2.25.4' + def work_version = '2.3.2' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" @@ -126,7 +127,7 @@ dependencies { kapt 'dk.ilios:realmfieldnameshelper:1.1.1' // Work - implementation "androidx.work:work-runtime-ktx:2.3.3" + implementation "androidx.work:work-runtime-ktx:$work_version" // FP implementation "io.arrow-kt:arrow-core:$arrow_version" diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt new file mode 100644 index 0000000000..fca406ac31 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt @@ -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 { + 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()?.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) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt index e599eda7f9..d584482774 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupTest.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.crypto.keysbackup +import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 import im.vector.matrix.android.InstrumentedTest 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.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP 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.keysbackup.model.KeysBackupVersionTrust 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.model.ImportRoomKeysResult 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.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Assert.fail +import org.junit.BeforeClass import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -325,46 +330,46 @@ class KeysBackupTest : InstrumentedTest { * - Restore must be successful * - *** There must be no more pending key share requests */ - @Test - fun restoreKeysBackupAndKeyShareRequestTest() { - fail("Check with Valere for this test. I think we do not send key share request") - - val testData = createKeysBackupScenarioWithPassword(null) - - // - Check the SDK sent key share requests - val cryptoStore2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store - val unsentRequest = cryptoStore2 - .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT)) - val sentRequest = cryptoStore2 - .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT)) - - // Request is either sent or unsent - assertTrue(unsentRequest != null || sentRequest != null) - - // - Restore the e2e backup from the homeserver - val importRoomKeysResult = mTestHelper.doSync { - testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, - testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, - null, - null, - null, - it - ) - } - - checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) - - // - There must be no more pending key share requests - val unsentRequestAfterRestoration = cryptoStore2 - .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT)) - val sentRequestAfterRestoration = cryptoStore2 - .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT)) - - // Request is either sent or unsent - assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null) - - testData.cleanUp(mTestHelper) - } +// @Test +// fun restoreKeysBackupAndKeyShareRequestTest() { +// fail("Check with Valere for this test. I think we do not send key share request") +// +// val testData = createKeysBackupScenarioWithPassword(null) +// +// // - Check the SDK sent key share requests +// val cryptoStore2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store +// val unsentRequest = cryptoStore2 +// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT)) +// val sentRequest = cryptoStore2 +// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT)) +// +// // Request is either sent or unsent +// assertTrue(unsentRequest != null || sentRequest != null) +// +// // - Restore the e2e backup from the homeserver +// val importRoomKeysResult = mTestHelper.doSync { +// testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!, +// testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey, +// null, +// null, +// null, +// it +// ) +// } +// +// checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) +// +// // - There must be no more pending key share requests +// val unsentRequestAfterRestoration = cryptoStore2 +// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.UNSENT)) +// val sentRequestAfterRestoration = cryptoStore2 +// .getOutgoingRoomKeyRequestByState(setOf(ShareRequestState.SENT)) +// +// // Request is either sent or unsent +// assertTrue(unsentRequestAfterRestoration == null && sentRequestAfterRestoration == null) +// +// testData.cleanUp(mTestHelper) +// } /** * - Do an e2e backup to the homeserver with a recovery key diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt index d19fad4b59..ff1780865c 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.verification.qrcode import androidx.test.ext.junit.runners.AndroidJUnit4 import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64NoPadding import org.amshove.kluent.shouldBeNull import org.amshove.kluent.shouldEqual import org.amshove.kluent.shouldEqualTo diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/MXCryptoConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/MXCryptoConfig.kt index dc08023d99..a8d576bae9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/MXCryptoConfig.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/crypto/MXCryptoConfig.kt @@ -23,5 +23,14 @@ data class MXCryptoConfig( // Tell whether the encryption of the event content is enabled for the invited members. // 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". - 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 + ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/Try.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/Try.kt new file mode 100644 index 0000000000..3afcac08c1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/Try.kt @@ -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 tryThis(operation: () -> A): A? { + return try { + operation() + } catch (any: Throwable) { + null + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt index d6823d49cb..ab8417b542 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -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.events.model.Content 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.NewSessionListener import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest @@ -87,6 +88,8 @@ interface CryptoService { fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? + fun requestRoomKeyForEvent(event: Event) + fun reRequestRoomKeyForEvent(event: Event) fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) @@ -132,4 +135,6 @@ interface CryptoService { fun removeSessionListener(listener: NewSessionListener) fun getOutgoingRoomKeyRequest(): List + fun getIncomingRoomKeyRequest(): List + fun getGossipingEventsTrail(): List } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt index ff4745ef46..181508402d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt @@ -68,4 +68,7 @@ interface CrossSigningService { fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult + + fun onSecretSSKGossip(sskPrivateKey: String): Boolean + fun onSecretUSKGossip(uskPrivateKey: String): Boolean } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt index 596d8d3e5d..ad0c6f10fa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt @@ -111,6 +111,9 @@ interface SharedSecretStorageService { fun checkShouldBeAbleToAccessSecrets(secretNames: List, keyId: String?) : IntegrityResult + + fun requestSecret(name: String, myOtherDeviceId: String) + data class KeyRef( val keyId: String?, val keySpec: SsssKeySpec? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CancelGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CancelGossipRequestWorker.kt new file mode 100644 index 0000000000..a87dae3690 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CancelGossipRequestWorker.kt @@ -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> + ) { + 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(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() + 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) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index 2ea0a71c50..7b7490f233 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -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.session.crypto.CryptoService 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.events.model.Content 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.megolm.MXMegolmEncryptionFactory 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.DeviceTrustLevel 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.MXEncryptEventContentResult 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.SecretSendEventContent 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.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.sync.model.SyncResponse 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.JsonCanonicalizer import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers @@ -137,7 +143,7 @@ internal class DefaultCryptoService @Inject constructor( // private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager, // - private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, // Actions private val setDeviceVerificationAction: SetDeviceVerificationAction, private val megolmSessionDataImporter: MegolmSessionDataImporter, @@ -189,6 +195,7 @@ internal class DefaultCryptoService @Inject constructor( override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) { setDeviceNameTask .configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) { + this.executionThread = TaskThread.CRYPTO this.callback = object : MatrixCallback { override fun onSuccess(data: Unit) { // bg refresh of crypto device @@ -207,6 +214,7 @@ internal class DefaultCryptoService @Inject constructor( override fun deleteDevice(deviceId: String, callback: MatrixCallback) { deleteDeviceTask .configureWith(DeleteDeviceTask.Params(deviceId)) { + this.executionThread = TaskThread.CRYPTO this.callback = callback } .executeBy(taskExecutor) @@ -215,6 +223,7 @@ internal class DefaultCryptoService @Inject constructor( override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback) { deleteDeviceWithUserPasswordTask .configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password)) { + this.executionThread = TaskThread.CRYPTO this.callback = callback } .executeBy(taskExecutor) @@ -231,6 +240,7 @@ internal class DefaultCryptoService @Inject constructor( override fun getDevicesList(callback: MatrixCallback) { getDevicesTask .configureWith { + this.executionThread = TaskThread.CRYPTO this.callback = callback } .executeBy(taskExecutor) @@ -239,6 +249,7 @@ internal class DefaultCryptoService @Inject constructor( override fun getDeviceInfo(deviceId: String, callback: MatrixCallback) { getDeviceInfoTask .configureWith(GetDeviceInfoTask.Params(deviceId)) { + this.executionThread = TaskThread.CRYPTO this.callback = callback } .executeBy(taskExecutor) @@ -301,7 +312,6 @@ internal class DefaultCryptoService @Inject constructor( runCatching { uploadDeviceKeys() oneTimeKeysUploader.maybeUploadOneTimeKeys() - outgoingRoomKeyRequestManager.start() keysBackupService.checkAndStartKeysBackup() if (isInitialSync) { // refresh the devices list for each known room members @@ -329,8 +339,6 @@ internal class DefaultCryptoService @Inject constructor( fun close() = runBlocking(coroutineDispatchers.crypto) { cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) - outgoingRoomKeyRequestManager.stop() - olmDevice.release() cryptoStore.close() } @@ -689,18 +697,24 @@ internal class DefaultCryptoService @Inject constructor( * @param event the event */ fun onToDeviceEvent(event: Event) { + // event have already been decrypted cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { when (event.getClearType()) { EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> { + cryptoStore.saveGossipingEvent(event) + // Keys are imported directly, not waiting for end of sync onRoomKeyEvent(event) } - EventType.REQUEST_SECRET, 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) } EventType.SEND_SECRET -> { - // incomingRoomKeyRequestManager.onGossipingRequestEvent(event) + cryptoStore.saveGossipingEvent(event) + onSecretSendReceived(event) } else -> { // ignore @@ -716,6 +730,7 @@ internal class DefaultCryptoService @Inject constructor( */ private fun onRoomKeyEvent(event: Event) { val roomKeyContent = event.getClearContent().toModel() ?: return + Timber.v("## onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>") if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { Timber.e("## onRoomKeyEvent() : missing fields") return @@ -728,6 +743,78 @@ internal class DefaultCryptoService @Inject constructor( 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() + ?: 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() ?: 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. * @@ -1003,14 +1090,14 @@ internal class DefaultCryptoService @Inject constructor( 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 * * @param requestBody requestBody */ 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. */ override fun reRequestRoomKeyForEvent(event: Event) { - val wireContent = event.content - if (wireContent == null) { + val wireContent = event.content.toModel() ?: return Unit.also { Timber.e("## reRequestRoomKeyForEvent Failed to re-request key, null content") - return } val requestBody = RoomKeyRequestBody( - algorithm = wireContent["algorithm"]?.toString(), + algorithm = wireContent.algorithm, roomId = event.roomId, - senderKey = wireContent["sender_key"]?.toString(), - sessionId = wireContent["session_id"]?.toString() + senderKey = wireContent.senderKey, + sessionId = wireContent.sessionId ) - outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody) + outgoingGossipingRequestManager.resendRoomKeyRequest(requestBody) + } + + override fun requestRoomKeyForEvent(event: Event) { + val wireContent = event.content.toModel() ?: 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,9 +1193,9 @@ internal class DefaultCryptoService @Inject constructor( override fun removeSessionListener(listener: NewSessionListener) { roomDecryptorProvider.removeSessionListener(listener) } - /* ========================================================================================== - * DEBUG INFO - * ========================================================================================== */ +/* ========================================================================================== + * DEBUG INFO + * ========================================================================================== */ override fun toString(): String { return "DefaultCryptoService of " + credentials.userId + " (" + credentials.deviceId + ")" @@ -1101,4 +1204,12 @@ internal class DefaultCryptoService @Inject constructor( override fun getOutgoingRoomKeyRequest(): List { return cryptoStore.getOutgoingRoomKeyRequests() } + + override fun getIncomingRoomKeyRequest(): List { + return cryptoStore.getIncomingRoomKeyRequests() + } + + override fun getGossipingEventsTrail(): List { + return cryptoStore.getGossipingEventsTrail() + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/GossipingRequestState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/GossipingRequestState.kt new file mode 100644 index 0000000000..b218a2e387 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/GossipingRequestState.kt @@ -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 +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRequestCancellation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRequestCancellation.kt index 7751f149f1..98e1e95423 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRequestCancellation.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRequestCancellation.kt @@ -37,7 +37,8 @@ data class IncomingRequestCancellation( /** * The request id */ - override val requestId: String? = null + override val requestId: String? = null, + override val localCreationTimestamp: Long? ) : IncomingShareRequestCommon { companion object { /** @@ -52,7 +53,8 @@ data class IncomingRequestCancellation( IncomingRequestCancellation( userId = event.senderId, deviceId = it.requestingDeviceId, - requestId = it.requestId + requestId = it.requestId, + localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequest.kt index 8648041094..13f3d38677 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequest.kt @@ -46,6 +46,8 @@ data class IncomingRoomKeyRequest( */ val requestBody: RoomKeyRequestBody? = null, + val state: GossipingRequestState = GossipingRequestState.NONE, + /** * 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. */ @Transient - var ignore: Runnable? = null + var ignore: Runnable? = null, + override val localCreationTimestamp: Long? ) : IncomingShareRequestCommon { companion object { /** @@ -72,7 +75,8 @@ data class IncomingRoomKeyRequest( userId = event.senderId, deviceId = it.requestingDeviceId, requestId = it.requestId, - requestBody = it.body ?: RoomKeyRequestBody() + requestBody = it.body ?: RoomKeyRequestBody(), + localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt index 98c73d75f5..4fdd011aee 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt @@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.crypto 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.USER_SIGNING_KEY_SSSS_NAME import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener @@ -35,8 +35,8 @@ import javax.inject.Inject internal class IncomingRoomKeyRequestManager @Inject constructor( private val credentials: Credentials, private val cryptoStore: IMXCryptoStore, - private val crossSigningService: CrossSigningService, - private val secrSecretCryptoProvider: ShareSecretCryptoProvider, + private val cryptoConfig: MXCryptoConfig, + private val secretSecretCryptoProvider: ShareSecretCryptoProvider, private val roomDecryptorProvider: RoomDecryptorProvider) { // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations @@ -48,8 +48,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( private val gossipingRequestListeners: MutableSet = HashSet() init { - receiveGossipingRequests.addAll(cryptoStore.getPendingIncomingSecretShareRequests()) - receiveGossipingRequests.addAll(cryptoStore.getPendingIncomingRoomKeyRequests()) + receiveGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests()) } /** @@ -59,21 +58,37 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( * @param event the announcement event. */ fun onGossipingRequestEvent(event: Event) { + Timber.v("## onGossipingRequestEvent type ${event.type} from user ${event.senderId}") val roomKeyShare = event.getClearContent().toModel() + val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } when (roomKeyShare?.action) { GossipingToDeviceObject.ACTION_SHARE_REQUEST -> { if (event.getClearType() == EventType.REQUEST_SECRET) { IncomingSecretShareRequest.fromEvent(event)?.let { + // save in DB + cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) receiveGossipingRequests.add(it) } } else if (event.getClearType() == EventType.ROOM_KEY_REQUEST) { IncomingRoomKeyRequest.fromEvent(event)?.let { - receiveGossipingRequests.add(it) + 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) + } } } } - 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 */ fun processReceivedGossipingRequests() { + Timber.v("## processReceivedGossipingRequests()") + val roomKeyRequestsToProcess = receiveGossipingRequests.toList() receiveGossipingRequests.clear() for (request in roomKeyRequestsToProcess) { @@ -102,16 +119,25 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( } } - if (null != receivedRequestCancellations) { - for (request in receivedRequestCancellations!!) { - 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 - // about, but we don't currently have a record of that, so we just pass - // everything through. - onRoomKeyRequestCancellation(request) - cryptoStore.deleteIncomingRoomKeyRequest(request) + receivedRequestCancellations?.forEach { request -> + Timber.v("## processReceivedGossipingRequests() : m.room_key_request cancellation $request") + // 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 + // 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) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER) + } } } } @@ -123,11 +149,11 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( val roomId = body!!.roomId 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) { // TODO: determine if we sent this device the keys already: in Timber.w("## processReceivedGossipingRequests() : Ignoring room key request from other user for now") - cryptoStore.deleteIncomingRoomKeyRequest(request) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) return } // 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) if (null == decryptor) { Timber.w("## processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) return } if (!decryptor.hasKeysForKeyRequest(request)) { Timber.w("## processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}") - cryptoStore.deleteIncomingRoomKeyRequest(request) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) return } if (credentials.deviceId == deviceId && credentials.userId == userId) { Timber.v("## processReceivedGossipingRequests() : oneself device - ignored") - cryptoStore.deleteIncomingRoomKeyRequest(request) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) return } request.share = Runnable { decryptor.shareKeysWithDevice(request) - cryptoStore.deleteIncomingRoomKeyRequest(request) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) } request.ignore = Runnable { - cryptoStore.deleteIncomingRoomKeyRequest(request) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } // if the device is verified already, share the keys val device = cryptoStore.getUserDevice(userId, deviceId!!) if (device != null) { if (device.isVerified) { Timber.v("## processReceivedGossipingRequests() : device is already verified: sharing keys") - cryptoStore.deleteIncomingRoomKeyRequest(request) request.share?.run() return } if (device.isBlocked) { Timber.v("## processReceivedGossipingRequests() : device is blocked -> ignored") - cryptoStore.deleteIncomingRoomKeyRequest(request) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) return } } - // If cross signing is available on account we automatically discard untrust devices request - if (cryptoStore.getMyCrossSigningInfo() != null) { + // As per config we automatically discard untrusted devices request + if (cryptoConfig.discardRoomKeyRequestsFromUntrustedDevices) { + Timber.v("## processReceivedGossipingRequests() : discardRoomKeyRequestsFromUntrustedDevices") // 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 } - cryptoStore.storeIncomingRoomKeyRequest(request) - - // Legacy, pass to application layer to decide what to do + // Pass to application layer to decide what to do onRoomKeyRequest(request) } private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) { val secretName = request.secretName ?: return Unit.also { - cryptoStore.deleteIncomingSecretRequest(request) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) Timber.v("## processIncomingSecretShareRequest() : Missing secret name") } val userId = request.userId if (userId == null || credentials.userId != userId) { - Timber.e("## processIncomingSecretShareRequest() : Ignoring secret share request from other user") - cryptoStore.deleteIncomingRoomKeyRequest(request) + Timber.e("## processIncomingSecretShareRequest() : Ignoring secret share request from other users") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) 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) { SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user else -> null }?.let { secretValue -> // TODO check if locally trusted and not outdated - if (cryptoStore.getUserDevice(userId, request.deviceId ?: "")?.trustLevel?.isLocallyVerified() == true) { - secrSecretCryptoProvider.shareSecretWithDevice(request, secretValue) - cryptoStore.deleteIncomingRoomKeyRequest(request) + Timber.i("## processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted") + if (isDeviceLocallyVerified == true) { + secretSecretCryptoProvider.shareSecretWithDevice(request, secretValue) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) } return } request.ignore = Runnable { - cryptoStore.deleteIncomingRoomKeyRequest(request) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } request.share = { secretValue -> - secrSecretCryptoProvider.shareSecretWithDevice(request, secretValue) - cryptoStore.deleteIncomingRoomKeyRequest(request) + secretSecretCryptoProvider.shareSecretWithDevice(request, secretValue) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) } onShareRequest(request) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingSecretShareRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingSecretShareRequest.kt index 6deb5b4692..2fcd3e22d5 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingSecretShareRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingSecretShareRequest.kt @@ -54,7 +54,9 @@ data class IncomingSecretShareRequest( * The runnable to call to ignore the key share request. */ @Transient - var ignore: Runnable? = null + var ignore: Runnable? = null, + + override val localCreationTimestamp: Long? ) : IncomingShareRequestCommon { companion object { @@ -71,7 +73,8 @@ data class IncomingSecretShareRequest( userId = event.senderId, deviceId = it.requestingDeviceId, requestId = it.requestId, - secretName = it.secretName + secretName = it.secretName, + localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingShareRequestCommon.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingShareRequestCommon.kt index 8999f835c8..f39a0d80d1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingShareRequestCommon.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingShareRequestCommon.kt @@ -31,4 +31,6 @@ interface IncomingShareRequestCommon { * The request id */ val requestId: String? + + val localCreationTimestamp: Long? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingShareRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequest.kt similarity index 81% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingShareRequest.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequest.kt index b56ba22da6..30fc5fdb4a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingShareRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequest.kt @@ -16,10 +16,10 @@ package im.vector.matrix.android.internal.crypto -interface OutgoingShareRequest { - var recipients: List> +interface OutgoingGossipingRequest { + var recipients: Map> var requestId: String - var state: ShareRequestState + var state: OutgoingGossipingRequestState // transaction id for the cancellation, if any - var cancellationTxnId: String? + //var cancellationTxnId: String? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt new file mode 100755 index 0000000000..e7c5ef89c8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt @@ -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>) { + 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>) { + 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(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(WorkerParamsFactory.toData(params), true) + postWork(workRequest) + + if (resend) { + val reSendParams = SendGossipRequestWorker.Params( + sessionId = sessionId, + keyShareRequest = request.copy(requestId = LocalEcho.createLocalEchoId()) + ) + val reSendWorkRequest = createWork(WorkerParamsFactory.toData(reSendParams), true) + postWork(reSendWorkRequest) + } + } + + private inline fun createWork(data: Data, startChain: Boolean): OneTimeWorkRequest { + return workManagerProvider.matrixOneTimeWorkRequestBuilder() + .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) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequest.kt index 2b9f8003ee..7e359a677c 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequest.kt @@ -17,22 +17,27 @@ package im.vector.matrix.android.internal.crypto +import com.squareup.moshi.JsonClass import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody /** * Represents an outgoing room key request */ -class OutgoingRoomKeyRequest( +@JsonClass(generateAdapter = true) +data class OutgoingRoomKeyRequest( // RequestBody - var requestBody: RoomKeyRequestBody?, // list of recipients for the request - override var recipients: List>, // Unique id for this request. Used for both + var requestBody: RoomKeyRequestBody?, + // list of recipients for the request + override var recipients: Map>, + // Unique id for this request. Used for both // 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 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 + // override var cancellationTxnId: String? = null +) : OutgoingGossipingRequest { - // transaction id for the cancellation, if any - override var cancellationTxnId: String? = null /** * Used only for log. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt deleted file mode 100755 index 5102afef7c..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequestManager.kt +++ /dev/null @@ -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>) { - 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 { - 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 { - 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>, - transactionId: String?, - callback: MatrixCallback) { - val contentMap = MXUsersDevicesMap() - - 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") - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingSecretRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingSecretRequest.kt index a5305c1a25..1497796743 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingSecretRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingSecretRequest.kt @@ -16,21 +16,23 @@ package im.vector.matrix.android.internal.crypto +import com.squareup.moshi.JsonClass + /** * Represents an outgoing room key request */ +@JsonClass(generateAdapter = true) class OutgoingSecretRequest( // Secret Name - var secretName: String?, + val secretName: String?, // list of recipients for the request - override var recipients: List>, + override var recipients: Map>, // Unique id for this request. Used for both // 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 override var requestId: String, // current state of this request - override var state: ShareRequestState) : OutgoingShareRequest { + override var state: OutgoingGossipingRequestState) : OutgoingGossipingRequest { // transaction id for the cancellation, if any - override var cancellationTxnId: String? = null } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/SendGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/SendGossipRequestWorker.kt new file mode 100644 index 0000000000..cffd58cbf2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/SendGossipRequestWorker.kt @@ -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(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() + 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) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ShareSecretCryptoProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ShareSecretCryptoProvider.kt index e40caf4eed..78e587c0f1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ShareSecretCryptoProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ShareSecretCryptoProvider.kt @@ -16,8 +16,10 @@ 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.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.event.SecretSendEventContent 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import timber.log.Timber import javax.inject.Inject @@ -32,6 +35,7 @@ internal class ShareSecretCryptoProvider @Inject constructor( val messageEncrypter: MessageEncrypter, val sendToDeviceTask: SendToDeviceTask, val deviceListManager: DeviceListManager, + private val olmDecryptionFactory: MXOlmDecryptionFactory, val cryptoCoroutineScope: CoroutineScope, val cryptoStore: IMXCryptoStore, 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 ?: "") + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MegolmSessionDataImporter.kt index 6f41116b90..cac4659ae5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MegolmSessionDataImporter.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/actions/MegolmSessionDataImporter.kt @@ -20,7 +20,7 @@ import androidx.annotation.WorkerThread import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.internal.crypto.MXOlmDevice 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.model.ImportRoomKeysResult 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, private val roomDecryptorProvider: RoomDecryptorProvider, - private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val cryptoStore: IMXCryptoStore) { /** @@ -73,7 +73,7 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi sessionId = megolmSessionData.sessionId ) - outgoingRoomKeyRequestManager.cancelRoomKeyRequest(roomKeyRequestBody) + outgoingGossipingRequestManager.cancelRoomKeyRequest(roomKeyRequestBody) // Have another go at decrypting events sent with this session decrypting.onNewSession(megolmSessionData.senderKey!!, sessionId!!) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt index ff740fb1f9..e9176ad6d9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt @@ -68,4 +68,6 @@ internal interface IMXDecrypting { fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {} fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue : String) {} + + fun requestKeysForEvent(event: Event) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index c8a1d628d7..36b4e40957 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -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.MXOlmDevice 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.MessageEncrypter import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting @@ -46,7 +46,7 @@ import timber.log.Timber internal class MXMegolmDecryption(private val userId: String, private val olmDevice: MXOlmDevice, private val deviceListManager: DeviceListManager, - private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val messageEncrypter: MessageEncrypter, private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, private val cryptoStore: IMXCryptoStore, @@ -144,25 +144,26 @@ internal class MXMegolmDecryption(private val userId: String, * * @param event the event */ - private fun requestKeysForEvent(event: Event) { - val sender = event.senderId!! - val encryptedEventContent = event.content.toModel()!! + override fun requestKeysForEvent(event: Event) { + val sender = event.senderId ?: return + val encryptedEventContent = event.content.toModel() + val senderDevice = encryptedEventContent?.deviceId ?: return - val recipients = ArrayList>() - - val selfMap = HashMap() - // TODO Replace this hard coded keys (see OutgoingRoomKeyRequestManager) - selfMap["userId"] = userId - selfMap["deviceId"] = "*" - recipients.add(selfMap) - - if (sender != userId) { - val senderMap = HashMap() - senderMap["userId"] = sender - senderMap["deviceId"] = encryptedEventContent.deviceId!! - recipients.add(senderMap) + val recipients = if (event.senderId != userId) { + mapOf( + userId to listOf("*") + ) + } else { + // for the case where you share the key with a device that has a broken olm session + // The other user might Re-shares a megolm session key with devices if the key has already been + // sent to them. + mapOf( + userId to listOf("*"), + sender to listOf(senderDevice) + ) } + val requestBody = RoomKeyRequestBody( roomId = event.roomId, algorithm = encryptedEventContent.algorithm, @@ -170,7 +171,7 @@ internal class MXMegolmDecryption(private val userId: String, sessionId = encryptedEventContent.sessionId ) - outgoingRoomKeyRequestManager.sendRoomKeyRequest(requestBody, recipients) + outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients) } /** @@ -271,7 +272,7 @@ internal class MXMegolmDecryption(private val userId: String, senderKey = senderKey ) - outgoingRoomKeyRequestManager.cancelRoomKeyRequest(content) + outgoingGossipingRequestManager.cancelRoomKeyRequest(content) onNewSession(senderKey, roomKeyContent.sessionId) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt index 7cddd27779..e8044186d8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt @@ -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.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.MessageEncrypter import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore @@ -32,7 +32,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor( @UserId private val userId: String, private val olmDevice: MXOlmDevice, private val deviceListManager: DeviceListManager, - private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val messageEncrypter: MessageEncrypter, private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, private val cryptoStore: IMXCryptoStore, @@ -46,7 +46,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor( userId, olmDevice, deviceListManager, - outgoingRoomKeyRequestManager, + outgoingGossipingRequestManager, messageEncrypter, ensureOlmSessionsForDevicesAction, cryptoStore, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt index e0454aea0d..0a8ef3993b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt @@ -210,4 +210,8 @@ internal class MXOlmDecryption( return res["payload"] } + + override fun requestKeysForEvent(event: Event) { + // nop + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt index 6251f72418..661af7fb49 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -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.MXOlmDevice 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.KeyUsage 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.greenrobot.eventbus.EventBus import org.matrix.olm.OlmPkSigning import org.matrix.olm.OlmUtility @@ -62,7 +63,7 @@ internal class DefaultCrossSigningService @Inject constructor( private val taskExecutor: TaskExecutor, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope, - private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener { private var olmUtility: OlmUtility? = null @@ -299,6 +300,58 @@ internal class DefaultCrossSigningService @Inject constructor( 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?, uskKeyPrivateKey: String?, sskPrivateKey: String? @@ -549,97 +602,103 @@ internal class DefaultCrossSigningService @Inject constructor( } override fun trustUser(otherUserId: String, callback: MatrixCallback) { - Timber.d("## CrossSigning - Mark user $userId as trusted ") - // We should have this user keys - val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey() - if (otherMasterKeys == null) { - callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known")) - return - } - val myKeys = getUserCrossSigningKeys(userId) - if (myKeys == null) { - callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account")) - return - } - val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey - if (userPubKey == null || userPkSigning == null) { - callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey")) - return - } + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + Timber.d("## CrossSigning - Mark user $userId as trusted ") + // We should have this user keys + val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey() + if (otherMasterKeys == null) { + callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known")) + return@launch + } + val myKeys = getUserCrossSigningKeys(userId) + if (myKeys == null) { + callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account")) + return@launch + } + val userPubKey = myKeys.userKey()?.unpaddedBase64PublicKey + if (userPubKey == null || userPkSigning == null) { + callback.onFailure(Throwable("## CrossSigning - Cannot sign from this account, privateKeyUnknown $userPubKey")) + return@launch + } - // Sign the other MasterKey with our UserSigning key - val newSignature = JsonCanonicalizer.getCanonicalJson(Map::class.java, - otherMasterKeys.signalableJSONDictionary()).let { userPkSigning?.sign(it) } + // Sign the other MasterKey with our UserSigning key + val newSignature = JsonCanonicalizer.getCanonicalJson(Map::class.java, + otherMasterKeys.signalableJSONDictionary()).let { userPkSigning?.sign(it) } - if (newSignature == null) { - // race?? - callback.onFailure(Throwable("## CrossSigning - Failed to sign")) - return + if (newSignature == null) { + // race?? + callback.onFailure(Throwable("## CrossSigning - Failed to sign")) + return@launch + } + + cryptoStore.setUserKeysAsTrusted(otherUserId, true) + // TODO update local copy with new signature directly here? kind of local echo of trust? + + Timber.d("## CrossSigning - Upload signature of $userId MSK signed by USK") + val uploadQuery = UploadSignatureQueryBuilder() + .withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature)) + .build() + uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { + this.executionThread = TaskThread.CRYPTO + this.callback = callback + }.executeBy(taskExecutor) } - - cryptoStore.setUserKeysAsTrusted(otherUserId, true) - // TODO update local copy with new signature directly here? kind of local echo of trust? - - Timber.d("## CrossSigning - Upload signature of $userId MSK signed by USK") - val uploadQuery = UploadSignatureQueryBuilder() - .withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature)) - .build() - uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { - this.executionThread = TaskThread.CRYPTO - this.callback = callback - }.executeBy(taskExecutor) } override fun markMyMasterKeyAsTrusted() { - cryptoStore.markMyMasterKeyAsLocallyTrusted(true) - checkSelfTrust() + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoStore.markMyMasterKeyAsLocallyTrusted(true) + checkSelfTrust() + } } override fun trustDevice(deviceId: String, callback: MatrixCallback) { - // This device should be yours - val device = cryptoStore.getUserDevice(userId, deviceId) - if (device == null) { - callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours")) - return + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + // This device should be yours + val device = cryptoStore.getUserDevice(userId, deviceId) + if (device == null) { + callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours")) + return@launch + } + + val myKeys = getUserCrossSigningKeys(userId) + if (myKeys == null) { + callback.onFailure(Throwable("CrossSigning is not setup for this account")) + return@launch + } + + val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey + if (ssPubKey == null || selfSigningPkSigning == null) { + callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey")) + return@launch + } + + // Sign with self signing + val newSignature = selfSigningPkSigning?.sign(device.canonicalSignable()) + + if (newSignature == null) { + // race?? + callback.onFailure(Throwable("Failed to sign")) + return@launch + } + val toUpload = device.copy( + signatures = mapOf( + userId + to + mapOf( + "ed25519:$ssPubKey" to newSignature + ) + ) + ) + + val uploadQuery = UploadSignatureQueryBuilder() + .withDeviceInfo(toUpload) + .build() + uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { + this.executionThread = TaskThread.CRYPTO + this.callback = callback + }.executeBy(taskExecutor) } - - val myKeys = getUserCrossSigningKeys(userId) - if (myKeys == null) { - callback.onFailure(Throwable("CrossSigning is not setup for this account")) - return - } - - val ssPubKey = myKeys.selfSigningKey()?.unpaddedBase64PublicKey - if (ssPubKey == null || selfSigningPkSigning == null) { - callback.onFailure(Throwable("Cannot sign from this account, public and/or privateKey Unknown $ssPubKey")) - return - } - - // Sign with self signing - val newSignature = selfSigningPkSigning?.sign(device.canonicalSignable()) - - if (newSignature == null) { - // race?? - callback.onFailure(Throwable("Failed to sign")) - return - } - val toUpload = device.copy( - signatures = mapOf( - userId - to - mapOf( - "ed25519:$ssPubKey" to newSignature - ) - ) - ) - - val uploadQuery = UploadSignatureQueryBuilder() - .withDeviceInfo(toUpload) - .build() - uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { - this.executionThread = TaskThread.CRYPTO - this.callback = callback - }.executeBy(taskExecutor) } override fun checkDeviceTrust(otherUserId: String, otherDeviceId: String, locallyTrusted: Boolean?): DeviceTrustResult { @@ -738,7 +797,7 @@ internal class DefaultCrossSigningService @Inject constructor( // If it's me, recheck trust of all users and devices? val users = ArrayList() if (otherUserId == userId && currentTrust != trusted) { - reRequestAllPendingRoomKeyRequest() +// reRequestAllPendingRoomKeyRequest() cryptoStore.updateUsersTrust { users.add(it) checkUserTrust(it).isVerified() @@ -755,16 +814,18 @@ internal class DefaultCrossSigningService @Inject constructor( } } - private fun reRequestAllPendingRoomKeyRequest() { - Timber.d("## CrossSigning - reRequest pending outgoing room key requests") - cryptoStore.getOutgoingRoomKeyRequests().forEach { - it.requestBody?.let { requestBody -> - if (cryptoStore.getInboundGroupSession(requestBody.sessionId ?: "", requestBody.senderKey ?: "") == null) { - outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody) - } else { - outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody) - } - } - } - } +// private fun reRequestAllPendingRoomKeyRequest() { +// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { +// Timber.d("## CrossSigning - reRequest pending outgoing room key requests") +// cryptoStore.getOutgoingRoomKeyRequests().forEach { +// it.requestBody?.let { requestBody -> +// if (cryptoStore.getInboundGroupSession(requestBody.sessionId ?: "", requestBody.senderKey ?: "") == null) { +// outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody) +// } else { +// outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody) +// } +// } +// } +// } +// } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt index e3e8f3de27..b124f7590e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt @@ -33,7 +33,7 @@ data class CryptoDeviceInfo( ) : CryptoInfo { val isVerified: Boolean - get() = trustLevel?.isVerified() ?: false + get() = trustLevel?.isVerified() == true val isUnknown: Boolean get() = trustLevel == null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/GossipingToDeviceObject.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/GossipingToDeviceObject.kt index def8c7f620..deaccdef16 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/GossipingToDeviceObject.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/GossipingToDeviceObject.kt @@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass * Interface representing an room key action request * Note: this class cannot be abstract because of [org.matrix.androidsdk.core.JsonUtils.toRoomKeyShare] */ -internal interface GossipingToDeviceObject : SendToDeviceObject { +interface GossipingToDeviceObject : SendToDeviceObject { val action: String? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt index 3eb6600e5e..5def4ae2a3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.model.rest import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.di.MoshiProvider /** * Class representing an room key request body content @@ -35,4 +36,16 @@ data class RoomKeyRequestBody( @Json(name = "session_id") 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) } + } + } +} + + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt index e61743251c..c2fc6fe96b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt @@ -23,7 +23,7 @@ import com.squareup.moshi.JsonClass * Class representing a room key request content */ @JsonClass(generateAdapter = true) -internal data class RoomKeyShareRequest( +data class RoomKeyShareRequest( @Json(name = "action") override val action: String? = GossipingToDeviceObject.ACTION_SHARE_REQUEST, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt index 649f60887d..1bd55dd35d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt @@ -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.SsssKeySpec 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_CURVE25519_AES_SHA2 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.tools.HkdfSha256 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.util.MatrixCoroutineDispatchers import kotlinx.coroutines.CoroutineScope @@ -55,7 +57,9 @@ import javax.inject.Inject import kotlin.experimental.and internal class DefaultSharedSecretStorageService @Inject constructor( + @UserId private val userId: String, private val accountDataService: AccountDataService, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope ) : SharedSecretStorageService { @@ -429,4 +433,11 @@ internal class DefaultSharedSecretStorageService @Inject constructor( return IntegrityResult.Success(keyInfo.content.passphrase != null) } + + override fun requestSecret(name: String, myOtherDeviceId: String) { + outgoingGossipingRequestManager.sendSecretShareRequest( + name, + mapOf(userId to listOf(myOtherDeviceId)) + ) + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt index b05b6cfa59..c5d09bc111 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt @@ -19,13 +19,15 @@ package im.vector.matrix.android.internal.crypto.store import androidx.lifecycle.LiveData 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.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.IncomingSecretShareRequest +import im.vector.matrix.android.internal.crypto.IncomingShareRequestCommon 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.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.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper @@ -118,7 +120,10 @@ internal interface IMXCryptoStore { * @return the pending IncomingRoomKeyRequest requests */ fun getPendingIncomingRoomKeyRequests(): List - fun getPendingIncomingSecretShareRequests(): List + + fun getPendingIncomingGossipingRequests(): List + fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) +// fun getPendingIncomingSecretShareRequests(): List /** * Indicate if the store contains data for the passed account. @@ -190,8 +195,8 @@ internal interface IMXCryptoStore { fun storeUserDevices(userId: String, devices: Map?) fun storeUserCrossSigningKeys(userId: String, masterKey: CryptoCrossSigningKey?, - selfSigningKey: CryptoCrossSigningKey?, - userSigningKey: CryptoCrossSigningKey?) + selfSigningKey: CryptoCrossSigningKey?, + userSigningKey: CryptoCrossSigningKey?) /** * Retrieve the known devices for a user. @@ -209,6 +214,7 @@ internal interface IMXCryptoStore { // TODO temp fun getLiveDeviceList(): LiveData> + /** * Store the crypto algorithm for a room. * @@ -350,45 +356,49 @@ internal interface IMXCryptoStore { * @param request the request * @return either the same instance as passed in, or the existing one. */ - fun getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): OutgoingRoomKeyRequest? + fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>): OutgoingRoomKeyRequest? + fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map>): OutgoingSecretRequest? + + fun saveGossipingEvent(event: Event) /** * Look for room key requests by state. * * @param states the states * @return an OutgoingRoomKeyRequest or null */ - fun getOutgoingRoomKeyRequestByState(states: Set): OutgoingRoomKeyRequest? +// fun getOutgoingRoomKeyRequestByState(states: Set): OutgoingRoomKeyRequest? +// fun getOutgoingSecretShareRequestByState(states: Set): OutgoingSecretRequest? /** * Update an existing outgoing request. * * @param request the request */ - fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) +// fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) /** * Delete an outgoing room key request. * * @param transactionId the transaction id. */ - fun deleteOutgoingRoomKeyRequest(transactionId: String) +// fun deleteOutgoingRoomKeyRequest(transactionId: String) /** * Store an incomingRoomKeyRequest instance * * @param incomingRoomKeyRequest the incoming key request */ - fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) +// fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) /** * Delete an incomingRoomKeyRequest instance * * @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 @@ -400,6 +410,8 @@ internal interface IMXCryptoStore { */ fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? + fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) + fun addNewSessionListener(listener: NewSessionListener) fun removeSessionListener(listener: NewSessionListener) @@ -411,20 +423,21 @@ internal interface IMXCryptoStore { /** * Gets the current crosssigning info */ - fun getMyCrossSigningInfo() : MXCrossSigningInfo? + fun getMyCrossSigningInfo(): MXCrossSigningInfo? + fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) - fun getCrossSigningInfo(userId: String) : MXCrossSigningInfo? - fun getLiveCrossSigningInfo(userId: String) : LiveData> + fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? + fun getLiveCrossSigningInfo(userId: String): LiveData> fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) - fun getCrossSigningPrivateKeys() : PrivateKeysInfo? + fun getCrossSigningPrivateKeys(): PrivateKeysInfo? 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() @@ -432,5 +445,8 @@ internal interface IMXCryptoStore { // Dev tools - fun getOutgoingRoomKeyRequests() : List + fun getOutgoingRoomKeyRequests(): List + fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? + fun getIncomingRoomKeyRequests(): List + fun getGossipingEventsTrail(): List } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt index 81988fe209..642c466e42 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt @@ -59,7 +59,12 @@ fun doRealmQueryAndCopyList(realmConfiguration: RealmConfigura */ fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) { 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) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt index 1b4410d48c..83b8459d72 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt @@ -21,14 +21,21 @@ import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy 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.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.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.IncomingSecretShareRequest import im.vector.matrix.android.internal.crypto.IncomingShareRequestCommon 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.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.model.CryptoCrossSigningKey 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.DeviceInfoEntity 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.IncomingRoomKeyRequestEntityFields -import im.vector.matrix.android.internal.crypto.store.db.model.IncomingSecretRequestEntity -import im.vector.matrix.android.internal.crypto.store.db.model.IncomingSecretRequestEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntity +import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntity +import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields 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.OlmInboundGroupSessionEntity 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.OlmSessionEntityFields -import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingRoomKeyRequestEntity -import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingRoomKeyRequestEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity +import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity 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.getById 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.MoshiProvider import im.vector.matrix.android.internal.session.SessionScope import io.realm.Realm import io.realm.RealmConfiguration @@ -801,152 +809,334 @@ internal class RealmCryptoStore @Inject constructor( } override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? { - return doRealmQueryAndCopy(realmConfiguration) { - it.where() - .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_ALGORITHM, requestBody.algorithm) - .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_ROOM_ID, requestBody.roomId) - .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_SENDER_KEY, requestBody.senderKey) - .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_SESSION_ID, requestBody.sessionId) - .findFirst() + return monarchy.fetchAllCopiedSync { realm -> + realm.where() + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + }.mapNotNull { + it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + }.firstOrNull { + 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? { - if (request.requestBody == null) { - return null - } + override fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? { +// return monarchy.fetchAllCopiedSync { realm -> +//// realm.where() +//// .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) +//// .equalTo(GossipingEventEntityFields.SENDER, credentials.userId) +//// }.mapNotNull { +//// ContentMapper.map(it.content)?.toModel() +//// }.firstOrNull { +//// it.secretName == secretName +//// } + TODO("not implemented") + } - val existingOne = getOutgoingRoomKeyRequest(request.requestBody!!) - - if (existingOne != null) { - return existingOne + override fun getIncomingRoomKeyRequests(): List { + return monarchy.fetchAllCopiedSync { realm -> + realm.where() + .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + }.mapNotNull { + it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest } + } + + override fun getGossipingEventsTrail(): List { + return monarchy.fetchAllCopiedSync { realm -> + realm.where() + }.map { + it.toModel() + } + } + + override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>): OutgoingRoomKeyRequest? { + // Insert the request and return the one passed in parameter + var request: OutgoingRoomKeyRequest? = null + doRealmTransaction(realmConfiguration) { realm -> + + val existing = realm.where() + .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>): OutgoingSecretRequest? { + + var request: OutgoingSecretRequest? = null // Insert the request and return the one passed in parameter - doRealmTransaction(realmConfiguration) { - it.createObject(OutgoingRoomKeyRequestEntity::class.java, request.requestId).apply { - putRequestBody(request.requestBody) - putRecipients(request.recipients) - cancellationTxnId = request.cancellationTxnId - state = request.state.ordinal + doRealmTransaction(realmConfiguration) { realm -> + val existing = realm.where() + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) + .equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName) + .findAll() + .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 } - override fun getOutgoingRoomKeyRequestByState(states: Set): OutgoingRoomKeyRequest? { - val statesIndex = states.map { it.ordinal }.toTypedArray() - return doRealmQueryAndCopy(realmConfiguration) { - it.where() - .`in`(OutgoingRoomKeyRequestEntityFields.STATE, statesIndex) - .findFirst() + override fun saveGossipingEvent(event: Event) { + val now = System.currentTimeMillis() + val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now + val entity = GossipingEventEntity( + type = event.type, + sender = event.senderId, + ageLocalTs = ageLocalTs, + content = ContentMapper.map(event.content) + ).apply { + sendState = SendState.SYNCED + decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult) + decryptionErrorCode = event.mCryptoError?.name } - ?.toOutgoingRoomKeyRequest() - } - - 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) + doRealmTransaction(realmConfiguration) { realm -> + realm.insertOrUpdate(entity) } } - override fun deleteOutgoingRoomKeyRequest(transactionId: String) { - doRealmTransaction(realmConfiguration) { - it.where() - .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_ID, transactionId) - .findFirst() - ?.deleteFromRealm() +// override fun getOutgoingRoomKeyRequestByState(states: Set): OutgoingRoomKeyRequest? { +// val statesIndex = states.map { it.ordinal }.toTypedArray() +// return doRealmQueryAndCopy(realmConfiguration) { realm -> +// realm.where() +// .equalTo(GossipingEventEntityFields.SENDER, credentials.userId) +// .findAll() +// .filter {entity -> +// states.any { it == entity.requestState} +// } +// }.mapNotNull { +// ContentMapper.map(it.content)?.toModel() +// } +// ?.toOutgoingRoomKeyRequest() +// } +// +// override fun getOutgoingSecretShareRequestByState(states: Set): OutgoingSecretRequest? { +// val statesIndex = states.map { it.ordinal }.toTypedArray() +// return doRealmQueryAndCopy(realmConfiguration) { +// it.where() +// .`in`(OutgoingSecretRequestEntityFields.STATE, statesIndex) +// .findFirst() +// } +// ?.toOutgoingSecretRequest() +// } + +// 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) { +// doRealmTransaction(realmConfiguration) { +// it.where() +// .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_ID, transactionId) +// .findFirst() +// ?.deleteFromRealm() +// } +// } + +// override fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) { +// if (incomingRoomKeyRequest == null) { +// return +// } +// +// doRealmTransaction(realmConfiguration) { +// // Delete any previous store request with the same parameters +// it.where() +// .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() +// .equalTo(GossipingEventEntityFields.TYPE, EventType.ROOM_KEY_REQUEST) +// .notEqualTo(GossipingEventEntityFields.SENDER, credentials.userId) +// .findAll() +// .filter { +// ContentMapper.map(it.content).toModel()?.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() + .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 storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) { - if (incomingRoomKeyRequest == null) { - return - } - - doRealmTransaction(realmConfiguration) { - // Delete any previous store request with the same parameters - it.where() - .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() - .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId) - .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId) - .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId) - .findAll() - .deleteAllFromRealm() - } - } - - override fun deleteIncomingSecretRequest(request: IncomingSecretShareRequest) { - doRealmTransaction(realmConfiguration) { - it.where() - .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 updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where() + .equalTo(OutgoingGossipingRequestEntityFields.REQUEST_ID, requestId) + .findAll().forEach { + it.requestState = state + } } } override fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? { - return doRealmQueryAndCopy(realmConfiguration) { - it.where() - .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, userId) - .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, deviceId) - .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, requestId) - .findFirst() - } - ?.toIncomingRoomKeyRequest() + return doRealmQueryAndCopyList(realmConfiguration) { realm -> + realm.where() + .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, deviceId) + .equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, userId) + .findAll() + }.mapNotNull { entity -> + entity.toIncomingGossipingRequest() as? IncomingRoomKeyRequest + }.firstOrNull() } - override fun getPendingIncomingRoomKeyRequests(): MutableList { + override fun getPendingIncomingRoomKeyRequests(): List { return doRealmQueryAndCopyList(realmConfiguration) { - it.where() + it.where() + .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name) .findAll() } - .map { - it.toIncomingRoomKeyRequest() + .map { entity -> + IncomingRoomKeyRequest( + userId = entity.otherUserId, + deviceId = entity.otherDeviceId, + requestId = entity.requestId, + requestBody = entity.getRequestedKeyInfo(), + localCreationTimestamp = entity.localCreationTimestamp + ) } - .toMutableList() } - override fun getPendingIncomingSecretShareRequests(): List { + override fun getPendingIncomingGossipingRequests(): List { return doRealmQueryAndCopyList(realmConfiguration) { - it.where() + it.where() + .equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name) .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 { +// return doRealmQueryAndCopyList(realmConfiguration) { +// it.where() +// .findAll() +// }.map { +// it.toIncomingSecretShareRequest() +// } +// } + /* ========================================================================================== * Cross Signing * ========================================================================================== */ @@ -1051,10 +1241,12 @@ internal class RealmCryptoStore @Inject constructor( override fun getOutgoingRoomKeyRequests(): List { return monarchy.fetchAllMappedSync({ realm -> - realm.where(OutgoingRoomKeyRequestEntity::class.java) - }, { - it.toOutgoingRoomKeyRequest() - }) + realm + .where(OutgoingGossipingRequestEntity::class.java) + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + }, { entity -> + entity.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + }).filterNotNull() } override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt index 6d10e9d8aa..80173c050c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -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.CryptoMetadataEntityFields 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.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.UserEntityFields import im.vector.matrix.android.internal.di.SerializeNulls @@ -140,21 +141,38 @@ internal object RealmCryptoStoreMigration : RealmMigration { private fun migrateTo2(realm: DynamicRealm) { Timber.d("Step 1 -> 2") + realm.schema.remove("OutgoingRoomKeyRequestEntity") + realm.schema.remove("IncomingRoomKeyRequestEntity") - realm.schema.create("IncomingSecretRequestEntity") - .addField(IncomingSecretRequestEntityFields.DEVICE_ID, String::class.java) - .addField(IncomingSecretRequestEntityFields.SECRET_NAME, String::class.java) - .addField(IncomingSecretRequestEntityFields.REQUEST_ID, String::class.java) - .addField(IncomingSecretRequestEntityFields.USER_ID, String::class.java) + //Not need to migrate existing request, just start fresh? + + realm.schema.create("GossipingEventEntity") + .addField(GossipingEventEntityFields.TYPE, 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") - .addField(OutgoingSecretRequestEntityFields.REQUEST_ID, String::class.java) - .addPrimaryKey(OutgoingSecretRequestEntityFields.REQUEST_ID) - .addField(OutgoingSecretRequestEntityFields.SECRET_NAME, String::class.java) - .addField(OutgoingSecretRequestEntityFields.CANCELLATION_TXN_ID, String::class.java) - .addField(OutgoingSecretRequestEntityFields.RECIPIENTS_DATA, String::class.java) - .addField(OutgoingSecretRequestEntityFields.STATE, Int::class.java) + realm.schema.create("IncomingGossipingRequestEntity") + .addField(IncomingGossipingRequestEntityFields.REQUEST_ID, String::class.java) + .addField(IncomingGossipingRequestEntityFields.TYPE_STR, String::class.java) + .addField(IncomingGossipingRequestEntityFields.OTHER_USER_ID, String::class.java) + .addField(IncomingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java) + .addField(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, String::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) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt index d9d4496d4e..0d48f4671b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt @@ -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.CryptoRoomEntity 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.IncomingSecretRequestEntity +import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntity +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.KeysBackupDataEntity 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.OutgoingRoomKeyRequestEntity -import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingSecretRequestEntity +import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity import io.realm.annotations.RealmModule @@ -40,16 +39,19 @@ import io.realm.annotations.RealmModule CryptoMetadataEntity::class, CryptoRoomEntity::class, DeviceInfoEntity::class, - IncomingRoomKeyRequestEntity::class, +// IncomingRoomKeyRequestEntity::class, KeysBackupDataEntity::class, OlmInboundGroupSessionEntity::class, OlmSessionEntity::class, - OutgoingRoomKeyRequestEntity::class, +// OutgoingRoomKeyRequestEntity::class, UserEntity::class, KeyInfoEntity::class, CrossSigningInfoEntity::class, TrustLevelEntity::class, - IncomingSecretRequestEntity::class, - OutgoingSecretRequestEntity::class +// IncomingSecretRequestEntity::class, +// OutgoingSecretRequestEntity::class, + GossipingEventEntity::class, + IncomingGossipingRequestEntity::class, + OutgoingGossipingRequestEntity::class ]) internal class RealmCryptoStoreModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/GossipingEventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/GossipingEventEntity.kt new file mode 100644 index 0000000000..1c5c4d267b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/GossipingEventEntity.kt @@ -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::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) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt new file mode 100644 index 0000000000..1482a18d74 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt @@ -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 + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingRoomKeyRequestEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingRoomKeyRequestEntity.kt index 38cece99ac..ef3ba75f23 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingRoomKeyRequestEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingRoomKeyRequestEntity.kt @@ -1,56 +1,56 @@ -/* - * 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.store.db.model - -import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest -import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody -import io.realm.RealmObject - -internal open class IncomingRoomKeyRequestEntity( - var requestId: String? = null, - var userId: String? = null, - var deviceId: String? = null, - // RoomKeyRequestBody fields - var requestBodyAlgorithm: String? = null, - var requestBodyRoomId: String? = null, - var requestBodySenderKey: String? = null, - var requestBodySessionId: String? = null -) : RealmObject() { - - fun toIncomingRoomKeyRequest(): IncomingRoomKeyRequest { - return IncomingRoomKeyRequest( - requestId = requestId, - userId = userId, - deviceId = deviceId, - requestBody = RoomKeyRequestBody( - algorithm = requestBodyAlgorithm, - roomId = requestBodyRoomId, - senderKey = requestBodySenderKey, - sessionId = requestBodySessionId - ) - ) - } - - fun putRequestBody(requestBody: RoomKeyRequestBody?) { - requestBody?.let { - requestBodyAlgorithm = it.algorithm - requestBodyRoomId = it.roomId - requestBodySenderKey = it.senderKey - requestBodySessionId = it.sessionId - } - } -} +///* +// * 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.store.db.model +// +//import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest +//import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody +//import io.realm.RealmObject +// +//internal open class IncomingRoomKeyRequestEntity( +// var requestId: String? = null, +// var userId: String? = null, +// var deviceId: String? = null, +// // RoomKeyRequestBody fields +// var requestBodyAlgorithm: String? = null, +// var requestBodyRoomId: String? = null, +// var requestBodySenderKey: String? = null, +// var requestBodySessionId: String? = null +//) : RealmObject() { +// +// fun toIncomingRoomKeyRequest(): IncomingRoomKeyRequest { +// return IncomingRoomKeyRequest( +// requestId = requestId, +// userId = userId, +// deviceId = deviceId, +// requestBody = RoomKeyRequestBody( +// algorithm = requestBodyAlgorithm, +// roomId = requestBodyRoomId, +// senderKey = requestBodySenderKey, +// sessionId = requestBodySessionId +// ) +// ) +// } +// +// fun putRequestBody(requestBody: RoomKeyRequestBody?) { +// requestBody?.let { +// requestBodyAlgorithm = it.algorithm +// requestBodyRoomId = it.roomId +// requestBodySenderKey = it.senderKey +// requestBodySessionId = it.sessionId +// } +// } +//} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingSecretRequestEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingSecretRequestEntity.kt index e1eb274e3d..81095dfb52 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingSecretRequestEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingSecretRequestEntity.kt @@ -1,37 +1,37 @@ -/* - * Copyright (c) 2020 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.matrix.android.internal.crypto.store.db.model - -import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest -import io.realm.RealmObject - -internal open class IncomingSecretRequestEntity( - var requestId: String? = null, - var userId: String? = null, - var deviceId: String? = null, - var secretName: String? = null -) : RealmObject() { - - fun toIncomingSecretShareRequest(): IncomingSecretShareRequest { - return IncomingSecretShareRequest( - requestId = requestId, - userId = userId, - deviceId = deviceId, - secretName = secretName - ) - } -} +///* +// * Copyright (c) 2020 New Vector Ltd +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ +// +//package im.vector.matrix.android.internal.crypto.store.db.model +// +//import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest +//import io.realm.RealmObject +// +//internal open class IncomingSecretRequestEntity( +// var requestId: String? = null, +// var userId: String? = null, +// var deviceId: String? = null, +// var secretName: String? = null +//) : RealmObject() { +// +// fun toIncomingSecretShareRequest(): IncomingSecretShareRequest { +// return IncomingSecretShareRequest( +// requestId = requestId, +// userId = userId, +// deviceId = deviceId, +// secretName = secretName +// ) +// } +//} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt new file mode 100644 index 0000000000..e6be88790d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt @@ -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 = 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>> = + MoshiProvider.providesMoshi().adapter>>(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>? { + return this.recipientsData?.let { recipientsDataMapper.fromJson(it) } + } + + fun setRecipients(recipients: Map>) { + this.recipientsData = recipientsDataMapper.toJson(recipients) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingRoomKeyRequestEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingRoomKeyRequestEntity.kt index 7b4b515c85..9e1ac89bd3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingRoomKeyRequestEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingRoomKeyRequestEntity.kt @@ -1,77 +1,77 @@ -/* - * 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.store.db.model - -import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest -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.store.db.deserializeFromRealm -import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm -import io.realm.RealmObject -import io.realm.annotations.PrimaryKey - -internal open class OutgoingRoomKeyRequestEntity( - @PrimaryKey var requestId: String? = null, - var cancellationTxnId: String? = null, - // Serialized Json - var recipientsData: String? = null, - // RoomKeyRequestBody fields - var requestBodyAlgorithm: String? = null, - var requestBodyRoomId: String? = null, - var requestBodySenderKey: String? = null, - var requestBodySessionId: String? = null, - // State - var state: Int = 0 -) : RealmObject() { - - /** - * Convert to OutgoingRoomKeyRequest - */ - fun toOutgoingRoomKeyRequest(): OutgoingRoomKeyRequest { - val cancellationTxnId = this.cancellationTxnId - return OutgoingRoomKeyRequest( - RoomKeyRequestBody( - algorithm = requestBodyAlgorithm, - roomId = requestBodyRoomId, - senderKey = requestBodySenderKey, - sessionId = requestBodySessionId - ), - getRecipients()!!, - requestId!!, - ShareRequestState.from(state) - ).apply { - this.cancellationTxnId = cancellationTxnId - } - } - - private fun getRecipients(): List>? { - return deserializeFromRealm(recipientsData) - } - - fun putRecipients(recipients: List>?) { - recipientsData = serializeForRealm(recipients) - } - - fun putRequestBody(requestBody: RoomKeyRequestBody?) { - requestBody?.let { - requestBodyAlgorithm = it.algorithm - requestBodyRoomId = it.roomId - requestBodySenderKey = it.senderKey - requestBodySessionId = it.sessionId - } - } -} +///* +// * 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.store.db.model +// +//import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest +//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.store.db.deserializeFromRealm +//import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm +//import io.realm.RealmObject +//import io.realm.annotations.PrimaryKey +// +//internal open class OutgoingRoomKeyRequestEntity( +// @PrimaryKey var requestId: String? = null, +// var cancellationTxnId: String? = null, +// // Serialized Json +// var recipientsData: String? = null, +// // RoomKeyRequestBody fields +// var requestBodyAlgorithm: String? = null, +// var requestBodyRoomId: String? = null, +// var requestBodySenderKey: String? = null, +// var requestBodySessionId: String? = null, +// // State +// var state: Int = 0 +//) : RealmObject() { +// +// /** +// * Convert to OutgoingRoomKeyRequest +// */ +// fun toOutgoingRoomKeyRequest(): OutgoingRoomKeyRequest { +// val cancellationTxnId = this.cancellationTxnId +// return OutgoingRoomKeyRequest( +// RoomKeyRequestBody( +// algorithm = requestBodyAlgorithm, +// roomId = requestBodyRoomId, +// senderKey = requestBodySenderKey, +// sessionId = requestBodySessionId +// ), +// getRecipients()!!, +// requestId!!, +// ShareRequestState.from(state) +// ).apply { +// this.cancellationTxnId = cancellationTxnId +// } +// } +// +// private fun getRecipients(): List>? { +// return deserializeFromRealm(recipientsData) +// } +// +// fun putRecipients(recipients: List>?) { +// recipientsData = serializeForRealm(recipients) +// } +// +// fun putRequestBody(requestBody: RoomKeyRequestBody?) { +// requestBody?.let { +// requestBodyAlgorithm = it.algorithm +// requestBodyRoomId = it.roomId +// requestBodySenderKey = it.senderKey +// requestBodySessionId = it.sessionId +// } +// } +//} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingSecretRequestEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingSecretRequestEntity.kt index e6ce91fc10..4a4a2d8155 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingSecretRequestEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingSecretRequestEntity.kt @@ -1,63 +1,63 @@ -/* - * Copyright (c) 2020 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.matrix.android.internal.crypto.store.db.model - -import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest -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.serializeForRealm -import io.realm.RealmObject -import io.realm.annotations.PrimaryKey - -internal open class OutgoingSecretRequestEntity( - @PrimaryKey var requestId: String? = null, - var cancellationTxnId: String? = null, - // Serialized Json - var recipientsData: String? = null, - // RoomKeyRequestBody fields - var secretName: String? = null, - // State - var state: Int = 0 -) : RealmObject() { - - /** - * Convert to OutgoingRoomKeyRequest - */ - fun toOutgoingSecretRequest(): OutgoingSecretRequest { - val cancellationTxnId = this.cancellationTxnId - return OutgoingSecretRequest( - secretName, - getRecipients() ?: emptyList(), - requestId!!, - ShareRequestState.from(state) - ).apply { - this.cancellationTxnId = cancellationTxnId - } - } - - private fun getRecipients(): List>? { - return try { - deserializeFromRealm(recipientsData) - } catch (failure: Throwable) { - null - } - } - - fun putRecipients(recipients: List>?) { - recipientsData = serializeForRealm(recipients) - } -} +///* +// * Copyright (c) 2020 New Vector Ltd +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ +// +//package im.vector.matrix.android.internal.crypto.store.db.model +// +//import im.vector.matrix.android.internal.crypto.OutgoingSecretRequest +//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.serializeForRealm +//import io.realm.RealmObject +//import io.realm.annotations.PrimaryKey +// +//internal open class OutgoingSecretRequestEntity( +// @PrimaryKey var requestId: String? = null, +// var cancellationTxnId: String? = null, +// // Serialized Json +// var recipientsData: String? = null, +// // RoomKeyRequestBody fields +// var secretName: String? = null, +// // State +// var state: Int = 0 +//) : RealmObject() { +// +// /** +// * Convert to OutgoingRoomKeyRequest +// */ +// fun toOutgoingSecretRequest(): OutgoingSecretRequest { +// val cancellationTxnId = this.cancellationTxnId +// return OutgoingSecretRequest( +// secretName, +// getRecipients() ?: emptyList(), +// requestId!!, +// ShareRequestState.from(state) +// ).apply { +// this.cancellationTxnId = cancellationTxnId +// } +// } +// +// private fun getRecipients(): List>? { +// return try { +// deserializeFromRealm(recipientsData) +// } catch (failure: Throwable) { +// null +// } +// } +// +// fun putRecipients(recipients: List>?) { +// recipientsData = serializeForRealm(recipients) +// } +//} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt index 3fb2573bed..53ccb8b6f0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt @@ -61,7 +61,6 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor( params.events.forEach { event -> 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, // the message should be ignored by the receiver. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt index f56e261416..f6c9b3d50b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt @@ -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.events.model.EventType 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.fromBase64Safe import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index 1b07377fa1..2185d3b278 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -20,7 +20,9 @@ import dagger.BindsInstance import dagger.Component import im.vector.matrix.android.api.auth.data.SessionParams 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.SendGossipRequestWorker import im.vector.matrix.android.internal.crypto.verification.SendVerificationMessageWorker import im.vector.matrix.android.internal.di.MatrixComponent import im.vector.matrix.android.internal.di.SessionAssistedInjectModule @@ -106,6 +108,9 @@ internal interface SessionComponent { fun inject(worker: SendVerificationMessageWorker) + fun inject(worker: SendGossipRequestWorker) + fun inject(worker: CancelGossipRequestWorker) + @Component.Factory interface Factory { fun create( diff --git a/vector/build.gradle b/vector/build.gradle index 2aae593271..f26ad40279 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -249,6 +249,7 @@ dependencies { def moshi_version = '1.8.0' def daggerVersion = '2.25.4' def autofill_version = "1.0.0" + def work_version = '2.3.2' implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android-rx") @@ -296,7 +297,7 @@ dependencies { implementation 'com.airbnb.android:mvrx:1.3.0' // Work - implementation "androidx.work:work-runtime-ktx:2.3.3" + implementation "androidx.work:work-runtime-ktx:$work_version" // Paging implementation "androidx.paging:paging-runtime-ktx:2.1.1" diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 1bccdb6c25..f112fae83c 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -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.devices.VectorSettingsDevicesFragment 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.ignored.VectorSettingsIgnoredUsersFragment import im.vector.riotx.features.settings.push.PushGatewaysFragment @@ -371,11 +373,24 @@ interface FragmentModule { @Binds @IntoMap - @FragmentKey(KeyRequestListFragment::class) - fun bindKeyRequestListFragment(fragment: KeyRequestListFragment): Fragment + @FragmentKey(OutgoingKeyRequestListFragment::class) + fun bindOutgoingKeyRequestListFragment(fragment: OutgoingKeyRequestListFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(IncomingKeyRequestListFragment::class) + fun bindIncomingKeyRequestListFragment(fragment: IncomingKeyRequestListFragment): Fragment @Binds @IntoMap @FragmentKey(KeyRequestsFragment::class) fun bindKeyRequestsFragment(fragment: KeyRequestsFragment): Fragment + + + @Binds + @IntoMap + @FragmentKey(GossipingEventsPaperTrailFragment::class) + fun bindGossipingEventsPaperTrailFragment(fragment: GossipingEventsPaperTrailFragment): Fragment + + } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsEpoxyController.kt new file mode 100644 index 0000000000..f9224b5a4b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsEpoxyController.kt @@ -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() { + + 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() + 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() + val content = event.getClearContent().toModel() + 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 + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsPaperTrailFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsPaperTrailFragment.kt new file mode 100644 index 0000000000..e845061a70 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsPaperTrailFragment.kt @@ -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) + } + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt new file mode 100644 index 0000000000..f248ab1482 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt @@ -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> = Uninitialized +) : MvRxState + +class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted initialState: GossipingEventsPaperTrailState, + private val session: Session) + : VectorViewModel(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 { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: GossipingEventsPaperTrailState): GossipingEventsPaperTrailViewModel? { + val fragment: GossipingEventsPaperTrailFragment = (viewModelContext as FragmentViewModelContext).fragment() + + return fragment.viewModelFactory.create(state) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devtools/IncomingKeyRequestListFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devtools/IncomingKeyRequestListFragment.kt new file mode 100644 index 0000000000..653fbe36af --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/IncomingKeyRequestListFragment.kt @@ -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 + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestEpoxyController.kt index 4169ee71f4..f1c18806d9 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestEpoxyController.kt @@ -25,6 +25,7 @@ import im.vector.riotx.R import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.extensions.exhaustive 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 @@ -38,9 +39,75 @@ class KeyRequestEpoxyController @Inject constructor( // fun didTap(data: UserAccountData) } + var outgoing = true + var interactionListener: InteractionListener? = null 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 -> when (async) { is Uninitialized, @@ -57,6 +124,15 @@ class KeyRequestEpoxyController @Inject constructor( } } 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 { @@ -70,7 +146,7 @@ class KeyRequestEpoxyController @Inject constructor( title(roomKeyRequest.requestId) description( span { - span("sessionId:") { + span("sessionId:\n") { textStyle = "bold" } +"${roomKeyRequest.sessionId}" diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestListViewModel.kt index 6b09273f93..6812467b96 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestListViewModel.kt @@ -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.EmptyViewEvents import im.vector.riotx.core.platform.VectorViewModel +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch data class KeyRequestListViewState( val incomingRequests: Async> = Uninitialized, @@ -42,11 +44,24 @@ class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState : VectorViewModel(initialState) { init { - session.cryptoService().getOutgoingRoomKeyRequest().let { - setState { - copy( - outgoingRoomKeyRequest = Success(it) - ) + refresh() + } + + fun refresh() { + GlobalScope.launch { + session.cryptoService().getOutgoingRoomKeyRequest().let { + setState { + copy( + outgoingRoomKeyRequest = Success(it) + ) + } + } + session.cryptoService().getIncomingRoomKeyRequest().let { + setState { + copy( + incomingRequests = Success(it) + ) + } } } } @@ -58,12 +73,16 @@ class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState fun create(initialState: KeyRequestListViewState): KeyRequestListViewModel } + companion object : MvRxViewModelFactory { @JvmStatic override fun create(viewModelContext: ViewModelContext, state: KeyRequestListViewState): KeyRequestListViewModel? { - val fragment: KeyRequestListFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewModelFactory.create(state) + val context = viewModelContext as FragmentViewModelContext + val factory = (context.fragment as? IncomingKeyRequestListFragment)?.viewModelFactory + ?: (context.fragment as? OutgoingKeyRequestListFragment)?.viewModelFactory + + return factory?.create(state) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestsFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestsFragment.kt index 8b78f52c1e..b15d754181 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestsFragment.kt @@ -16,11 +16,14 @@ package im.vector.riotx.features.settings.devtools +import android.content.Context import android.os.Bundle +import android.view.MenuItem import android.view.View import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity 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 im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseActivity @@ -31,26 +34,79 @@ import javax.inject.Inject class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() { override fun getLayoutResId(): Int = R.layout.fragment_devtool_keyrequests + override fun getMenuRes(): Int = R.menu.menu_common_gossiping override fun onResume() { super.onResume() (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?) { super.onViewCreated(view, savedInstanceState) - devToolKeyRequestPager.adapter = KeyReqPagerAdapter(requireActivity()) + mPagerAdapter = KeyReqPagerAdapter(this) + devToolKeyRequestPager.adapter = mPagerAdapter + devToolKeyRequestPager.registerOnPageChangeCallback(pageAdapterListener) - TabLayoutMediator(devToolKeyRequestTabs, devToolKeyRequestPager) { tab, _ -> - tab.text = "Outgoing" + TabLayoutMediator(devToolKeyRequestTabs, devToolKeyRequestPager) { tab, position -> + when (position) { + 0 -> { + tab.text = "Outgoing" + } + 1 -> { + tab.text = "Incoming" + } + 2 -> { + tab.text = "Audit Trail" + } + } }.attach() } - private inner class KeyReqPagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { - override fun getItemCount(): Int = 1 + override fun onDestroyView() { + 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 { - 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) + } + } } } + } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestListFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devtools/OutgoingKeyRequestListFragment.kt similarity index 95% rename from vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestListFragment.kt rename to vector/src/main/java/im/vector/riotx/features/settings/devtools/OutgoingKeyRequestListFragment.kt index 71a4e19343..d79bb9934b 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/OutgoingKeyRequestListFragment.kt @@ -17,6 +17,7 @@ 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 @@ -28,14 +29,13 @@ import im.vector.riotx.core.resources.ColorProvider import kotlinx.android.synthetic.main.fragment_generic_recycler.* import javax.inject.Inject -class KeyRequestListFragment @Inject constructor( +class OutgoingKeyRequestListFragment @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 -> @@ -53,4 +53,5 @@ class KeyRequestListFragment @Inject constructor( recyclerView.cleanup() // epoxyController.interactionListener = null } + } diff --git a/vector/src/main/res/menu/menu_common_gossiping.xml b/vector/src/main/res/menu/menu_common_gossiping.xml new file mode 100644 index 0000000000..08b23c760c --- /dev/null +++ b/vector/src/main/res/menu/menu_common_gossiping.xml @@ -0,0 +1,10 @@ + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 7105d56a5c..643160ef21 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -12,6 +12,8 @@ Unlock encrypted messages history + Refresh +