diff --git a/CHANGES.md b/CHANGES.md index fc1d75f539..b07a6b567f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ Changes in RiotX 0.19.0 (2020-XX-XX) =================================================== Features ✨: - - + - Cross-Signing | Support SSSS secret sharing (#944) Improvements 🙌: - diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index f980279a8d..5f614763d5 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.3' 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..1dc0907b65 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt @@ -0,0 +1,198 @@ +/* + * 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()) + + @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 77ba66d341..3042a3c68f 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 @@ -34,7 +34,6 @@ 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.OutgoingRoomKeyRequest 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 @@ -326,46 +325,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(OutgoingRoomKeyRequest.RequestState.UNSENT)) - val sentRequest = cryptoStore2 - .getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.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(OutgoingRoomKeyRequest.RequestState.UNSENT)) - val sentRequestAfterRestoration = cryptoStore2 - .getOutgoingRoomKeyRequestByState(setOf(OutgoingRoomKeyRequest.RequestState.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/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 1360924270..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 @@ -22,12 +22,14 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService -import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener +import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener 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 import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult @@ -86,13 +88,15 @@ interface CryptoService { fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? + fun requestRoomKeyForEvent(event: Event) + fun reRequestRoomKeyForEvent(event: Event) fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) - fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) + fun addRoomKeysRequestListener(listener: GossipingRequestListener) - fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) + fun removeRoomKeysRequestListener(listener: GossipingRequestListener) fun getDevicesList(callback: MatrixCallback) @@ -129,4 +133,8 @@ interface CryptoService { fun addNewSessionListener(newSessionListener: NewSessionListener) 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..fe3f643124 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) + fun onSecretUSKGossip(uskPrivateKey: String) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keyshare/RoomKeysRequestListener.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keyshare/GossipingRequestListener.kt similarity index 70% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keyshare/RoomKeysRequestListener.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keyshare/GossipingRequestListener.kt index 5bce27e1b4..1dad685f41 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keyshare/RoomKeysRequestListener.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keyshare/GossipingRequestListener.kt @@ -17,12 +17,13 @@ package im.vector.matrix.android.api.session.crypto.keyshare import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest -import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCancellation +import im.vector.matrix.android.internal.crypto.IncomingRequestCancellation +import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest /** * Room keys events listener */ -interface RoomKeysRequestListener { +interface GossipingRequestListener { /** * An room key request has been received. * @@ -30,10 +31,16 @@ interface RoomKeysRequestListener { */ fun onRoomKeyRequest(request: IncomingRoomKeyRequest) + /** + * Returns the secret value to be shared + * @return true if is handled + */ + fun onSecretShareRequest(request: IncomingSecretShareRequest) : Boolean + /** * A room key request cancellation has been received. * * @param request the cancellation request */ - fun onRoomKeyRequestCancellation(request: IncomingRoomKeyRequestCancellation) + fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationService.kt index 5cad8d985c..75033082d6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/verification/VerificationService.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.crypto.verification import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.LocalEcho /** @@ -136,4 +137,6 @@ interface VerificationService { return age in tooInThePast..tooInTheFuture } } + + fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index d131960893..a60d0fd9ac 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -142,12 +142,12 @@ data class Event( } fun toContentStringWithIndent(): String { - val contentMap = toContent().toMutableMap() + val contentMap = toContent() return JSONObject(contentMap).toString(4) } fun toClearContentStringWithIndent(): String? { - val contentMap = this.mxDecryptionResult?.payload?.toMutableMap() + val contentMap = this.mxDecryptionResult?.payload val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java) return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) } } 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..d32e459dd6 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,8 @@ 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..54f89dc8b9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CancelGossipRequestWorker.kt @@ -0,0 +1,118 @@ +/* + * 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 bf923e9c58..9e702ee9ac 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,7 +33,9 @@ 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.keyshare.RoomKeysRequestListener +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 import im.vector.matrix.android.api.session.events.model.EventType @@ -55,7 +57,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 +84,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 @@ -115,6 +120,7 @@ internal class DefaultCryptoService @Inject constructor( private val myDeviceInfoHolder: Lazy, // the crypto store private val cryptoStore: IMXCryptoStore, + // Olm device private val olmDevice: MXOlmDevice, // Set of parameters used to configure/customize the end-to-end crypto. @@ -136,7 +142,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, @@ -188,6 +194,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 @@ -206,6 +213,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) @@ -214,6 +222,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) @@ -230,6 +239,7 @@ internal class DefaultCryptoService @Inject constructor( override fun getDevicesList(callback: MatrixCallback) { getDevicesTask .configureWith { + this.executionThread = TaskThread.CRYPTO this.callback = callback } .executeBy(taskExecutor) @@ -238,6 +248,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) @@ -300,14 +311,13 @@ internal class DefaultCryptoService @Inject constructor( runCatching { uploadDeviceKeys() oneTimeKeysUploader.maybeUploadOneTimeKeys() - outgoingRoomKeyRequestManager.start() keysBackupService.checkAndStartKeysBackup() if (isInitialSync) { // refresh the devices list for each known room members deviceListManager.invalidateAllDeviceLists() deviceListManager.refreshOutdatedDeviceLists() } else { - incomingRoomKeyRequestManager.processReceivedRoomKeyRequests() + incomingRoomKeyRequestManager.processReceivedGossipingRequests() } }.fold( { @@ -328,8 +338,6 @@ internal class DefaultCryptoService @Inject constructor( fun close() = runBlocking(coroutineDispatchers.crypto) { cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) - outgoingRoomKeyRequestManager.stop() - olmDevice.release() cryptoStore.close() } @@ -368,7 +376,7 @@ internal class DefaultCryptoService @Inject constructor( // Make sure we process to-device messages before generating new one-time-keys #2782 deviceListManager.refreshOutdatedDeviceLists() oneTimeKeysUploader.maybeUploadOneTimeKeys() - incomingRoomKeyRequestManager.processReceivedRoomKeyRequests() + incomingRoomKeyRequestManager.processReceivedGossipingRequests() } } } @@ -688,13 +696,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 -> { - incomingRoomKeyRequestManager.onRoomKeyRequestEvent(event) + // 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 -> { + cryptoStore.saveGossipingEvent(event) + onSecretSendReceived(event) } else -> { // ignore @@ -710,6 +729,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 @@ -722,6 +742,46 @@ internal class DefaultCryptoService @Inject constructor( alg.onRoomKeyEvent(event, keysBackupService) } + private fun onSecretSendReceived(event: Event) { + Timber.i("## onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}") + 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 secretContent = event.getClearContent().toModel() ?: return + + val existingRequest = cryptoStore + .getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId } + + if (existingRequest == null) { + Timber.i("## onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") + return + } + + when (existingRequest.secretName) { + SELF_SIGNING_KEY_SSSS_NAME -> { + crossSigningService.onSecretSSKGossip(secretContent.secretValue) + return + } + USER_SIGNING_KEY_SSSS_NAME -> { + crossSigningService.onSecretUSKGossip(secretContent.secretValue) + return + } + else -> { + // Ask to application layer? + Timber.v("## onSecretSend() : secret not handled by SDK") + } + } + } + /** * Handle an m.room.encryption event. * @@ -735,7 +795,7 @@ internal class DefaultCryptoService @Inject constructor( val userIds = getRoomUserIds(roomId) setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds) } catch (throwable: Throwable) { - Timber.e(throwable) + Timber.e(throwable, "## onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ") } } } @@ -997,14 +1057,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) } /** @@ -1013,37 +1073,53 @@ 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}") + } + } } /** - * Add a RoomKeysRequestListener listener. + * Add a GossipingRequestListener listener. * * @param listener listener */ - override fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) { + override fun addRoomKeysRequestListener(listener: GossipingRequestListener) { incomingRoomKeyRequestManager.addRoomKeysRequestListener(listener) } /** - * Add a RoomKeysRequestListener listener. + * Add a GossipingRequestListener listener. * * @param listener listener */ - override fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) { + override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { incomingRoomKeyRequestManager.removeRoomKeysRequestListener(listener) } @@ -1084,11 +1160,23 @@ 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 + ")" } + + 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/DeviceListManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt index 04b301ba9e..37a5ee18e1 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt @@ -361,13 +361,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM // Handle cross signing keys update val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also { - Timber.d("## CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}") + Timber.v("## CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}") } val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also { - Timber.d("## CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}") + Timber.v("## CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}") } val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also { - Timber.d("## CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}") + Timber.v("## CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}") } cryptoStore.storeUserCrossSigningKeys( userId, 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/IncomingRoomKeyRequestCancellation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRequestCancellation.kt similarity index 67% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCancellation.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRequestCancellation.kt index 6779936f3a..98e1e95423 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCancellation.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRequestCancellation.kt @@ -18,12 +18,12 @@ 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.toModel -import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareCancellation +import im.vector.matrix.android.internal.crypto.model.rest.ShareRequestCancellation /** - * IncomingRoomKeyRequestCancellation describes the incoming room key cancellation. + * IncomingRequestCancellation describes the incoming room key cancellation. */ -data class IncomingRoomKeyRequestCancellation( +data class IncomingRequestCancellation( /** * The user id */ @@ -37,22 +37,24 @@ data class IncomingRoomKeyRequestCancellation( /** * The request id */ - override val requestId: String? = null -) : IncomingRoomKeyRequestCommon { + override val requestId: String? = null, + override val localCreationTimestamp: Long? +) : IncomingShareRequestCommon { companion object { /** * Factory * * @param event the event */ - fun fromEvent(event: Event): IncomingRoomKeyRequestCancellation? { + fun fromEvent(event: Event): IncomingRequestCancellation? { return event.getClearContent() - .toModel() + .toModel() ?.let { - IncomingRoomKeyRequestCancellation( + 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 39b4678a27..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,8 +58,9 @@ data class IncomingRoomKeyRequest( * The runnable to call to ignore the key share request. */ @Transient - var ignore: Runnable? = null -) : IncomingRoomKeyRequestCommon { + var ignore: Runnable? = null, + override val localCreationTimestamp: Long? +) : IncomingShareRequestCommon { companion object { /** * Factory @@ -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 92a117d64b..0bb89154f1 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,9 +17,15 @@ package im.vector.matrix.android.internal.crypto import im.vector.matrix.android.api.auth.data.Credentials -import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener +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 import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShare +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.rest.GossipingDefaultContent +import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObject import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.session.SessionScope import timber.log.Timber @@ -29,18 +35,20 @@ import javax.inject.Inject internal class IncomingRoomKeyRequestManager @Inject constructor( private val credentials: Credentials, private val cryptoStore: IMXCryptoStore, + private val cryptoConfig: MXCryptoConfig, + private val secretSecretCryptoProvider: ShareSecretCryptoProvider, private val roomDecryptorProvider: RoomDecryptorProvider) { // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations // we received in the current sync. - private val receivedRoomKeyRequests = ArrayList() - private val receivedRoomKeyRequestCancellations = ArrayList() + private val receivedGossipingRequests = ArrayList() + private val receivedRequestCancellations = ArrayList() // the listeners - private val roomKeysRequestListeners: MutableSet = HashSet() + private val gossipingRequestListeners: MutableSet = HashSet() init { - receivedRoomKeyRequests.addAll(cryptoStore.getPendingIncomingRoomKeyRequests()) + receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests()) } /** @@ -49,110 +57,223 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( * * @param event the announcement event. */ - fun onRoomKeyRequestEvent(event: Event) { - when (val roomKeyShareAction = event.getClearContent()?.get("action") as? String) { - RoomKeyShare.ACTION_SHARE_REQUEST -> IncomingRoomKeyRequest.fromEvent(event)?.let { receivedRoomKeyRequests.add(it) } - RoomKeyShare.ACTION_SHARE_CANCELLATION -> IncomingRoomKeyRequestCancellation.fromEvent(event)?.let { receivedRoomKeyRequestCancellations.add(it) } - else -> Timber.e("## onRoomKeyRequestEvent() : unsupported action $roomKeyShareAction") + 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 { + 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 { + // save in DB + cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) + receivedGossipingRequests.add(it) + } + } + } else if (event.getClearType() == EventType.ROOM_KEY_REQUEST) { + IncomingRoomKeyRequest.fromEvent(event)?.let { + if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) { + // ignore, it was sent by me as * + Timber.v("## onGossipingRequestEvent type ${event.type} ignore remote echo") + } else { + cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) + receivedGossipingRequests.add(it) + } + } + } + } + GossipingToDeviceObject.ACTION_SHARE_CANCELLATION -> { + IncomingRequestCancellation.fromEvent(event)?.let { + receivedRequestCancellations.add(it) + } + } + else -> { + Timber.e("## onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}") + } } } /** - * Process any m.room_key_request events which were queued up during the + * Process any m.room_key_request or m.secret.request events which were queued up during the * current sync. * It must be called on CryptoThread */ - fun processReceivedRoomKeyRequests() { - val roomKeyRequestsToProcess = receivedRoomKeyRequests.toList() - receivedRoomKeyRequests.clear() - for (request in roomKeyRequestsToProcess) { - val userId = request.userId - val deviceId = request.deviceId - val body = request.requestBody - val roomId = body!!.roomId - val alg = body.algorithm + fun processReceivedGossipingRequests() { + Timber.v("## processReceivedGossipingRequests()") - Timber.v("m.room_key_request 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("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now") + val roomKeyRequestsToProcess = receivedGossipingRequests.toList() + receivedGossipingRequests.clear() + for (request in roomKeyRequestsToProcess) { + if (request is IncomingRoomKeyRequest) { + processIncomingRoomKeyRequest(request) + } else if (request is IncomingSecretShareRequest) { + processIncomingSecretShareRequest(request) + } + } + + var receivedRequestCancellations: List? = null + + synchronized(this.receivedRequestCancellations) { + if (this.receivedRequestCancellations.isNotEmpty()) { + receivedRequestCancellations = this.receivedRequestCancellations.toList() + this.receivedRequestCancellations.clear() + } + } + + 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) + } + } + } + } + + private fun processIncomingRoomKeyRequest(request: IncomingRoomKeyRequest) { + val userId = request.userId + val deviceId = request.deviceId + val body = request.requestBody + val roomId = body!!.roomId + val alg = body.algorithm + + 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.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + // TODO: should we queue up requests we don't yet have keys for, in case they turn up later? + // if we don't have a decryptor for this room/alg, we don't have + // the keys for the requested events, and can drop the requests. + 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.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + + if (credentials.deviceId == deviceId && credentials.userId == userId) { + Timber.v("## processReceivedGossipingRequests() : oneself device - ignored") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + request.share = Runnable { + decryptor.shareKeysWithDevice(request) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) + } + request.ignore = Runnable { + 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") + request.share?.run() return } - // TODO: should we queue up requests we don't yet have keys for, in case they turn up later? - // if we don't have a decryptor for this room/alg, we don't have - // the keys for the requested events, and can drop the requests. - val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg) - if (null == decryptor) { - Timber.w("## processReceivedRoomKeyRequests() : room key request for unknown $alg in room $roomId") - continue - } - if (!decryptor.hasKeysForKeyRequest(request)) { - Timber.w("## processReceivedRoomKeyRequests() : room key request for unknown session ${body.sessionId!!}") - cryptoStore.deleteIncomingRoomKeyRequest(request) - continue - } - if (credentials.deviceId == deviceId && credentials.userId == userId) { - Timber.v("## processReceivedRoomKeyRequests() : oneself device - ignored") - cryptoStore.deleteIncomingRoomKeyRequest(request) - continue + if (device.isBlocked) { + Timber.v("## processReceivedGossipingRequests() : device is blocked -> ignored") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return } - request.share = Runnable { - decryptor.shareKeysWithDevice(request) - cryptoStore.deleteIncomingRoomKeyRequest(request) - } - request.ignore = Runnable { - cryptoStore.deleteIncomingRoomKeyRequest(request) - } - // if the device is verified already, share the keys - val device = cryptoStore.getUserDevice(userId, deviceId!!) - if (device != null) { - if (device.isVerified) { - Timber.v("## processReceivedRoomKeyRequests() : device is already verified: sharing keys") - cryptoStore.deleteIncomingRoomKeyRequest(request) - request.share?.run() - continue + } + + // 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.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return + } + + // Pass to application layer to decide what to do + onRoomKeyRequest(request) + } + + private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) { + val secretName = request.secretName ?: return Unit.also { + 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 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) } - if (device.isBlocked) { - Timber.v("## processReceivedRoomKeyRequests() : device is blocked -> ignored") - cryptoStore.deleteIncomingRoomKeyRequest(request) - continue + 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 cross signing is available on account we automatically discard untrust devices request - if (cryptoStore.getMyCrossSigningInfo() != null) { - // At this point the device is unknown, we don't want to bother user with that - cryptoStore.deleteIncomingRoomKeyRequest(request) - continue - } - - cryptoStore.storeIncomingRoomKeyRequest(request) - onRoomKeyRequest(request) + if (!device.isVerified || device.isBlocked) { + Timber.v("## processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device") + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) + return } - var receivedRoomKeyRequestCancellations: List? = null + val isDeviceLocallyVerified = cryptoStore.getUserDevice(userId, deviceId)?.trustLevel?.isLocallyVerified() - synchronized(this.receivedRoomKeyRequestCancellations) { - if (this.receivedRoomKeyRequestCancellations.isNotEmpty()) { - receivedRoomKeyRequestCancellations = this.receivedRoomKeyRequestCancellations.toList() - this.receivedRoomKeyRequestCancellations.clear() + // 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 + Timber.i("## processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted") + if (isDeviceLocallyVerified == true) { + secretSecretCryptoProvider.shareSecretWithDevice(request, secretValue) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) } + return } - if (null != receivedRoomKeyRequestCancellations) { - for (request in receivedRoomKeyRequestCancellations!!) { - Timber.v("## ## processReceivedRoomKeyRequests() : 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) - } + request.ignore = Runnable { + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } + + request.share = { secretValue -> + secretSecretCryptoProvider.shareSecretWithDevice(request, secretValue) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) + } + + onShareRequest(request) } /** @@ -161,8 +282,8 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( * @param request the request */ private fun onRoomKeyRequest(request: IncomingRoomKeyRequest) { - synchronized(roomKeysRequestListeners) { - for (listener in roomKeysRequestListeners) { + synchronized(gossipingRequestListeners) { + for (listener in gossipingRequestListeners) { try { listener.onRoomKeyRequest(request) } catch (e: Exception) { @@ -172,14 +293,33 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( } } + /** + * Ask for a value to the listeners, and take the first one + */ + private fun onShareRequest(request: IncomingSecretShareRequest) { + synchronized(gossipingRequestListeners) { + for (listener in gossipingRequestListeners) { + try { + if (listener.onSecretShareRequest(request)) { + return + } + } catch (e: Exception) { + Timber.e(e, "## onRoomKeyRequest() failed") + } + } + } + // Not handled, ignore + request.ignore?.run() + } + /** * A room key request cancellation has been received. * * @param request the cancellation request */ - private fun onRoomKeyRequestCancellation(request: IncomingRoomKeyRequestCancellation) { - synchronized(roomKeysRequestListeners) { - for (listener in roomKeysRequestListeners) { + private fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) { + synchronized(gossipingRequestListeners) { + for (listener in gossipingRequestListeners) { try { listener.onRoomKeyRequestCancellation(request) } catch (e: Exception) { @@ -189,15 +329,15 @@ internal class IncomingRoomKeyRequestManager @Inject constructor( } } - fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) { - synchronized(roomKeysRequestListeners) { - roomKeysRequestListeners.add(listener) + fun addRoomKeysRequestListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.add(listener) } } - fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) { - synchronized(roomKeysRequestListeners) { - roomKeysRequestListeners.remove(listener) + fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.remove(listener) } } } 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 new file mode 100755 index 0000000000..2fcd3e22d5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingSecretShareRequest.kt @@ -0,0 +1,82 @@ +/* + * 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 im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.internal.crypto.model.rest.SecretShareRequest + +/** + * IncomingRoomKeyRequest class defines the incoming room keys request. + */ +data class IncomingSecretShareRequest( + /** + * The user id + */ + override val userId: String? = null, + + /** + * The device id + */ + override val deviceId: String? = null, + + /** + * The request id + */ + override val requestId: String? = null, + + /** + * The request body + */ + val secretName: String? = null, + + /** + * The runnable to call to accept to share the keys + */ + @Transient + var share: ((String) -> Unit)? = null, + + /** + * The runnable to call to ignore the key share request. + */ + @Transient + var ignore: Runnable? = null, + + override val localCreationTimestamp: Long? + +) : IncomingShareRequestCommon { + companion object { + /** + * Factory + * + * @param event the event + */ + fun fromEvent(event: Event): IncomingSecretShareRequest? { + return event.getClearContent() + .toModel() + ?.let { + IncomingSecretShareRequest( + userId = event.senderId, + deviceId = it.requestingDeviceId, + requestId = it.requestId, + secretName = it.secretName, + localCreationTimestamp = event.ageLocalTs ?: System.currentTimeMillis() + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCommon.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingShareRequestCommon.kt similarity index 91% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCommon.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingShareRequestCommon.kt index a7b1c6b117..f39a0d80d1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCommon.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingShareRequestCommon.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.crypto -interface IncomingRoomKeyRequestCommon { +interface IncomingShareRequestCommon { /** * The user id */ @@ -31,4 +31,6 @@ interface IncomingRoomKeyRequestCommon { * The request id */ val requestId: String? + + val localCreationTimestamp: Long? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt index 47ec85ec8c..54f34c6c9d 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt @@ -770,7 +770,7 @@ internal class MXOlmDevice @Inject constructor( return session } } else { - Timber.w("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId") + Timber.v("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId") throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequest.kt new file mode 100644 index 0000000000..2fb0c7094b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequest.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.internal.crypto + +interface OutgoingGossipingRequest { + var recipients: Map> + var requestId: String + var state: OutgoingGossipingRequestState + // transaction id for the cancellation, if any + // 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..7c83ccc9bf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingGossipingRequestManager.kt @@ -0,0 +1,189 @@ +/* + * 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.delay +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) { + // A bit dirty, but for better stability give other party some time to mark + // devices trusted :/ + delay(1500) + 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 a66b0242ec..b01c9d9b3f 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,26 @@ 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 - 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 - var requestId: String, // current state of this request - var state: RequestState) { - - // transaction id for the cancellation, if any - var cancellationTxnId: String? = null + override var requestId: String, // current state of this request + override var state: OutgoingGossipingRequestState + // transaction id for the cancellation, if any + // override var cancellationTxnId: String? = null +) : OutgoingGossipingRequest { /** * Used only for log. @@ -53,66 +57,4 @@ class OutgoingRoomKeyRequest( get() = if (null != requestBody) { requestBody!!.sessionId } else null - - /** - * possible states for a room key request - * - * - * The state machine looks like: - *
-     *
-     *      |
-     *      V
-     *    UNSENT  -----------------------------+
-     *      |                                  |
-     *      | (send successful)                | (cancellation requested)
-     *      V                                  |
-     *     SENT                                |
-     *      |--------------------------------  |  --------------+
-     *      |                                  |                |
-     *      |                                  |                | (cancellation requested with intent
-     *      |                                  |                | to resend a new request)
-     *      | (cancellation requested)         |                |
-     *      V                                  |                V
-     *  CANCELLATION_PENDING                   | CANCELLATION_PENDING_AND_WILL_RESEND
-     *      |                                  |                |
-     *      | (cancellation sent)              |                | (cancellation sent. Create new request
-     *      |                                  |                |  in the UNSENT state)
-     *      V                                  |                |
-     *  (deleted)  <---------------------------+----------------+
-     *  
- */ - - enum class RequestState { - /** - * request not yet sent - */ - UNSENT, - /** - * request sent, awaiting reply - */ - SENT, - /** - * reply received, cancellation not yet sent - */ - CANCELLATION_PENDING, - /** - * Cancellation not yet sent, once sent, a new request will be done - */ - CANCELLATION_PENDING_AND_WILL_RESEND, - /** - * sending failed - */ - FAILED; - - companion object { - fun from(state: Int) = when (state) { - 0 -> UNSENT - 1 -> SENT - 2 -> CANCELLATION_PENDING - 3 -> CANCELLATION_PENDING_AND_WILL_RESEND - else /*4*/ -> FAILED - } - } - } } 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 b59c93ba83..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.RoomKeyShareCancellation -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(), OutgoingRoomKeyRequest.RequestState.UNSENT)) - - if (req?.state == OutgoingRoomKeyRequest.RequestState.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) { - OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING, - OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> { - // nothing to do here - } - OutgoingRoomKeyRequest.RequestState.UNSENT, - OutgoingRoomKeyRequest.RequestState.FAILED -> { - Timber.v("## cancelRoomKeyRequest() : deleting unnecessary room key request for $requestBody") - cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId) - } - OutgoingRoomKeyRequest.RequestState.SENT -> { - if (andResend) { - req.state = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND - } else { - req.state = OutgoingRoomKeyRequest.RequestState.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(OutgoingRoomKeyRequest.RequestState.UNSENT, - OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING, - OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND)) - - if (null == outgoingRoomKeyRequest) { - Timber.v("## sendOutgoingRoomKeyRequests() : No more outgoing room key requests") - sendOutgoingRoomKeyRequestsRunning.set(false) - return - } - - if (OutgoingRoomKeyRequest.RequestState.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: OutgoingRoomKeyRequest.RequestState) { - if (request.state !== OutgoingRoomKeyRequest.RequestState.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(OutgoingRoomKeyRequest.RequestState.SENT) - } - - override fun onFailure(failure: Throwable) { - Timber.e("## sendOutgoingRoomKeyRequest failed") - onDone(OutgoingRoomKeyRequest.RequestState.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 = RoomKeyShareCancellation( - 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 === OutgoingRoomKeyRequest.RequestState.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 new file mode 100755 index 0000000000..1497796743 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingSecretRequest.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto + +import com.squareup.moshi.JsonClass + +/** + * Represents an outgoing room key request + */ +@JsonClass(generateAdapter = true) +class OutgoingSecretRequest( + // Secret Name + val secretName: String?, + // 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: OutgoingGossipingRequestState) : OutgoingGossipingRequest { + + // transaction id for the cancellation, if any +} 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..fb9c45da45 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/SendGossipRequestWorker.kt @@ -0,0 +1,148 @@ +/* + * 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.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 new file mode 100644 index 0000000000..78e587c0f1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/ShareSecretCryptoProvider.kt @@ -0,0 +1,74 @@ +/* + * 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 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 +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 + +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 +) { + fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue: String) { + val userId = request.userId ?: return + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + runCatching { deviceListManager.downloadKeys(listOf(userId), false) } + .mapCatching { + val deviceId = request.deviceId + val deviceInfo = cryptoStore.getUserDevice(userId, deviceId ?: "") ?: throw RuntimeException() + + Timber.i("## shareSecretWithDevice() : sharing secret ${request.secretName} with device $userId:$deviceId") + + val payloadJson = mutableMapOf("type" to EventType.SEND_SECRET) + payloadJson["content"] = SecretSendEventContent( + requestId = request.requestId ?: "", + secretValue = secretValue + ) + + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) + val sendToDeviceMap = MXUsersDevicesMap() + sendToDeviceMap.setObject(userId, deviceId, encodedPayload) + Timber.i("## shareSecretWithDevice() : sending to $userId:$deviceId") + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + sendToDeviceTask.execute(sendToDeviceParams) + } + } + } + + 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 8a66029026..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 @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.algorithms 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.IncomingSecretShareRequest import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService @@ -65,4 +66,8 @@ internal interface IMXDecrypting { * @param request keyRequest */ 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..d5b3b3b034 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,23 +144,23 @@ 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( @@ -170,7 +170,7 @@ internal class MXMegolmDecryption(private val userId: String, sessionId = encryptedEventContent.sessionId ) - outgoingRoomKeyRequestManager.sendRoomKeyRequest(requestBody, recipients) + outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients) } /** @@ -271,7 +271,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 acc9f4134d..74a4c6bee8 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,6 +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.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 @@ -61,6 +62,7 @@ internal class DefaultCrossSigningService @Inject constructor( private val taskExecutor: TaskExecutor, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener { private var olmUtility: OlmUtility? = null @@ -297,6 +299,59 @@ internal class DefaultCrossSigningService @Inject constructor( cryptoStore.clearOtherUserTrust() } + override fun onSecretSSKGossip(sskPrivateKey: String) { + Timber.i("## CrossSigning - onSecretSSKGossip") + val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also { + Timber.e("## CrossSigning - onSecretSSKGossip() received secret but public key is not known") + } + + sskPrivateKey.fromBase64() + .let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { + selfSigningPkSigning?.releaseSigning() + selfSigningPkSigning = pkSigning + Timber.i("## CrossSigning - Loading SSK success") + cryptoStore.storeSSKPrivateKey(sskPrivateKey) + return + } else { + Timber.e("## CrossSigning - onSecretSSKGossip() private key do not match public key") + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + Timber.e("## CrossSigning - onSecretSSKGossip() ${failure.localizedMessage}") + pkSigning.releaseSigning() + } + } + } + + override fun onSecretUSKGossip(uskPrivateKey: String) { + Timber.i("## CrossSigning - onSecretUSKGossip") + val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also { + Timber.e("## CrossSigning - onSecretUSKGossip() received secret but public key is not knwow ") + } + + uskPrivateKey.fromBase64() + .let { privateKeySeed -> + val pkSigning = OlmPkSigning() + try { + if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { + userPkSigning?.releaseSigning() + userPkSigning = pkSigning + Timber.i("## CrossSigning - Loading USK success") + cryptoStore.storeUSKPrivateKey(uskPrivateKey) + return + } else { + Timber.e("## CrossSigning - onSecretUSKGossip() private key do not match public key") + pkSigning.releaseSigning() + } + } catch (failure: Throwable) { + pkSigning.releaseSigning() + } + } + } + override fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?, uskKeyPrivateKey: String?, sskPrivateKey: String? @@ -396,7 +451,7 @@ internal class DefaultCrossSigningService @Inject constructor( * Will not force a download of the key, but will verify signatures trust chain */ override fun checkUserTrust(otherUserId: String): UserTrustResult { - Timber.d("## CrossSigning checkUserTrust for $otherUserId") + Timber.v("## CrossSigning checkUserTrust for $otherUserId") if (otherUserId == userId) { return checkSelfTrust() } @@ -547,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 { @@ -706,7 +767,7 @@ internal class DefaultCrossSigningService @Inject constructor( Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users") userIds.forEach { otherUserId -> checkUserTrust(otherUserId).let { - Timber.d("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}") + Timber.v("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}") setUserKeysAsTrusted(otherUserId, it.isVerified()) } @@ -714,7 +775,7 @@ internal class DefaultCrossSigningService @Inject constructor( val devices = cryptoStore.getUserDeviceList(otherUserId) devices?.forEach { device -> val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) - Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") + Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) } @@ -736,6 +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() cryptoStore.updateUsersTrust { users.add(it) checkUserTrust(it).isVerified() @@ -744,11 +806,26 @@ internal class DefaultCrossSigningService @Inject constructor( users.forEach { cryptoStore.getUserDeviceList(it)?.forEach { device -> val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) - Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") + Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) } } } } } + +// 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/event/SecretSendEventContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/SecretSendEventContent.kt new file mode 100644 index 0000000000..4a856b32a1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/SecretSendEventContent.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an encrypted event content + */ +@JsonClass(generateAdapter = true) +data class SecretSendEventContent( + @Json(name = "request_id") val requestId: String, + @Json(name = "secret") val secretValue: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShare.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/GossipingToDeviceObject.kt similarity index 70% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShare.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/GossipingToDeviceObject.kt index 4ea95d84ae..deaccdef16 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShare.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/GossipingToDeviceObject.kt @@ -15,11 +15,14 @@ */ package im.vector.matrix.android.internal.crypto.model.rest +import com.squareup.moshi.Json +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 RoomKeyShare : SendToDeviceObject { +interface GossipingToDeviceObject : SendToDeviceObject { val action: String? @@ -32,3 +35,10 @@ internal interface RoomKeyShare : SendToDeviceObject { const val ACTION_SHARE_CANCELLATION = "request_cancellation" } } + +@JsonClass(generateAdapter = true) +data class GossipingDefaultContent( + @Json(name = "action") override val action: String?, + @Json(name = "requesting_device_id") override val requestingDeviceId: String?, + @Json(name = "m.request_id") override val requestId: String? = null +) : GossipingToDeviceObject 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..0b7c3a201f 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,14 @@ 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 d92bc03aab..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,9 +23,9 @@ 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? = RoomKeyShare.ACTION_SHARE_REQUEST, + override val action: String? = GossipingToDeviceObject.ACTION_SHARE_REQUEST, @Json(name = "requesting_device_id") override val requestingDeviceId: String? = null, @@ -35,4 +35,4 @@ internal data class RoomKeyShareRequest( @Json(name = "body") val body: RoomKeyRequestBody? = null -) : RoomKeyShare +) : GossipingToDeviceObject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SecretShareRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SecretShareRequest.kt new file mode 100644 index 0000000000..86ae042166 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SecretShareRequest.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing a room key request content + */ +@JsonClass(generateAdapter = true) +data class SecretShareRequest( + @Json(name = "action") + override val action: String? = GossipingToDeviceObject.ACTION_SHARE_REQUEST, + + @Json(name = "requesting_device_id") + override val requestingDeviceId: String? = null, + + @Json(name = "request_id") + override val requestId: String? = null, + + @Json(name = "name") + val secretName: String? = null +) : GossipingToDeviceObject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareCancellation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/ShareRequestCancellation.kt similarity index 80% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareCancellation.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/ShareRequestCancellation.kt index b394993338..10be81be7d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareCancellation.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/ShareRequestCancellation.kt @@ -17,18 +17,19 @@ 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.crypto.model.rest.GossipingToDeviceObject.Companion.ACTION_SHARE_CANCELLATION /** * Class representing a room key request cancellation content */ @JsonClass(generateAdapter = true) -internal data class RoomKeyShareCancellation( +internal data class ShareRequestCancellation( @Json(name = "action") - override val action: String? = RoomKeyShare.ACTION_SHARE_CANCELLATION, + override val action: String? = ACTION_SHARE_CANCELLATION, @Json(name = "requesting_device_id") override val requestingDeviceId: String? = null, @Json(name = "request_id") override val requestId: String? = null -) : RoomKeyShare +) : GossipingToDeviceObject 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 e89f4a49ed..5594cbdf17 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,11 +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.GossipingRequestState import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest -import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCommon +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.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 @@ -117,6 +121,10 @@ internal interface IMXCryptoStore { */ fun getPendingIncomingRoomKeyRequests(): List + fun getPendingIncomingGossipingRequests(): List + fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) +// fun getPendingIncomingSecretShareRequests(): List + /** * Indicate if the store contains data for the passed account. * @@ -187,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. @@ -206,6 +214,7 @@ internal interface IMXCryptoStore { // TODO temp fun getLiveDeviceList(): LiveData> + /** * Store the crypto algorithm for a room. * @@ -347,43 +356,13 @@ 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? - /** - * Look for room key requests by state. - * - * @param states the states - * @return an OutgoingRoomKeyRequest or null - */ - fun getOutgoingRoomKeyRequestByState(states: Set): OutgoingRoomKeyRequest? + fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map>): OutgoingSecretRequest? - /** - * Update an existing outgoing request. - * - * @param request the request - */ - fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) + fun saveGossipingEvent(event: Event) - /** - * Delete an outgoing room key request. - * - * @param transactionId the transaction id. - */ - fun deleteOutgoingRoomKeyRequest(transactionId: String) - - /** - * Store an incomingRoomKeyRequest instance - * - * @param incomingRoomKeyRequest the incoming key request - */ - fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) - - /** - * Delete an incomingRoomKeyRequest instance - * - * @param incomingRoomKeyRequest the incoming key request - */ - fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequestCommon) + fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) /** * Search an IncomingRoomKeyRequest @@ -395,6 +374,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) @@ -406,22 +387,34 @@ 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 storeSSKPrivateKey(ssk: String?) + fun storeUSKPrivateKey(usk: String?) + + 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() fun updateUsersTrust(check: (String) -> Boolean) + + // Dev tools + + fun getOutgoingRoomKeyRequests(): List + fun getOutgoingSecretKeyRequests(): 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 a93203fc21..20cc327b3d 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,12 +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.IncomingRoomKeyRequestCommon +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.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 @@ -44,16 +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.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 @@ -62,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 @@ -359,7 +371,23 @@ internal class RealmCryptoStore @Inject constructor( doRealmTransaction(realmConfiguration) { realm -> realm.where().findFirst()?.apply { xSignMasterPrivateKey = msk + xSignUserPrivateKey = usk xSignSelfSignedPrivateKey = ssk + } + } + } + + override fun storeSSKPrivateKey(ssk: String?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where().findFirst()?.apply { + xSignSelfSignedPrivateKey = ssk + } + } + } + + override fun storeUSKPrivateKey(usk: String?) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where().findFirst()?.apply { xSignUserPrivateKey = usk } } @@ -797,131 +825,328 @@ 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(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName) + }.mapNotNull { + it.toOutgoingGossipingRequest() as? OutgoingSecretRequest + }.firstOrNull() + } - 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: IncomingRoomKeyRequestCommon) { - 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 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 getPendingIncomingGossipingRequests(): List { + return doRealmQueryAndCopyList(realmConfiguration) { + it.where() + .equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name) + .findAll() + } + .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 * ========================================================================================== */ @@ -1024,6 +1249,28 @@ internal class RealmCryptoStore @Inject constructor( } } + override fun getOutgoingRoomKeyRequests(): List { + return monarchy.fetchAllMappedSync({ realm -> + realm + .where(OutgoingGossipingRequestEntity::class.java) + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + }, { entity -> + entity.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + }) + .filterNotNull() + } + + override fun getOutgoingSecretKeyRequests(): List { + return monarchy.fetchAllMappedSync({ realm -> + realm + .where(OutgoingGossipingRequestEntity::class.java) + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) + }, { entity -> + entity.toOutgoingGossipingRequest() as? OutgoingSecretRequest + }) + .filterNotNull() + } + override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? { return doRealmQueryAndCopy(realmConfiguration) { realm -> realm.where(CrossSigningInfoEntity::class.java) 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 6839f6995b..932316aab8 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,7 +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.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.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 @@ -34,102 +37,146 @@ import timber.log.Timber internal object RealmCryptoStoreMigration : RealmMigration { // Version 1L added Cross Signing info persistence - const val CRYPTO_STORE_SCHEMA_VERSION = 1L + const val CRYPTO_STORE_SCHEMA_VERSION = 2L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion") - if (oldVersion <= 0) { - Timber.d("Step 0 -> 1") - Timber.d("Create KeyInfoEntity") + if (oldVersion <= 0) migrateTo1(realm) + if (oldVersion <= 1) migrateTo2(realm) + } - val trustLevelentityEntitySchema = realm.schema.create("TrustLevelEntity") - .addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java) - .setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true) - .addField(TrustLevelEntityFields.LOCALLY_VERIFIED, Boolean::class.java) - .setNullable(TrustLevelEntityFields.LOCALLY_VERIFIED, true) + private fun migrateTo1(realm: DynamicRealm) { + Timber.d("Step 0 -> 1") + Timber.d("Create KeyInfoEntity") - val keyInfoEntitySchema = realm.schema.create("KeyInfoEntity") - .addField(KeyInfoEntityFields.PUBLIC_KEY_BASE64, String::class.java) - .addField(KeyInfoEntityFields.SIGNATURES, String::class.java) - .addRealmListField(KeyInfoEntityFields.USAGES.`$`, String::class.java) - .addRealmObjectField(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema) + val trustLevelentityEntitySchema = realm.schema.create("TrustLevelEntity") + .addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java) + .setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true) + .addField(TrustLevelEntityFields.LOCALLY_VERIFIED, Boolean::class.java) + .setNullable(TrustLevelEntityFields.LOCALLY_VERIFIED, true) - Timber.d("Create CrossSigningInfoEntity") + val keyInfoEntitySchema = realm.schema.create("KeyInfoEntity") + .addField(KeyInfoEntityFields.PUBLIC_KEY_BASE64, String::class.java) + .addField(KeyInfoEntityFields.SIGNATURES, String::class.java) + .addRealmListField(KeyInfoEntityFields.USAGES.`$`, String::class.java) + .addRealmObjectField(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema) - val crossSigningInfoSchema = realm.schema.create("CrossSigningInfoEntity") - .addField(CrossSigningInfoEntityFields.USER_ID, String::class.java) - .addPrimaryKey(CrossSigningInfoEntityFields.USER_ID) - .addRealmListField(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`, keyInfoEntitySchema) + Timber.d("Create CrossSigningInfoEntity") - Timber.d("Updating UserEntity table") - realm.schema.get("UserEntity") - ?.addRealmObjectField(UserEntityFields.CROSS_SIGNING_INFO_ENTITY.`$`, crossSigningInfoSchema) + val crossSigningInfoSchema = realm.schema.create("CrossSigningInfoEntity") + .addField(CrossSigningInfoEntityFields.USER_ID, String::class.java) + .addPrimaryKey(CrossSigningInfoEntityFields.USER_ID) + .addRealmListField(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`, keyInfoEntitySchema) - Timber.d("Updating CryptoMetadataEntity table") - realm.schema.get("CryptoMetadataEntity") - ?.addField(CryptoMetadataEntityFields.X_SIGN_MASTER_PRIVATE_KEY, String::class.java) - ?.addField(CryptoMetadataEntityFields.X_SIGN_USER_PRIVATE_KEY, String::class.java) - ?.addField(CryptoMetadataEntityFields.X_SIGN_SELF_SIGNED_PRIVATE_KEY, String::class.java) + Timber.d("Updating UserEntity table") + realm.schema.get("UserEntity") + ?.addRealmObjectField(UserEntityFields.CROSS_SIGNING_INFO_ENTITY.`$`, crossSigningInfoSchema) - val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build() - val listMigrationAdapter = moshi.adapter>(Types.newParameterizedType( - List::class.java, - String::class.java, - Any::class.java - )) - val mapMigrationAdapter = moshi.adapter(Types.newParameterizedType( - Map::class.java, - String::class.java, - Any::class.java - )) + Timber.d("Updating CryptoMetadataEntity table") + realm.schema.get("CryptoMetadataEntity") + ?.addField(CryptoMetadataEntityFields.X_SIGN_MASTER_PRIVATE_KEY, String::class.java) + ?.addField(CryptoMetadataEntityFields.X_SIGN_USER_PRIVATE_KEY, String::class.java) + ?.addField(CryptoMetadataEntityFields.X_SIGN_SELF_SIGNED_PRIVATE_KEY, String::class.java) - realm.schema.get("DeviceInfoEntity") - ?.addField(DeviceInfoEntityFields.USER_ID, String::class.java) - ?.addField(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, String::class.java) - ?.addField(DeviceInfoEntityFields.KEYS_MAP_JSON, String::class.java) - ?.addField(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, String::class.java) - ?.addField(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, String::class.java) - ?.addField(DeviceInfoEntityFields.IS_BLOCKED, Boolean::class.java) - ?.setNullable(DeviceInfoEntityFields.IS_BLOCKED, true) - ?.addRealmObjectField(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema) - ?.transform { obj -> + val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build() + val listMigrationAdapter = moshi.adapter>(Types.newParameterizedType( + List::class.java, + String::class.java, + Any::class.java + )) + val mapMigrationAdapter = moshi.adapter(Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + )) - val oldSerializedData = obj.getString("deviceInfoData") - deserializeFromRealm(oldSerializedData)?.let { oldDevice -> + realm.schema.get("DeviceInfoEntity") + ?.addField(DeviceInfoEntityFields.USER_ID, String::class.java) + ?.addField(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.KEYS_MAP_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, String::class.java) + ?.addField(DeviceInfoEntityFields.IS_BLOCKED, Boolean::class.java) + ?.setNullable(DeviceInfoEntityFields.IS_BLOCKED, true) + ?.addRealmObjectField(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevelentityEntitySchema) + ?.transform { obj -> - val trustLevel = realm.createObject("TrustLevelEntity") - when (oldDevice.verified) { - MXDeviceInfo.DEVICE_VERIFICATION_UNKNOWN -> { - obj.setNull(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`) - } - MXDeviceInfo.DEVICE_VERIFICATION_BLOCKED -> { - trustLevel.setNull(TrustLevelEntityFields.LOCALLY_VERIFIED) - trustLevel.setNull(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED) - obj.setBoolean(DeviceInfoEntityFields.IS_BLOCKED, oldDevice.isBlocked) - obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel) - } - MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED -> { - trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, false) - trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false) - obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel) - } - MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED -> { - trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, true) - trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false) - obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel) - } + val oldSerializedData = obj.getString("deviceInfoData") + deserializeFromRealm(oldSerializedData)?.let { oldDevice -> + + val trustLevel = realm.createObject("TrustLevelEntity") + when (oldDevice.verified) { + MXDeviceInfo.DEVICE_VERIFICATION_UNKNOWN -> { + obj.setNull(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`) + } + MXDeviceInfo.DEVICE_VERIFICATION_BLOCKED -> { + trustLevel.setNull(TrustLevelEntityFields.LOCALLY_VERIFIED) + trustLevel.setNull(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED) + obj.setBoolean(DeviceInfoEntityFields.IS_BLOCKED, oldDevice.isBlocked) + obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel) + } + MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED -> { + trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, false) + trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false) + obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel) + } + MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED -> { + trustLevel.setBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED, true) + trustLevel.setBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, false) + obj.setObject(DeviceInfoEntityFields.TRUST_LEVEL_ENTITY.`$`, trustLevel) } - - obj.setString(DeviceInfoEntityFields.USER_ID, oldDevice.userId) - obj.setString(DeviceInfoEntityFields.IDENTITY_KEY, oldDevice.identityKey()) - obj.setString(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, listMigrationAdapter.toJson(oldDevice.algorithms)) - obj.setString(DeviceInfoEntityFields.KEYS_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.keys)) - obj.setString(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.signatures)) - obj.setString(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.unsigned)) } + + obj.setString(DeviceInfoEntityFields.USER_ID, oldDevice.userId) + obj.setString(DeviceInfoEntityFields.IDENTITY_KEY, oldDevice.identityKey()) + obj.setString(DeviceInfoEntityFields.ALGORITHM_LIST_JSON, listMigrationAdapter.toJson(oldDevice.algorithms)) + obj.setString(DeviceInfoEntityFields.KEYS_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.keys)) + obj.setString(DeviceInfoEntityFields.SIGNATURE_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.signatures)) + obj.setString(DeviceInfoEntityFields.UNSIGNED_MAP_JSON, mapMigrationAdapter.toJson(oldDevice.unsigned)) } - ?.removeField("deviceInfoData") - } + } + ?.removeField("deviceInfoData") + } + + private fun migrateTo2(realm: DynamicRealm) { + Timber.d("Step 1 -> 2") + realm.schema.remove("OutgoingRoomKeyRequestEntity") + realm.schema.remove("IncomingRoomKeyRequestEntity") + + // 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) + .addIndex(GossipingEventEntityFields.SENDER) + .addField(GossipingEventEntityFields.DECRYPTION_RESULT_JSON, String::class.java) + .addField(GossipingEventEntityFields.DECRYPTION_ERROR_CODE, String::class.java) + .addField(GossipingEventEntityFields.AGE_LOCAL_TS, Long::class.java) + .setNullable(GossipingEventEntityFields.AGE_LOCAL_TS, true) + .addField(GossipingEventEntityFields.SEND_STATE_STR, String::class.java) + + realm.schema.create("IncomingGossipingRequestEntity") + .addField(IncomingGossipingRequestEntityFields.REQUEST_ID, String::class.java) + .addIndex(IncomingGossipingRequestEntityFields.REQUEST_ID) + .addField(IncomingGossipingRequestEntityFields.TYPE_STR, String::class.java) + .addIndex(IncomingGossipingRequestEntityFields.TYPE_STR) + .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) + .addIndex(OutgoingGossipingRequestEntityFields.REQUEST_ID) + .addField(OutgoingGossipingRequestEntityFields.RECIPIENTS_DATA, String::class.java) + .addField(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java) + .addField(OutgoingGossipingRequestEntityFields.TYPE_STR, String::class.java) + .addIndex(OutgoingGossipingRequestEntityFields.TYPE_STR) + .addField(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, 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 1053cc5f43..3da91c6268 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,12 +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.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.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 @@ -38,14 +39,15 @@ import io.realm.annotations.RealmModule CryptoMetadataEntity::class, CryptoRoomEntity::class, DeviceInfoEntity::class, - IncomingRoomKeyRequestEntity::class, KeysBackupDataEntity::class, OlmInboundGroupSessionEntity::class, OlmSessionEntity::class, - OutgoingRoomKeyRequestEntity::class, UserEntity::class, KeyInfoEntity::class, CrossSigningInfoEntity::class, - TrustLevelEntity::class + TrustLevelEntity::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..131ddfafc6 --- /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..bb7d497a0e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt @@ -0,0 +1,89 @@ +/* + * 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.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..31d7d3374e 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 new file mode 100644 index 0000000000..9f2175329c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingSecretRequestEntity.kt @@ -0,0 +1,37 @@ +// /* +// * Copyright (c) 2020 New Vector Ltd +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ +// +// package im.vector.matrix.android.internal.crypto.store.db.model +// +// import 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..19049c099c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt @@ -0,0 +1,104 @@ +/* + * 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.internal.crypto.GossipRequestType +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.RealmObject +import io.realm.annotations.Index + +internal open class OutgoingGossipingRequestEntity( + @Index var requestId: String? = null, + var recipientsData: String? = null, + var requestedInfoStr: String? = null, + @Index var typeStr: String? = 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 = 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 86fc177f2b..4eee322a6a 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,76 +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.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!!, - OutgoingRoomKeyRequest.RequestState.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 new file mode 100644 index 0000000000..4a1c8ce46c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingSecretRequestEntity.kt @@ -0,0 +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) +// } +// } 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 51c5015a1d..014234885a 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 @@ -60,8 +60,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor( // TODO ignore initial sync or back pagination? 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") + Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") // 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. @@ -85,6 +84,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor( ) } catch (e: MXCryptoError) { Timber.e("## SAS Failed to decrypt event: ${event.eventId}") + params.verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event) } } Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt index f88e6e4f35..989ddc9804 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerif import im.vector.matrix.android.api.session.crypto.verification.SasMode 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.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import timber.log.Timber @@ -33,6 +34,7 @@ internal class DefaultIncomingSASDefaultVerificationTransaction( override val deviceId: String?, private val cryptoStore: IMXCryptoStore, crossSigningService: CrossSigningService, + outgoingGossipingRequestManager: OutgoingGossipingRequestManager, deviceFingerprint: String, transactionId: String, otherUserID: String, @@ -43,6 +45,7 @@ internal class DefaultIncomingSASDefaultVerificationTransaction( deviceId, cryptoStore, crossSigningService, + outgoingGossipingRequestManager, deviceFingerprint, transactionId, otherUserID, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt index 85ab8f0bf6..6c7e8f29d3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.crypto.verification.CancelCode import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction 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.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import timber.log.Timber @@ -30,6 +31,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( deviceId: String?, cryptoStore: IMXCryptoStore, crossSigningService: CrossSigningService, + outgoingGossipingRequestManager: OutgoingGossipingRequestManager, deviceFingerprint: String, transactionId: String, otherUserId: String, @@ -40,6 +42,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( deviceId, cryptoStore, crossSigningService, + outgoingGossipingRequestManager, deviceFingerprint, transactionId, otherUserId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt index 3b9d62a5dd..f6364c2125 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt @@ -35,6 +35,7 @@ import im.vector.matrix.android.api.session.crypto.verification.safeValueOf 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.RelationType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent @@ -50,10 +51,12 @@ import im.vector.matrix.android.api.session.room.model.message.MessageVerificati import im.vector.matrix.android.api.session.room.model.message.ValidVerificationDone import im.vector.matrix.android.internal.crypto.DeviceListManager import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder +import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager 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.model.CryptoDeviceInfo 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.rest.KeyVerificationAccept import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey @@ -86,6 +89,7 @@ internal class DefaultVerificationService @Inject constructor( @UserId private val userId: String, @DeviceId private val deviceId: String?, private val cryptoStore: IMXCryptoStore, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val myDeviceInfoHolder: Lazy, private val deviceListManager: DeviceListManager, private val setDeviceVerificationAction: SetDeviceVerificationAction, @@ -354,6 +358,27 @@ internal class DefaultVerificationService @Inject constructor( */ } + override fun onPotentiallyInterestingEventRoomFailToDecrypt(event: Event) { + // When Should/Can we cancel?? + val relationContent = event.content.toModel()?.relatesTo + if (relationContent?.type == RelationType.REFERENCE) { + val relatedId = relationContent.eventId ?: return + // at least if request was sent by me, I can safely cancel without interfering + pendingRequests[event.senderId]?.firstOrNull { + it.transactionId == relatedId && !it.isIncoming + }?.let { pr -> + verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) + .cancelTransaction( + relatedId, + event.senderId ?: "", + event.getSenderKey() ?: "", + CancelCode.InvalidMessage + ) + updatePendingRequest(pr.copy(cancelConclusion = CancelCode.InvalidMessage)) + } + } + } + private suspend fun onRoomStartRequestReceived(event: Event) { val startReq = event.getClearContent().toModel() ?.copy( @@ -482,6 +507,7 @@ internal class DefaultVerificationService @Inject constructor( deviceId, cryptoStore, crossSigningService, + outgoingGossipingRequestManager, myDeviceInfoHolder.get().myDevice.fingerprint()!!, startReq.transactionId, otherUserId, @@ -781,6 +807,7 @@ internal class DefaultVerificationService @Inject constructor( senderId, readyReq.fromDevice, crossSigningService, + outgoingGossipingRequestManager, cryptoStore, qrCodeData, userId, @@ -962,6 +989,7 @@ internal class DefaultVerificationService @Inject constructor( deviceId, cryptoStore, crossSigningService, + outgoingGossipingRequestManager, myDeviceInfoHolder.get().myDevice.fingerprint()!!, txID, otherUserId, @@ -1137,6 +1165,7 @@ internal class DefaultVerificationService @Inject constructor( deviceId, cryptoStore, crossSigningService, + outgoingGossipingRequestManager, myDeviceInfoHolder.get().myDevice.fingerprint()!!, transactionId, otherUserId, @@ -1273,6 +1302,7 @@ internal class DefaultVerificationService @Inject constructor( otherUserId, otherDeviceId, crossSigningService, + outgoingGossipingRequestManager, cryptoStore, qrCodeData, userId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt index 6396447352..cb83fdfe8b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt @@ -17,8 +17,11 @@ package im.vector.matrix.android.internal.crypto.verification import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService +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.verification.VerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState +import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import timber.log.Timber @@ -29,6 +32,7 @@ import timber.log.Timber internal abstract class DefaultVerificationTransaction( private val setDeviceVerificationAction: SetDeviceVerificationAction, private val crossSigningService: CrossSigningService, + private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val userId: String, override val transactionId: String, override val otherUserId: String, @@ -54,6 +58,14 @@ internal abstract class DefaultVerificationTransaction( protected fun trust(canTrustOtherUserMasterKey: Boolean, toVerifyDeviceIds: List, eventuallyMarkMyMasterKeyAsTrusted: Boolean) { + Timber.d("## Verification: trust ($otherUserId,$otherDeviceId) , verifiedDevices:$toVerifyDeviceIds") + Timber.d("## Verification: trust Mark myMSK trusted $eventuallyMarkMyMasterKeyAsTrusted") + + // TODO what if the otherDevice is not in this list? and should we + toVerifyDeviceIds.forEach { + setDeviceVerified(otherUserId, it) + } + // If not me sign his MSK and upload the signature if (canTrustOtherUserMasterKey) { // we should trust this master key @@ -83,11 +95,13 @@ internal abstract class DefaultVerificationTransaction( }) } - // TODO what if the otherDevice is not in this list? and should we - toVerifyDeviceIds.forEach { - setDeviceVerified(otherUserId, it) + transport.done(transactionId) { + if (otherUserId == userId) { + outgoingGossipingRequestManager.sendSecretShareRequest(SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*"))) + outgoingGossipingRequestManager.sendSecretShareRequest(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*"))) + } } - transport.done(transactionId) + state = VerificationTxState.Verified } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt index 18532a2e26..cfb3d7e38e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASDefaultVerificationTransaction.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.crypto.verification.SasMode import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction 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.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.model.MXKey import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore @@ -42,6 +43,7 @@ internal abstract class SASDefaultVerificationTransaction( open val deviceId: String?, private val cryptoStore: IMXCryptoStore, crossSigningService: CrossSigningService, + outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val deviceFingerprint: String, transactionId: String, otherUserId: String, @@ -50,6 +52,7 @@ internal abstract class SASDefaultVerificationTransaction( ) : DefaultVerificationTransaction( setDeviceVerificationAction, crossSigningService, + outgoingGossipingRequestManager, userId, transactionId, otherUserId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt index cedcf2865d..75ffa5e082 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt @@ -46,7 +46,8 @@ internal interface VerificationTransport { otherUserDeviceId: String?, code: CancelCode) - fun done(transactionId: String) + fun done(transactionId: String, + onDone: (() -> Unit)?) /** * Creates an accept message suitable for this transport diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt index 7a6e3b40ac..b7b7335011 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt @@ -22,8 +22,8 @@ import androidx.work.ExistingWorkPolicy import androidx.work.Operation import androidx.work.WorkInfo import im.vector.matrix.android.R -import im.vector.matrix.android.api.session.crypto.verification.ValidVerificationInfoRequest import im.vector.matrix.android.api.session.crypto.verification.CancelCode +import im.vector.matrix.android.api.session.crypto.verification.ValidVerificationInfoRequest import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event @@ -107,7 +107,7 @@ internal class VerificationTransportRoomMessage( // }, listenerExecutor) val workLiveData = workManagerProvider.workManager - .getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork") + .getWorkInfosForUniqueWorkLiveData(uniqueQueueName()) val observer = object : Observer> { override fun onChanged(workInfoList: List?) { @@ -228,7 +228,8 @@ internal class VerificationTransportRoomMessage( enqueueSendWork(workerParams) } - override fun done(transactionId: String) { + override fun done(transactionId: String, + onDone: (() -> Unit)?) { Timber.d("## SAS sending done for $transactionId") val event = createEventAndLocalEcho( type = EventType.KEY_VERIFICATION_DONE, @@ -244,7 +245,26 @@ internal class VerificationTransportRoomMessage( sessionId = sessionId, event = event )) - enqueueSendWork(workerParams) + val enqueueInfo = enqueueSendWork(workerParams) + + val workLiveData = workManagerProvider.workManager + .getWorkInfosForUniqueWorkLiveData(uniqueQueueName()) + val observer = object : Observer> { + override fun onChanged(workInfoList: List?) { + workInfoList + ?.filter { it.state == WorkInfo.State.SUCCEEDED } + ?.firstOrNull { it.id == enqueueInfo.second } + ?.let { _ -> + onDone?.invoke() + workLiveData.removeObserver(this) + } + } + } + + // TODO listen to DB to get synced info + GlobalScope.launch(Dispatchers.Main) { + workLiveData.observeForever(observer) + } } private fun enqueueSendWork(workerParams: Data): Pair { @@ -254,10 +274,12 @@ internal class VerificationTransportRoomMessage( .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS) .build() return workManagerProvider.workManager - .beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest) + .beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND, workRequest) .enqueue() to workRequest.id } + private fun uniqueQueueName() = "${roomId}_VerificationWork" + override fun createAccept(tid: String, keyAgreementProtocol: String, hash: String, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt index d07a83d7b1..290fc88878 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt @@ -145,7 +145,7 @@ internal class VerificationTransportToDevice( .executeBy(taskExecutor) } - override fun done(transactionId: String) { + override fun done(transactionId: String, onDone: (() -> Unit)?) { val otherUserId = tx?.otherUserId ?: return val otherUserDeviceId = tx?.otherDeviceId ?: return val cancelMessage = KeyVerificationDone(transactionId) @@ -155,6 +155,7 @@ internal class VerificationTransportToDevice( .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_DONE, contentMap, transactionId)) { this.callback = object : MatrixCallback { override fun onSuccess(data: Unit) { + onDone?.invoke() Timber.v("## SAS verification [$transactionId] done") } 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..8fc12f000a 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 @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.crypto.verification.CancelCode import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction 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.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64 import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64Safe @@ -36,6 +37,7 @@ internal class DefaultQrCodeVerificationTransaction( override val otherUserId: String, override var otherDeviceId: String?, private val crossSigningService: CrossSigningService, + outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val cryptoStore: IMXCryptoStore, // Not null only if other user is able to scan QR code private val qrCodeData: QrCodeData?, @@ -45,6 +47,7 @@ internal class DefaultQrCodeVerificationTransaction( ) : DefaultVerificationTransaction( setDeviceVerificationAction, crossSigningService, + outgoingGossipingRequestManager, userId, transactionId, otherUserId, @@ -181,13 +184,12 @@ internal class DefaultQrCodeVerificationTransaction( // qrCodeData.sharedSecret will be used to send the start request start(otherQrCodeData.sharedSecret) - // Trust the other user trust(canTrustOtherUserMasterKey, toVerifyDeviceIds.distinct(), eventuallyMarkMyMasterKeyAsTrusted = true) } - private fun start(remoteSecret: String) { + private fun start(remoteSecret: String, onDone: (() -> Unit)? = null) { if (state != VerificationTxState.None) { Timber.e("## Verification QR: start verification from invalid state") // should I cancel?? @@ -205,7 +207,7 @@ internal class DefaultQrCodeVerificationTransaction( startMessage, VerificationTxState.Started, CancelCode.User, - null + onDone ) } 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/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt index 98046a4a36..d466ecc321 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -238,7 +238,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain ) } catch (e: MXCryptoError) { - Timber.w("Failed to decrypt e2e replace") + Timber.v("Failed to decrypt e2e replace") // TODO -> we should keep track of this and retry, or aggregation will be broken } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt index 2d6656c2e3..02c06b0e56 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventDecryptor.kt @@ -114,7 +114,7 @@ internal class TimelineEventDecryptor @Inject constructor( Timber.v("Successfully decrypted event $eventId") eventEntity.setDecryptionResult(result) } catch (e: MXCryptoError) { - Timber.w(e, "Failed to decrypt event $eventId") + Timber.v(e, "Failed to decrypt event $eventId") if (e is MXCryptoError.Base && e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { // Keep track of unknown sessions to automatically try to decrypt on new session eventEntity.decryptionErrorCode = e.errorType.name diff --git a/vector/build.gradle b/vector/build.gradle index 2aae593271..66ec6808c8 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.3' 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 d7e89a62f6..4e60a1bdf7 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,6 +74,10 @@ 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.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 import im.vector.riotx.features.share.IncomingShareFragment @@ -366,4 +370,24 @@ interface FragmentModule { @IntoMap @FragmentKey(AccountDataFragment::class) fun bindAccountDataFragment(fragment: AccountDataFragment): Fragment + + @Binds + @IntoMap + @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/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt index cbb0e904e4..6eb316456a 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt @@ -171,7 +171,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { override fun invalidate() { // no-ops by default - Timber.w("invalidate() method has not been implemented") + Timber.v("invalidate() method has not been implemented") } protected fun setArguments(args: Parcelable? = null) { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt index 534de09ce0..d9860e6bad 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt @@ -22,13 +22,14 @@ package im.vector.riotx.features.crypto.keysrequest import android.content.Context import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session -import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener +import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationService import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest -import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCancellation +import im.vector.matrix.android.internal.crypto.IncomingRequestCancellation +import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap @@ -54,7 +55,7 @@ import javax.inject.Singleton @Singleton class KeyRequestHandler @Inject constructor(private val context: Context) - : RoomKeysRequestListener, + : GossipingRequestListener, VerificationService.Listener { private val alertsToRequests = HashMap>() @@ -73,6 +74,13 @@ class KeyRequestHandler @Inject constructor(private val context: Context) session = null } + override fun onSecretShareRequest(request: IncomingSecretShareRequest) : Boolean { + // By default riotX will not prompt if the SDK has decided that the request should not be fulfilled + Timber.v("## onSecretShareRequest() : Ignoring $request") + request.ignore?.run() + return true + } + /** * Handle incoming key request. * @@ -194,20 +202,6 @@ class KeyRequestHandler @Inject constructor(private val context: Context) denyAllRequests(mappingKey) } - // TODO send to the new profile page -// alert.addButton( -// context.getString(R.string.start_verification_short_label), -// Runnable { -// alert.weakCurrentActivity?.get()?.let { -// val intent = SASVerificationActivity.outgoingIntent(it, -// session?.myUserId ?: "", -// userId, deviceId) -// it.startActivity(intent) -// } -// }, -// false -// ) - alert.addButton(context.getString(R.string.share_without_verifying_short_label), Runnable { shareAllSessions(mappingKey) }) @@ -238,7 +232,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context) * * @param request the cancellation request. */ - override fun onRoomKeyRequestCancellation(request: IncomingRoomKeyRequestCancellation) { + override fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) { // see if we can find the request in the queue val userId = request.userId val deviceId = request.deviceId diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt index 2ef7b11b0e..3230686d58 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt @@ -71,4 +71,5 @@ sealed class RoomDetailAction : VectorViewModelAction { data class DeclineVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction() data class RequestVerification(val userId: String) : RoomDetailAction() data class ResumeVerification(val transactionId: String, val otherUserId: String?) : RoomDetailAction() + data class ReRequestKeys(val eventId: String) : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index f83adaf8a7..e748478e6a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -124,6 +124,7 @@ import im.vector.riotx.features.attachments.preview.AttachmentsPreviewActivity import im.vector.riotx.features.attachments.preview.AttachmentsPreviewArgs import im.vector.riotx.features.attachments.toGroupedContentAttachmentData import im.vector.riotx.features.command.Command +import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.riotx.features.crypto.util.toImageRes import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.home.AvatarRenderer @@ -1236,6 +1237,14 @@ class RoomDetailFragment @Inject constructor( is EventSharedAction.OnUrlLongClicked -> { onUrlLongClicked(action.url) } + is EventSharedAction.ReRequestKey -> { + roomDetailViewModel.handle(RoomDetailAction.ReRequestKeys(action.eventId)) + } + is EventSharedAction.UseKeyBackup -> { + context?.let { + startActivity(KeysBackupRestoreActivity.intent(it)) + } + } else -> { Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index f7a6c09022..2ad90f073a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -209,6 +209,7 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) is RoomDetailAction.RequestVerification -> handleRequestVerification(action) is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) + is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) } } @@ -886,6 +887,14 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun handleReRequestKeys(action: RoomDetailAction.ReRequestKeys) { + // Check if this request is still active and handled by me + room.getTimeLineEvent(action.eventId)?.let { + session.cryptoService().reRequestRoomKeyForEvent(it.root) + _viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.e2e_re_request_encryption_key_dialog_content))) + } + } + private fun handleReplyToOptions(action: RoomDetailAction.ReplyToOptions) { room.sendOptionsReply(action.eventId, action.optionIndex, action.optionValue) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt index b9e2ab2093..e38c055d52 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -102,4 +102,10 @@ sealed class EventSharedAction(@StringRes val titleRes: Int, // An url in the event preview has been long clicked data class OnUrlLongClicked(val url: String) : EventSharedAction(0, 0) + + data class ReRequestKey(val eventId: String) : + EventSharedAction(R.string.e2e_re_request_encryption_key, R.drawable.key_small) + + object UseKeyBackup : + EventSharedAction(R.string.e2e_use_keybackup, R.drawable.shield) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 1fe1db27d7..5212e1469d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -25,6 +25,7 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import dagger.Lazy import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.events.model.toModel @@ -209,6 +210,31 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } ?: "" } + private fun getRedactionReason(timelineEvent: TimelineEvent): String { + return (timelineEvent + .root + .unsignedData + ?.redactedEvent + ?.content + ?.get("reason") as? String) + ?.takeIf { it.isNotBlank() } + .let { reason -> + if (reason == null) { + if (timelineEvent.root.isRedactedBySameUser()) { + stringProvider.getString(R.string.event_redacted_by_user_reason) + } else { + stringProvider.getString(R.string.event_redacted_by_admin_reason) + } + } else { + if (timelineEvent.root.isRedactedBySameUser()) { + stringProvider.getString(R.string.event_redacted_by_user_reason_with_reason, reason) + } else { + stringProvider.getString(R.string.event_redacted_by_admin_reason_with_reason, reason) + } + } + } + } + private fun actionsForEvent(timelineEvent: TimelineEvent): List { val messageContent: MessageContent? = timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() ?: timelineEvent.root.getClearContent().toModel() @@ -272,8 +298,21 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } if (vectorPreferences.developerMode()) { + if (timelineEvent.isEncrypted() && timelineEvent.root.mCryptoError != null) { + val keysBackupService = session.cryptoService().keysBackupService() + if (keysBackupService.state == KeysBackupState.NotTrusted + || (keysBackupService.state == KeysBackupState.ReadyToBackUp + && keysBackupService.canRestoreKeys()) + ) { + add(EventSharedAction.UseKeyBackup) + } + if (session.cryptoService().getCryptoDeviceInfo(session.myUserId).size > 1) { + add(EventSharedAction.ReRequestKey(timelineEvent.eventId)) + } + } + add(EventSharedAction.ViewSource(timelineEvent.root.toContentStringWithIndent())) - if (timelineEvent.isEncrypted()) { + if (timelineEvent.isEncrypted() && timelineEvent.root.mxDecryptionResult != null) { val decryptedContent = timelineEvent.root.toClearContentStringWithIndent() ?: stringProvider.getString(R.string.encryption_information_decryption_error) add(EventSharedAction.ViewDecryptedSource(decryptedContent)) diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt index 0f1d5f466e..94d3860cca 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt @@ -187,7 +187,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context fun refreshNotificationDrawer() { // Implement last throttler - Timber.w("refreshNotificationDrawer()") + Timber.v("refreshNotificationDrawer()") backgroundHandler.removeCallbacksAndMessages(null) backgroundHandler.postDelayed( { @@ -197,7 +197,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context @WorkerThread private fun refreshNotificationDrawerBg() { - Timber.w("refreshNotificationDrawerBg()") + Timber.v("refreshNotificationDrawerBg()") val session = activeSessionHolder.getSafeActiveSession() ?: return 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..8760f9ebb2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsEpoxyController.kt @@ -0,0 +1,236 @@ +/* + * 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.OlmEventContent +import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent +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.matrix.android.internal.crypto.model.rest.SecretShareRequest +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.GenericItem +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()) + itemClickAction(GenericItem.Action("view").apply { perform = Runnable { interactionListener?.didTap(event) } }) + 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}" + } else if (event.getClearType() == EventType.SEND_SECRET) { + val content = event.getClearContent().toModel() + + span("\nrequestId:") { + textStyle = "bold" + } + +" ${content?.requestId}" + span("\nFrom Device:") { + textStyle = "bold" + } + +" ${event.mxDecryptionResult?.payload?.get("sender_device")}" + } else if (event.getClearType() == EventType.REQUEST_SECRET) { + 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("\nsecretName:") { + textStyle = "bold" + } + +" ${content.secretName}" + } + span("\nrequestedBy: ") { + textStyle = "bold" + } + +"${content?.requestingDeviceId}" + } + } + } + ) + } + } + } + } + } + + 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..d7ffd8adfa --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/GossipingEventsPaperTrailFragment.kt @@ -0,0 +1,73 @@ +/* + * 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.matrix.android.api.session.events.model.Event +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 im.vector.riotx.core.utils.jsonViewerStyler +import kotlinx.android.synthetic.main.fragment_generic_recycler.* +import org.billcarsonfr.jsonviewer.JSonViewerDialog +import javax.inject.Inject + +class GossipingEventsPaperTrailFragment @Inject constructor( + val viewModelFactory: GossipingEventsPaperTrailViewModel.Factory, + private val epoxyController: GossipingEventsEpoxyController, + private val colorProvider: ColorProvider +) : VectorBaseFragment(), GossipingEventsEpoxyController.InteractionListener { + + override fun getLayoutResId() = R.layout.fragment_generic_recycler + + 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 didTap(event: Event) { + if (event.isEncrypted()) { + event.toClearContentStringWithIndent() + } else { + event.toContentStringWithIndent() + }?.let { + JSonViewerDialog.newInstance( + it, + -1, + jsonViewerStyler(colorProvider) + ).show(childFragmentManager, "JSON_VIEWER") + } + } +} 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..7a5b08752e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/IncomingKeyRequestListFragment.kt @@ -0,0 +1,55 @@ +/* + * 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) + } + + override fun onDestroyView() { + super.onDestroyView() + recyclerView.cleanup() + } +} 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 new file mode 100644 index 0000000000..b5e1303d89 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestEpoxyController.kt @@ -0,0 +1,164 @@ +/* + * 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.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 +import javax.inject.Inject + +class KeyRequestEpoxyController @Inject constructor( + private val stringProvider: StringProvider +) : TypedEpoxyController() { + + interface InteractionListener { + // 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, + 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/KeyRequestListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestListViewModel.kt new file mode 100644 index 0000000000..3b273adaf4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestListViewModel.kt @@ -0,0 +1,87 @@ +/* + * 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.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.internal.crypto.IncomingRoomKeyRequest +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, + val outgoingRoomKeyRequest: Async> = Uninitialized +) : MvRxState + +class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState: KeyRequestListViewState, + private val session: Session) + : VectorViewModel(initialState) { + + init { + refresh() + } + + fun refresh() { + GlobalScope.launch { + session.cryptoService().getOutgoingRoomKeyRequest().let { + setState { + copy( + outgoingRoomKeyRequest = Success(it) + ) + } + } + session.cryptoService().getIncomingRoomKeyRequest().let { + setState { + copy( + incomingRequests = Success(it) + ) + } + } + } + } + + override fun handle(action: EmptyAction) {} + + @AssistedInject.Factory + interface Factory { + fun create(initialState: KeyRequestListViewState): KeyRequestListViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: KeyRequestListViewState): KeyRequestListViewModel? { + 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 new file mode 100644 index 0000000000..76174558b5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/KeyRequestsFragment.kt @@ -0,0 +1,105 @@ +/* + * 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 androidx.fragment.app.Fragment +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 +import im.vector.riotx.core.platform.VectorBaseFragment +import kotlinx.android.synthetic.main.fragment_devtool_keyrequests.* +import javax.inject.Inject + +class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() { + + override fun getLayoutResId(): Int = R.layout.fragment_devtool_keyrequests + + 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) + mPagerAdapter = KeyReqPagerAdapter(this) + devToolKeyRequestPager.adapter = mPagerAdapter + devToolKeyRequestPager.registerOnPageChangeCallback(pageAdapterListener) + + TabLayoutMediator(devToolKeyRequestTabs, devToolKeyRequestPager) { tab, position -> + when (position) { + 0 -> { + tab.text = "Outgoing" + } + 1 -> { + tab.text = "Incoming" + } + 2 -> { + tab.text = "Audit Trail" + } + } + }.attach() + } + + 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 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/OutgoingKeyRequestListFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devtools/OutgoingKeyRequestListFragment.kt new file mode 100644 index 0000000000..658497c23d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devtools/OutgoingKeyRequestListFragment.kt @@ -0,0 +1,55 @@ +/* + * 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 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 -> + 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/res/layout/fragment_devtool_keyrequests.xml b/vector/src/main/res/layout/fragment_devtool_keyrequests.xml new file mode 100644 index 0000000000..ccd3cee660 --- /dev/null +++ b/vector/src/main/res/layout/fragment_devtool_keyrequests.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index f0c3bdc23d..76bcfe7290 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -301,7 +301,7 @@ You need to log back in to generate end-to-end encryption keys for this session and submit the public key to your homeserver.\nThis is a once off.\nSorry for the inconvenience. - Re-request encryption keys from your other sessions. + Re-request encryption keys from your other sessions. Key request sent. diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index cb2a29de28..45fc3a3781 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -7,6 +7,13 @@ + + Key Requests + + Unlock encrypted messages history + + Refresh + @@ -18,7 +25,6 @@ When rooms are upgraded Troubleshoot Set notification importance by event - Refresh diff --git a/vector/src/main/res/xml/vector_settings_advanced_settings.xml b/vector/src/main/res/xml/vector_settings_advanced_settings.xml index 34cd8743be..e92aae3ff9 100644 --- a/vector/src/main/res/xml/vector_settings_advanced_settings.xml +++ b/vector/src/main/res/xml/vector_settings_advanced_settings.xml @@ -65,13 +65,20 @@ app:fragment="im.vector.riotx.features.settings.push.PushRulesFragment" /> - + + + \ No newline at end of file