diff --git a/changelog.d/5494.feature b/changelog.d/5494.feature new file mode 100644 index 0000000000..59b8a78a2c --- /dev/null +++ b/changelog.d/5494.feature @@ -0,0 +1 @@ +Use key backup before requesting keys + refactor & improvement of key request/forward \ No newline at end of file diff --git a/changelog.d/5559.sdk b/changelog.d/5559.sdk new file mode 100644 index 0000000000..2466fcef48 --- /dev/null +++ b/changelog.d/5559.sdk @@ -0,0 +1,4 @@ +- New API to enable/disable key forwarding CryptoService#enableKeyGossiping() +- New API to limit room key request only to own devices MXCryptoConfig#limitRoomKeyRequestsToMyDevices +- Event Trail API has changed, now using AuditTrail events +- New API to manually accept an incoming key request CryptoService#manuallyAcceptRoomKeyRequest() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index 4ead511c4d..dfb4863d5b 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.common import android.os.SystemClock import android.util.Log import androidx.lifecycle.Observer +import org.amshove.kluent.fail import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull @@ -31,8 +32,16 @@ import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo +import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey +import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod @@ -40,13 +49,19 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxStat import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner +import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.awaitCallback +import org.matrix.android.sdk.api.util.toBase64NoPadding import java.util.UUID import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -296,33 +311,94 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) { } } + /** + * Initialize cross-signing, set up megolm backup and save all in 4S + */ + fun bootstrapSecurity(session: Session) { + initializeCrossSigning(session) + val ssssService = session.sharedSecretStorageService() + testHelper.runBlockingTest { + val keyInfo = ssssService.generateKey( + UUID.randomUUID().toString(), + null, + "ssss_key", + EmptyKeySigner() + ) + ssssService.setDefaultKey(keyInfo.keyId) + + ssssService.storeSecret( + MASTER_KEY_SSSS_NAME, + session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master!!, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) + + ssssService.storeSecret( + SELF_SIGNING_KEY_SSSS_NAME, + session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned!!, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) + + ssssService.storeSecret( + USER_SIGNING_KEY_SSSS_NAME, + session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user!!, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) + + // set up megolm backup + val creationInfo = awaitCallback { + session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) + } + val version = awaitCallback { + session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) + } + // Save it for gossiping + session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) + + extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret -> + ssssService.storeSecret( + KEYBACKUP_SECRET_SSSS_NAME, + secret, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + ) + } + } + } + fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) { assertTrue(alice.cryptoService().crossSigningService().canCrossSign()) assertTrue(bob.cryptoService().crossSigningService().canCrossSign()) - val requestID = UUID.randomUUID().toString() val aliceVerificationService = alice.cryptoService().verificationService() val bobVerificationService = bob.cryptoService().verificationService() - aliceVerificationService.beginKeyVerificationInDMs( - VerificationMethod.SAS, - requestID, - roomId, - bob.myUserId, - bob.sessionParams.credentials.deviceId!! - ) + val localId = UUID.randomUUID().toString() + aliceVerificationService.requestKeyVerificationInDMs( + localId = localId, + methods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), + otherUserId = bob.myUserId, + roomId = roomId + ).transactionId - // we should reach SHOW SAS on both - var alicePovTx: OutgoingSasVerificationTransaction? = null - var bobPovTx: IncomingSasVerificationTransaction? = null - - // wait for alice to get the ready testHelper.waitWithLatch { testHelper.retryPeriodicallyWithLatch(it) { - bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction - Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") - if (bobPovTx?.state == VerificationTxState.OnStarted) { - bobPovTx?.performAccept() + bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull { + it.requestInfo?.fromDevice == alice.sessionParams.deviceId + } != null + } + } + val incomingRequest = bobVerificationService.getExistingVerificationRequests(alice.myUserId).first { + it.requestInfo?.fromDevice == alice.sessionParams.deviceId + } + bobVerificationService.readyPendingVerification(listOf(VerificationMethod.SAS), alice.myUserId, incomingRequest.transactionId!!) + + var requestID: String? = null + // wait for it to be readied + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + val outgoingRequest = aliceVerificationService.getExistingVerificationRequests(bob.myUserId) + .firstOrNull { it.localId == localId } + if (outgoingRequest?.isReady == true) { + requestID = outgoingRequest.transactionId!! true } else { false @@ -330,9 +406,20 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) { } } + aliceVerificationService.beginKeyVerificationInDMs( + VerificationMethod.SAS, + requestID!!, + roomId, + bob.myUserId, + bob.sessionParams.credentials.deviceId!!) + + // we should reach SHOW SAS on both + var alicePovTx: OutgoingSasVerificationTransaction? = null + var bobPovTx: IncomingSasVerificationTransaction? = null + testHelper.waitWithLatch { testHelper.retryPeriodicallyWithLatch(it) { - alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID) as? OutgoingSasVerificationTransaction + alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}") alicePovTx?.state == VerificationTxState.ShortCodeReady } @@ -340,7 +427,7 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) { // wait for alice to get the ready testHelper.waitWithLatch { testHelper.retryPeriodicallyWithLatch(it) { - bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction + bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID!!) as? IncomingSasVerificationTransaction Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") if (bobPovTx?.state == VerificationTxState.OnStarted) { bobPovTx?.performAccept() @@ -392,4 +479,50 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) { return CryptoTestData(roomId, sessions) } + + fun ensureCanDecrypt(sentEventIds: List, session: Session, e2eRoomID: String, messagesText: List) { + sentEventIds.forEachIndexed { index, sentEventId -> + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root + testHelper.runBlockingTest { + try { + session.cryptoService().decryptEvent(event, "").let { result -> + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } + } catch (error: MXCryptoError) { + // nop + } + } + Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}") + event.getClearType() == EventType.MESSAGE && + messagesText[index] == event.getClearContent()?.toModel()?.body + } + } + } + } + + fun ensureCannotDecrypt(sentEventIds: List, session: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType? = null) { + sentEventIds.forEach { sentEventId -> + val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root + testHelper.runBlockingTest { + try { + session.cryptoService().decryptEvent(event, "") + fail("Should not be able to decrypt event") + } catch (error: MXCryptoError) { + val errorType = (error as? MXCryptoError.Base)?.errorType + if (expectedError == null) { + assertNotNull(errorType) + } else { + assertEquals("Unexpected reason", expectedError, errorType) + } + } + } + } + } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt index ed922fdddc..ebe4c5ff6f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt @@ -30,13 +30,21 @@ import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.crypto.RequestResult import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult +import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest +import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod +import org.matrix.android.sdk.api.session.crypto.verification.VerificationService +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoomSummary @@ -52,15 +60,13 @@ import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestMatrixCallback +import java.util.concurrent.CountDownLatch @RunWith(JUnit4::class) @FixMethodOrder(MethodSorters.JVM) @LargeTest class E2eeSanityTests : InstrumentedTest { - private val testHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(testHelper) - /** * Simple test that create an e2ee room. * Some new members are added, and a message is sent. @@ -72,16 +78,24 @@ class E2eeSanityTests : InstrumentedTest { */ @Test fun testSendingE2EEMessages() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = cryptoTestData.firstSession val e2eRoomID = cryptoTestData.roomId val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! + // we want to disable key gossiping to just check initial sending of keys + aliceSession.cryptoService().enableKeyGossiping(false) + cryptoTestData.secondSession?.cryptoService()?.enableKeyGossiping(false) // add some more users and invite them val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu") .map { - testHelper.createAccount(it, SessionTestParams(true)) + testHelper.createAccount(it, SessionTestParams(true)).also { + it.cryptoService().enableKeyGossiping(false) + } } Log.v("#E2E TEST", "All accounts created") @@ -95,18 +109,18 @@ class E2eeSanityTests : InstrumentedTest { // All user should accept invite otherAccounts.forEach { otherSession -> - waitForAndAcceptInviteInRoom(otherSession, e2eRoomID) + waitForAndAcceptInviteInRoom(testHelper, otherSession, e2eRoomID) Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID") } // check that alice see them as joined (not really necessary?) - ensureMembersHaveJoined(aliceSession, otherAccounts, e2eRoomID) + ensureMembersHaveJoined(testHelper, aliceSession, otherAccounts, e2eRoomID) Log.v("#E2E TEST", "All users have joined the room") Log.v("#E2E TEST", "Alice is sending the message") val text = "This is my message" - val sentEventId: String? = sendMessageInRoom(aliceRoomPOV, text) + val sentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, text) // val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first() Assert.assertTrue("Message should be sent", sentEventId != null) @@ -114,10 +128,10 @@ class E2eeSanityTests : InstrumentedTest { otherAccounts.forEach { otherSession -> testHelper.waitWithLatch { latch -> testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!) - timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE + val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } } @@ -136,10 +150,10 @@ class E2eeSanityTests : InstrumentedTest { } newAccount.forEach { - waitForAndAcceptInviteInRoom(it, e2eRoomID) + waitForAndAcceptInviteInRoom(testHelper, it, e2eRoomID) } - ensureMembersHaveJoined(aliceSession, newAccount, e2eRoomID) + ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID) // wait a bit testHelper.runBlockingTest { @@ -164,7 +178,7 @@ class E2eeSanityTests : InstrumentedTest { Log.v("#E2E TEST", "Alice sends a new message") val secondMessage = "2 This is my message" - val secondSentEventId: String? = sendMessageInRoom(aliceRoomPOV, secondMessage) + val secondSentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, secondMessage) // new members should be able to decrypt it newAccount.forEach { otherSession -> @@ -188,6 +202,14 @@ class E2eeSanityTests : InstrumentedTest { cryptoTestData.cleanUp(testHelper) } + @Test + fun testKeyGossipingIsEnabledByDefault() { + val testHelper = CommonTestHelper(context()) + val session = testHelper.createAccount("alice", SessionTestParams(true)) + Assert.assertTrue("Key gossiping should be enabled by default", session.cryptoService().isKeyGossipingEnabled()) + testHelper.signOutAndClose(session) + } + /** * Quick test for basic key backup * 1. Create e2e between Alice and Bob @@ -204,6 +226,9 @@ class E2eeSanityTests : InstrumentedTest { */ @Test fun testBasicBackupImport() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession!! @@ -227,16 +252,16 @@ class E2eeSanityTests : InstrumentedTest { val sentEventIds = mutableListOf() val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning") messagesText.forEach { text -> - val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also { + val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also { sentEventIds.add(it) } testHelper.waitWithLatch { latch -> testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) - timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE + val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } // we want more so let's discard the session @@ -289,22 +314,23 @@ class E2eeSanityTests : InstrumentedTest { } } // after initial sync events are not decrypted, so we have to try manually - ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) + cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) // Let's now import keys from backup - newBobSession.cryptoService().keysBackupService().let { keysBackupService -> + newBobSession.cryptoService().keysBackupService().let { kbs -> val keyVersionResult = testHelper.doSync { - keysBackupService.getVersion(version.version, it) + kbs.getVersion(version.version, it) } val importedResult = testHelper.doSync { - keysBackupService.restoreKeyBackupWithPassword( + kbs.restoreKeyBackupWithPassword( keyVersionResult!!, keyBackupPassword, null, null, - null, it + null, + it ) } @@ -312,7 +338,7 @@ class E2eeSanityTests : InstrumentedTest { } // ensure bob can now decrypt - ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) + cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) testHelper.signOutAndClose(newBobSession) } @@ -323,6 +349,9 @@ class E2eeSanityTests : InstrumentedTest { */ @Test fun testSimpleGossip() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = cryptoTestData.firstSession val bobSession = cryptoTestData.secondSession!! @@ -330,30 +359,28 @@ class E2eeSanityTests : InstrumentedTest { val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! - cryptoTestHelper.initializeCrossSigning(bobSession) - // let's send a few message to bob val sentEventIds = mutableListOf() val messagesText = listOf("1. Hello", "2. Bob") Log.v("#E2E TEST", "Alice sends some messages") messagesText.forEach { text -> - val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also { + val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also { sentEventIds.add(it) } testHelper.waitWithLatch { latch -> testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) - timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE + val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } } // Ensure bob can decrypt - ensureIsDecrypted(sentEventIds, bobSession, e2eRoomID) + ensureIsDecrypted(testHelper, sentEventIds, bobSession, e2eRoomID) // Let's now add a new bob session // Create a new session for bob @@ -363,7 +390,11 @@ class E2eeSanityTests : InstrumentedTest { // check that new bob can't currently decrypt Log.v("#E2E TEST", "check that new bob can't currently decrypt") - ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) + cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) +// newBobSession.cryptoService().getOutgoingRoomKeyRequests() +// .firstOrNull { +// it.sessionId == +// } // Try to request sentEventIds.forEach { sentEventId -> @@ -372,12 +403,34 @@ class E2eeSanityTests : InstrumentedTest { } // wait a bit - testHelper.runBlockingTest { - delay(10_000) - } + // we need to wait a couple of syncs to let sharing occurs +// testHelper.waitFewSyncs(newBobSession, 6) // Ensure that new bob still can't decrypt (keys must have been withheld) - ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.KEYS_WITHHELD) + sentEventIds.forEach { sentEventId -> + val megolmSessionId = newBobSession.getRoom(e2eRoomID)!! + .getTimelineEvent(sentEventId)!! + .root.content.toModel()!!.sessionId + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests() + .first { + it.sessionId == megolmSessionId && + it.roomId == e2eRoomID + } + .results.also { + Log.w("##TEST", "result list is $it") + } + .firstOrNull { it.userId == aliceSession.myUserId } + ?.result + aliceReply != null && + aliceReply is RequestResult.Failure && + WithHeldCode.UNAUTHORISED == aliceReply.code + } + } + } + + cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) // Now mark new bob session as verified @@ -390,12 +443,7 @@ class E2eeSanityTests : InstrumentedTest { newBobSession.cryptoService().reRequestRoomKeyForEvent(event) } - // wait a bit - testHelper.runBlockingTest { - delay(10_000) - } - - ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) + cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) cryptoTestData.cleanUp(testHelper) testHelper.signOutAndClose(newBobSession) @@ -406,6 +454,9 @@ class E2eeSanityTests : InstrumentedTest { */ @Test fun testForwardBetterKey() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val aliceSession = cryptoTestData.firstSession val bobSessionWithBetterKey = cryptoTestData.secondSession!! @@ -413,35 +464,33 @@ class E2eeSanityTests : InstrumentedTest { val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! - cryptoTestHelper.initializeCrossSigning(bobSessionWithBetterKey) - // let's send a few message to bob var firstEventId: String val firstMessage = "1. Hello" Log.v("#E2E TEST", "Alice sends some messages") firstMessage.let { text -> - firstEventId = sendMessageInRoom(aliceRoomPOV, text)!! + firstEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!! testHelper.waitWithLatch { latch -> testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId) - timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE + val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } } // Ensure bob can decrypt - ensureIsDecrypted(listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID) + ensureIsDecrypted(testHelper, listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID) // Let's add a new unverified session from bob val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true)) // check that new bob can't currently decrypt Log.v("#E2E TEST", "check that new bob can't currently decrypt") - ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) + cryptoTestHelper.ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, null) // Now let alice send a new message. this time the new bob session will be able to decrypt var secondEventId: String @@ -449,14 +498,14 @@ class E2eeSanityTests : InstrumentedTest { Log.v("#E2E TEST", "Alice sends some messages") secondMessage.let { text -> - secondEventId = sendMessageInRoom(aliceRoomPOV, text)!! + secondEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!! testHelper.waitWithLatch { latch -> testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId) - timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE + val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } } @@ -475,9 +524,7 @@ class E2eeSanityTests : InstrumentedTest { try { newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") fail("Should not be able to decrypt event") - } catch (error: MXCryptoError) { - val errorType = (error as? MXCryptoError.Base)?.errorType - assertEquals(MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, errorType) + } catch (_: MXCryptoError) { } } @@ -499,41 +546,45 @@ class E2eeSanityTests : InstrumentedTest { .markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!) // now let new session request - newBobSession.cryptoService().requestRoomKeyForEvent(firstEventNewBobPov.root) + newBobSession.cryptoService().reRequestRoomKeyForEvent(firstEventNewBobPov.root) - // wait a bit - testHelper.runBlockingTest { - delay(10_000) - } + // We need to wait for the key request to be sent out and then a reply to be received // old session should have shared the key at earliest known index now // we should be able to decrypt both - testHelper.runBlockingTest { - try { - newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") - } catch (error: MXCryptoError) { - fail("Should be able to decrypt first event now $error") - } - } - testHelper.runBlockingTest { - try { - newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") - } catch (error: MXCryptoError) { - fail("Should be able to decrypt event $error") + testHelper.waitWithLatch { + testHelper.retryPeriodicallyWithLatch(it) { + val canDecryptFirst = try { + testHelper.runBlockingTest { + newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") + } + true + } catch (error: MXCryptoError) { + false + } + val canDecryptSecond = try { + testHelper.runBlockingTest { + newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") + } + true + } catch (error: MXCryptoError) { + false + } + canDecryptFirst && canDecryptSecond } } - cryptoTestData.cleanUp(testHelper) + testHelper.signOutAndClose(aliceSession) + testHelper.signOutAndClose(bobSessionWithBetterKey) testHelper.signOutAndClose(newBobSession) } - private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? { + private fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? { aliceRoomPOV.sendService().sendTextMessage(text) var sentEventId: String? = null testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch -> val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60)) timeline.start() - testHelper.retryPeriodicallyWithLatch(latch) { val decryptedMsg = timeline.getSnapshot() .filter { it.root.getClearType() == EventType.MESSAGE } @@ -552,7 +603,157 @@ class E2eeSanityTests : InstrumentedTest { return sentEventId } - private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List, e2eRoomID: String) { + /** + * Test that if a better key is forwared (lower index, it is then used) + */ + @Test + fun testSelfInteractiveVerificationAndGossip() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + + val aliceSession = testHelper.createAccount("alice", SessionTestParams(true)) + cryptoTestHelper.bootstrapSecurity(aliceSession) + + // now let's create a new login from alice + + val aliceNewSession = testHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) + + val oldCompleteLatch = CountDownLatch(1) + lateinit var oldCode: String + aliceSession.cryptoService().verificationService().addListener(object : VerificationService.Listener { + + override fun verificationRequestUpdated(pr: PendingVerificationRequest) { + val readyInfo = pr.readyInfo + if (readyInfo != null) { + aliceSession.cryptoService().verificationService().beginKeyVerification( + VerificationMethod.SAS, + aliceSession.myUserId, + readyInfo.fromDevice, + readyInfo.transactionId + + ) + } + } + + override fun transactionUpdated(tx: VerificationTransaction) { + Log.d("##TEST", "exitsingPov: $tx") + val sasTx = tx as OutgoingSasVerificationTransaction + when (sasTx.uxState) { + OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { + // for the test we just accept? + oldCode = sasTx.getDecimalCodeRepresentation() + sasTx.userHasVerifiedShortCode() + } + OutgoingSasVerificationTransaction.UxState.VERIFIED -> { + // we can release this latch? + oldCompleteLatch.countDown() + } + else -> Unit + } + } + }) + + val newCompleteLatch = CountDownLatch(1) + lateinit var newCode: String + aliceNewSession.cryptoService().verificationService().addListener(object : VerificationService.Listener { + + override fun verificationRequestCreated(pr: PendingVerificationRequest) { + // let's ready + aliceNewSession.cryptoService().verificationService().readyPendingVerification( + listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), + aliceSession.myUserId, + pr.transactionId!! + ) + } + + var matchOnce = true + override fun transactionUpdated(tx: VerificationTransaction) { + Log.d("##TEST", "newPov: $tx") + + val sasTx = tx as IncomingSasVerificationTransaction + when (sasTx.uxState) { + IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { + // no need to accept as there was a request first it will auto accept + } + IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { + if (matchOnce) { + sasTx.userHasVerifiedShortCode() + newCode = sasTx.getDecimalCodeRepresentation() + matchOnce = false + } + } + IncomingSasVerificationTransaction.UxState.VERIFIED -> { + newCompleteLatch.countDown() + } + else -> Unit + } + } + }) + + // initiate self verification + aliceSession.cryptoService().verificationService().requestKeyVerification( + listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW), + aliceNewSession.myUserId, + listOf(aliceNewSession.sessionParams.deviceId!!) + ) + testHelper.await(oldCompleteLatch) + testHelper.await(newCompleteLatch) + assertEquals("Decimal code should have matched", oldCode, newCode) + + // Assert that devices are verified + val newDeviceFromOldPov: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId) + val oldDeviceFromNewPov: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId) + + Assert.assertTrue("new device should be verified from old point of view", newDeviceFromOldPov!!.isVerified) + Assert.assertTrue("old device should be verified from new point of view", oldDeviceFromNewPov!!.isVerified) + + // wait for secret gossiping to happen + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown() + } + } + + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null + } + } + + assertEquals( + "MSK Private parts should be the same", + aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master, + aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master + ) + assertEquals( + "USK Private parts should be the same", + aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user, + aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user + ) + + assertEquals( + "SSK Private parts should be the same", + aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned, + aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned + ) + + // Let's check that we have the megolm backup key + assertEquals( + "Megolm key should be the same", + aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey, + aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey + ) + assertEquals( + "Megolm version should be the same", + aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version, + aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version + ) + + testHelper.signOutAndClose(aliceSession) + testHelper.signOutAndClose(aliceNewSession) + } + + private fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List, e2eRoomID: String) { testHelper.waitWithLatch { latch -> testHelper.retryPeriodicallyWithLatch(latch) { otherAccounts.map { @@ -564,7 +765,7 @@ class E2eeSanityTests : InstrumentedTest { } } - private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) { + private fun waitForAndAcceptInviteInRoom(testHelper: CommonTestHelper, otherSession: Session, e2eRoomID: String) { testHelper.waitWithLatch { latch -> testHelper.retryPeriodicallyWithLatch(latch) { val roomSummary = otherSession.getRoomSummary(e2eRoomID) @@ -576,7 +777,8 @@ class E2eeSanityTests : InstrumentedTest { } } - testHelper.runBlockingTest(60_000) { + // not sure why it's taking so long :/ + testHelper.runBlockingTest(90_000) { Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID") try { otherSession.roomService().joinRoom(e2eRoomID) @@ -594,59 +796,14 @@ class E2eeSanityTests : InstrumentedTest { } } - private fun ensureCanDecrypt(sentEventIds: MutableList, session: Session, e2eRoomID: String, messagesText: List) { - sentEventIds.forEachIndexed { index, sentEventId -> - testHelper.waitWithLatch { latch -> - testHelper.retryPeriodicallyWithLatch(latch) { - val event = session.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root - testHelper.runBlockingTest { - try { - session.cryptoService().decryptEvent(event, "").let { result -> - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - } - } catch (error: MXCryptoError) { - // nop - } - } - event.getClearType() == EventType.MESSAGE && - messagesText[index] == event.getClearContent()?.toModel()?.body - } - } - } - } - - private fun ensureIsDecrypted(sentEventIds: List, session: Session, e2eRoomID: String) { + private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List, session: Session, e2eRoomID: String) { testHelper.waitWithLatch { latch -> sentEventIds.forEach { sentEventId -> testHelper.retryPeriodicallyWithLatch(latch) { - val timelineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) - timelineEvent != null && - timelineEvent.isEncrypted() && - timelineEvent.root.getClearType() == EventType.MESSAGE - } - } - } - } - - private fun ensureCannotDecrypt(sentEventIds: List, newBobSession: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType?) { - sentEventIds.forEach { sentEventId -> - val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root - testHelper.runBlockingTest { - try { - newBobSession.cryptoService().decryptEvent(event, "") - fail("Should not be able to decrypt event") - } catch (error: MXCryptoError) { - val errorType = (error as? MXCryptoError.Base)?.errorType - if (expectedError == null) { - Assert.assertNotNull(errorType) - } else { - assertEquals(expectedError, errorType, "Message expected to be UISI") - } + val timeLineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) + timeLineEvent != null && + timeLineEvent.isEncrypted() && + timeLineEvent.root.getClearType() == EventType.MESSAGE } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt index 8a1edec5e3..93aa78a305 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt @@ -27,7 +27,6 @@ import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.getTimelineEvent @@ -51,10 +50,7 @@ class PreShareKeysTest : InstrumentedTest { // clear any outbound session aliceSession.cryptoService().discardOutboundSession(e2eRoomID) - val preShareCount = bobSession.cryptoService().getGossipingEvents().count { - it.senderId == aliceSession.myUserId && - it.getClearType() == EventType.ROOM_KEY - } + val preShareCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys() assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount) Log.d("#Test", "Room Key Received from alice $preShareCount") @@ -66,23 +62,23 @@ class PreShareKeysTest : InstrumentedTest { testHelper.waitWithLatch { latch -> testHelper.retryPeriodicallyWithLatch(latch) { - val newGossipCount = bobSession.cryptoService().getGossipingEvents().count { - it.senderId == aliceSession.myUserId && - it.getClearType() == EventType.ROOM_KEY - } - newGossipCount > preShareCount + val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys() + newKeysCount > preShareCount } } - val latest = bobSession.cryptoService().getGossipingEvents().lastOrNull { - it.senderId == aliceSession.myUserId && - it.getClearType() == EventType.ROOM_KEY - } + val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting + val aliceOutboundSessionInRoom = aliceCryptoStore.getCurrentOutboundGroupSessionForRoom(e2eRoomID)!!.outboundGroupSession.sessionIdentifier() - val content = latest?.getClearContent().toModel() - assertNotNull("Bob should have received and decrypted a room key event from alice", content) - assertEquals("Wrong room", e2eRoomID, content!!.roomId) - val megolmSessionId = content.sessionId!! + val bobCryptoStore = (bobSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting + val aliceDeviceBobPov = bobCryptoStore.getUserDevice(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!)!! + val bobInboundForAlice = bobCryptoStore.getInboundGroupSession(aliceOutboundSessionInRoom, aliceDeviceBobPov.identityKey()!!) + assertNotNull("Bob should have received and decrypted a room key event from alice", bobInboundForAlice) + assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId) + + val megolmSessionId = bobInboundForAlice.olmInboundGroupSession!!.sessionIdentifier() + + assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId) val sharedIndex = aliceSession.cryptoService().getSharedWithInfo(e2eRoomID, megolmSessionId) .getObject(bobSession.myUserId, bobSession.sessionParams.deviceId) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt index 5066a4339f..2e4fd62822 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt @@ -19,59 +19,45 @@ package org.matrix.android.sdk.internal.crypto.gossiping import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest -import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue import junit.framework.TestCase.fail +import org.amshove.kluent.internal.assertEquals import org.junit.Assert +import org.junit.Assert.assertNull import org.junit.FixMethodOrder -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor -import org.matrix.android.sdk.api.auth.UserPasswordAuth -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse -import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState +import org.matrix.android.sdk.api.session.crypto.RequestResult import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion -import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo -import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState -import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod -import org.matrix.android.sdk.api.session.crypto.verification.VerificationService -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction -import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams -import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.TestConstants -import kotlin.coroutines.Continuation -import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) @LargeTest class KeyShareTests : InstrumentedTest { - private val commonTestHelper = CommonTestHelper(context()) - @Test - @Ignore("This test will be ignored until it is fixed") fun test_DoNotSelfShareIfNotTrusted() { + val commonTestHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}") // Create an encrypted room and add a message val roomId = commonTestHelper.runBlockingTest { @@ -86,11 +72,18 @@ class KeyShareTests : InstrumentedTest { assertNotNull(room) Thread.sleep(4_000) assertTrue(room?.roomCryptoService()?.isEncrypted() == true) - val sentEventId = commonTestHelper.sendTextMessage(room!!, "My Message", 1).first().eventId - // Open a new sessionx + val sentEvent = commonTestHelper.sendTextMessage(room!!, "My Message", 1).first() + val sentEventId = sentEvent.eventId + val sentEventText = sentEvent.getLastMessageContent()?.body - val aliceSession2 = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) + // Open a new session + val aliceSession2 = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(false)) + // block key requesting for now as decrypt will send requests (room summary is trying to decrypt) + aliceSession2.cryptoService().enableKeyGossiping(false) + commonTestHelper.syncSession(aliceSession2) + + Log.v("TEST", "=======> AliceSession 2 is ${aliceSession2.sessionParams.deviceId}") val roomSecondSessionPOV = aliceSession2.getRoom(roomId) @@ -107,7 +100,10 @@ class KeyShareTests : InstrumentedTest { } val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests() + assertEquals("There should be no request as it's disabled", 0, outgoingRequestsBefore.size) + // Try to request + aliceSession2.cryptoService().enableKeyGossiping(true) aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root) val eventMegolmSessionId = receivedEvent.root.content.toModel()?.sessionId @@ -117,10 +113,6 @@ class KeyShareTests : InstrumentedTest { commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodicallyWithLatch(latch) { aliceSession2.cryptoService().getOutgoingRoomKeyRequests() - .filter { req -> - // filter out request that was known before - !outgoingRequestsBefore.any { req.requestId == it.requestId } - } .let { val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId } outGoingRequestId = outgoing?.requestId @@ -141,20 +133,34 @@ class KeyShareTests : InstrumentedTest { commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodicallyWithLatch(latch) { // DEBUG LOGS - aliceSession.cryptoService().getIncomingRoomKeyRequests().let { - Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)") +// aliceSession.cryptoService().getIncomingRoomKeyRequests().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}") +// } +// Log.v("TEST", "=========================") +// } + + val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } + incoming != null + } + } + + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + // DEBUG LOGS + aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest -> 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", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") + Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") Log.v("TEST", "=========================") } - val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } - incoming?.state == GossipingRequestState.REJECTED + val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } + val reply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + val resultCode = (reply?.result as? RequestResult.Failure)?.code + resultCode == WithHeldCode.UNVERIFIED } } @@ -175,254 +181,301 @@ class KeyShareTests : InstrumentedTest { // Re request aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession.cryptoService().getIncomingRoomKeyRequests().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) - commonTestHelper.waitWithLatch { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession2.cryptoService().getOutgoingRoomKeyRequests().let { - it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == OutgoingGossipingRequestState.CANCELLED } - } - } - } - - try { - commonTestHelper.runBlockingTest { - aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo") - } - } catch (failure: Throwable) { - fail("should have been able to decrypt") - } + cryptoTestHelper.ensureCanDecrypt(listOf(receivedEvent.eventId), aliceSession2, roomId, listOf(sentEventText ?: "")) commonTestHelper.signOutAndClose(aliceSession) commonTestHelper.signOutAndClose(aliceSession2) } + // See E2ESanityTest for a test regarding secret sharing + + /** + * Test that the sender of a message accepts to re-share to another user + * if the key was originally shared with him + */ @Test - @Ignore("This test will be ignored until it is fixed") - fun test_ShareSSSSSecret() { - val aliceSession1 = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + fun test_reShareIfWasIntendedToBeShared() { + val commonTestHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(commonTestHelper) - commonTestHelper.doSync { - aliceSession1.cryptoService().crossSigningService() - .initializeCrossSigning( - object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume( - UserPasswordAuth( - user = aliceSession1.myUserId, - password = TestConstants.PASSWORD - ) - ) - } - }, it - ) - } + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = testData.firstSession + val roomFromAlice = aliceSession.getRoom(testData.roomId)!! + val bobSession = testData.secondSession!! - // Also bootstrap keybackup on first session - val creationInfo = commonTestHelper.doSync { - aliceSession1.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) - } - val version = commonTestHelper.doSync { - aliceSession1.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) - } - // Save it for gossiping - aliceSession1.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) + val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first() + val sentEventMegolmSession = sentEvent.root.content.toModel()!!.sessionId!! - val aliceSession2 = commonTestHelper.logIntoAccount(aliceSession1.myUserId, SessionTestParams(true)) + // bob should be able to decrypt + cryptoTestHelper.ensureCanDecrypt(listOf(sentEvent.eventId), bobSession, testData.roomId, listOf(sentEvent.getLastMessageContent()?.body ?: "")) - val aliceVerificationService1 = aliceSession1.cryptoService().verificationService() - val aliceVerificationService2 = aliceSession2.cryptoService().verificationService() - - // force keys download - commonTestHelper.doSync> { - aliceSession1.cryptoService().downloadKeys(listOf(aliceSession1.myUserId), true, it) - } - commonTestHelper.doSync> { - aliceSession2.cryptoService().downloadKeys(listOf(aliceSession2.myUserId), true, it) - } - - var session1ShortCode: String? = null - var session2ShortCode: String? = null - - aliceVerificationService1.addListener(object : VerificationService.Listener { - override fun transactionUpdated(tx: VerificationTransaction) { - Log.d("#TEST", "AA: tx incoming?:${tx.isIncoming} state ${tx.state}") - if (tx is SasVerificationTransaction) { - if (tx.state == VerificationTxState.OnStarted) { - (tx as IncomingSasVerificationTransaction).performAccept() - } - if (tx.state == VerificationTxState.ShortCodeReady) { - session1ShortCode = tx.getDecimalCodeRepresentation() - Thread.sleep(500) - tx.userHasVerifiedShortCode() - } - } - } - }) - - aliceVerificationService2.addListener(object : VerificationService.Listener { - override fun transactionUpdated(tx: VerificationTransaction) { - Log.d("#TEST", "BB: tx incoming?:${tx.isIncoming} state ${tx.state}") - if (tx is SasVerificationTransaction) { - if (tx.state == VerificationTxState.ShortCodeReady) { - session2ShortCode = tx.getDecimalCodeRepresentation() - Thread.sleep(500) - tx.userHasVerifiedShortCode() - } - } - } - }) - - val txId = "m.testVerif12" - aliceVerificationService2.beginKeyVerification( - VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId - ?: "", txId - ) + // Let's try to request any how. + // As it was share previously alice should accept to reshare + bobSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root) commonTestHelper.waitWithLatch { latch -> commonTestHelper.retryPeriodicallyWithLatch(latch) { - aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.deviceId ?: "")?.isVerified == true + val outgoing = bobSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val aliceReply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + aliceReply != null && aliceReply.result is RequestResult.Success } } - - assertNotNull(session1ShortCode) - Log.d("#TEST", "session1ShortCode: $session1ShortCode") - assertNotNull(session2ShortCode) - Log.d("#TEST", "session2ShortCode: $session2ShortCode") - assertEquals(session1ShortCode, session2ShortCode) - - // SSK and USK private keys should have been shared - - commonTestHelper.waitWithLatch(60_000) { latch -> - commonTestHelper.retryPeriodicallyWithLatch(latch) { - Log.d("#TEST", "CAN XS :${aliceSession2.cryptoService().crossSigningService().getMyCrossSigningKeys()}") - aliceSession2.cryptoService().crossSigningService().canCrossSign() - } - } - - // Test that key backup key has been shared to - commonTestHelper.waitWithLatch(60_000) { latch -> - val keysBackupService = aliceSession2.cryptoService().keysBackupService() - commonTestHelper.retryPeriodicallyWithLatch(latch) { - Log.d("#TEST", "Recovery :${keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}") - keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey - } - } - - commonTestHelper.signOutAndClose(aliceSession1) - commonTestHelper.signOutAndClose(aliceSession2) } + /** + * Test that our own devices accept to reshare to unverified device if it was shared initialy + * if the key was originally shared with him + */ @Test - @Ignore("This test will be ignored until it is fixed") - fun test_ImproperKeyShareBug() { - val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) + fun test_reShareToUnverifiedIfWasIntendedToBeShared() { + val commonTestHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(commonTestHelper) - commonTestHelper.doSync { - aliceSession.cryptoService().crossSigningService() - .initializeCrossSigning( - object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume( - UserPasswordAuth( - user = aliceSession.myUserId, - password = TestConstants.PASSWORD, - session = flowResponse.session - ) - ) - } - }, it - ) + val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true) + val aliceSession = testData.firstSession + val roomFromAlice = aliceSession.getRoom(testData.roomId)!! + + val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) + + // we wait for alice first session to be aware of that session? + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val newSession = aliceSession.cryptoService().getUserDevices(aliceSession.myUserId) + .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } + newSession != null + } } + val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first() + val sentEventMegolmSession = sentEvent.root.content.toModel()!!.sessionId!! - // Create an encrypted room and send a couple of messages - val roomId = commonTestHelper.runBlockingTest { - aliceSession.roomService().createRoom( - CreateRoomParams().apply { - visibility = RoomDirectoryVisibility.PRIVATE - enableEncryption() - } - ) + // Let's try to request any how. + // As it was share previously alice should accept to reshare + aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root) + + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val ownDeviceReply = + outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + ownDeviceReply != null && ownDeviceReply.result is RequestResult.Success + } } - val roomAlicePov = aliceSession.getRoom(roomId) - assertNotNull(roomAlicePov) - Thread.sleep(1_000) - assertTrue(roomAlicePov?.roomCryptoService()?.isEncrypted() == true) - val secondEventId = commonTestHelper.sendTextMessage(roomAlicePov!!, "Message", 3)[1].eventId + } - // Create bob session + /** + * Tests that keys reshared with own verified session are done from the earliest known index + */ + @Test + fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() { + val commonTestHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(commonTestHelper) - val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true)) - commonTestHelper.doSync { - bobSession.cryptoService().crossSigningService() - .initializeCrossSigning( - object : UserInteractiveAuthInterceptor { - override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { - promise.resume( - UserPasswordAuth( - user = bobSession.myUserId, - password = TestConstants.PASSWORD, - session = flowResponse.session - ) - ) - } - }, it - ) - } + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = testData.firstSession + val bobSession = testData.secondSession!! + val roomFromBob = bobSession.getRoom(testData.roomId)!! - // Let alice invite bob - commonTestHelper.runBlockingTest { - roomAlicePov.membershipService().invite(bobSession.myUserId, null) - } + val sentEvents = commonTestHelper.sendTextMessage(roomFromBob, "Hello", 3) + val sentEventMegolmSession = sentEvents.first().root.content.toModel()!!.sessionId!! - commonTestHelper.runBlockingTest { - bobSession.roomService().joinRoom(roomAlicePov.roomId, null, emptyList()) - } + // Let alice now add a new session + val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(false)) + aliceNewSession.cryptoService().enableKeyGossiping(false) + commonTestHelper.syncSession(aliceNewSession) - // we want to discard alice outbound session - aliceSession.cryptoService().discardOutboundSession(roomAlicePov.roomId) - - // and now resend a new message to reset index to 0 - commonTestHelper.sendTextMessage(roomAlicePov, "After", 1) - - val roomRoomBobPov = aliceSession.getRoom(roomId) - val beforeJoin = roomRoomBobPov!!.getTimelineEvent(secondEventId) - - var dRes = tryOrNull { - commonTestHelper.runBlockingTest { - bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") + // we wait bob first session to be aware of that session? + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId) + .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } + newSession != null } } - assert(dRes == null) + val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first() + val newEventId = newEvent.eventId + val newEventText = newEvent.getLastMessageContent()!!.body - // Try to re-ask the keys + // alice should be able to decrypt the new one + cryptoTestHelper.ensureCanDecrypt(listOf(newEventId), aliceNewSession, testData.roomId, listOf(newEventText)) + // but not the first one! + cryptoTestHelper.ensureCannotDecrypt(sentEvents.map { it.eventId }, aliceNewSession, testData.roomId) - bobSession.cryptoService().reRequestRoomKeyForEvent(beforeJoin!!.root) + // All should be using the same session id + sentEvents.forEach { + assertEquals(sentEventMegolmSession, it.root.content.toModel()!!.sessionId) + } + assertEquals(sentEventMegolmSession, newEvent.root.content.toModel()!!.sessionId) - Thread.sleep(3_000) + // Request a first time, bob should reply with unauthorized and alice should reply with unverified + aliceNewSession.cryptoService().enableKeyGossiping(true) + aliceNewSession.cryptoService().reRequestRoomKeyForEvent(newEvent.root) - // With the bug the first session would have improperly reshare that key :/ - dRes = tryOrNull { - commonTestHelper.runBlockingTest { - bobSession.cryptoService().decryptEvent(beforeJoin.root, "") + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val ownDeviceReply = outgoing?.results + ?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + val result = ownDeviceReply?.result + Log.v("TEST", "own device result is $result") + result != null && result is RequestResult.Failure && result.code == WithHeldCode.UNVERIFIED } } - Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel()?.body}") - assert(dRes?.clearEvent == null) + + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val bobDeviceReply = outgoing?.results + ?.firstOrNull { it.userId == bobSession.myUserId && it.fromDevice == bobSession.sessionParams.deviceId } + val result = bobDeviceReply?.result + Log.v("TEST", "bob device result is $result") + result != null && result is RequestResult.Success && result.chainIndex > 0 + } + } + + // it's a success but still can't decrypt first message + cryptoTestHelper.ensureCannotDecrypt(sentEvents.map { it.eventId }, aliceNewSession, testData.roomId) + + // Mark the new session as verified + aliceSession.cryptoService() + .verificationService() + .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!) + + // Let's now try to request + aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root) + + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + // DEBUG LOGS + aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest -> + Log.v("TEST", "=========================") + Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") + Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") + Log.v("TEST", "=========================") + } + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val ownDeviceReply = + outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + val result = ownDeviceReply?.result + result != null && result is RequestResult.Success && result.chainIndex == 0 + } + } + + // now the new session should be able to decrypt all! + cryptoTestHelper.ensureCanDecrypt( + sentEvents.map { it.eventId }, + aliceNewSession, + testData.roomId, + sentEvents.map { it.getLastMessageContent()!!.body } + ) + + // Additional test, can we check that bob replied successfully but with a ratcheted key + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId } + val result = bobReply?.result + result != null && result is RequestResult.Success && result.chainIndex == 3 + } + } + + commonTestHelper.signOutAndClose(aliceNewSession) + commonTestHelper.signOutAndClose(aliceSession) + commonTestHelper.signOutAndClose(bobSession) + } + + /** + * Tests that we don't cancel a request to early on first forward if the index is not good enough + */ + @Test + fun test_dontCancelToEarly() { + val commonTestHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) + val aliceSession = testData.firstSession + val bobSession = testData.secondSession!! + val roomFromBob = bobSession.getRoom(testData.roomId)!! + + val sentEvents = commonTestHelper.sendTextMessage(roomFromBob, "Hello", 3) + val sentEventMegolmSession = sentEvents.first().root.content.toModel()!!.sessionId!! + + // Let alice now add a new session + val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true)) + + // we wait bob first session to be aware of that session? + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId) + .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId } + newSession != null + } + } + + val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first() + val newEventId = newEvent.eventId + val newEventText = newEvent.getLastMessageContent()!!.body + + // alice should be able to decrypt the new one + cryptoTestHelper.ensureCanDecrypt(listOf(newEventId), aliceNewSession, testData.roomId, listOf(newEventText)) + // but not the first one! + cryptoTestHelper.ensureCannotDecrypt(sentEvents.map { it.eventId }, aliceNewSession, testData.roomId) + + // All should be using the same session id + sentEvents.forEach { + assertEquals(sentEventMegolmSession, it.root.content.toModel()!!.sessionId) + } + assertEquals(sentEventMegolmSession, newEvent.root.content.toModel()!!.sessionId) + + // Mark the new session as verified + aliceSession.cryptoService() + .verificationService() + .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!) + + // /!\ Stop initial alice session syncing so that it can't reply + aliceSession.cryptoService().enableKeyGossiping(false) + aliceSession.stopSync() + + // Let's now try to request + aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root) + + // Should get a reply from bob and not from alice + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + // Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}") + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId } + val result = bobReply?.result + result != null && result is RequestResult.Success && result.chainIndex == 3 + } + } + + val outgoingReq = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + + assertNull("We should not have a reply from first session", outgoingReq!!.results.firstOrNull { it.fromDevice == aliceSession.sessionParams.deviceId }) + assertEquals("The request should not be canceled", OutgoingRoomKeyRequestState.SENT, outgoingReq.state) + + // let's wake up alice + aliceSession.cryptoService().enableKeyGossiping(true) + aliceSession.startSync(true) + + // We should now get a reply from first session + commonTestHelper.waitWithLatch { latch -> + commonTestHelper.retryPeriodicallyWithLatch(latch) { + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + val ownDeviceReply = + outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId } + val result = ownDeviceReply?.result + result != null && result is RequestResult.Success && result.chainIndex == 0 + } + } + + // It should be in sent then cancel + val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession } + assertEquals("The request should be canceled", OutgoingRoomKeyRequestState.SENT_THEN_CANCELED, outgoing!!.state) + + commonTestHelper.signOutAndClose(aliceNewSession) + commonTestHelper.signOutAndClose(aliceSession) + commonTestHelper.signOutAndClose(bobSession) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt index b3896b02de..cb31a2232f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt @@ -21,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import org.junit.Assert import org.junit.FixMethodOrder -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -29,6 +28,7 @@ import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.crypto.RequestResult import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode @@ -46,12 +46,11 @@ import org.matrix.android.sdk.common.TestConstants @LargeTest class WithHeldTests : InstrumentedTest { - private val testHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(testHelper) - @Test - @Ignore("This test will be ignored until it is fixed") fun test_WithHeldUnverifiedReason() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + // ============================= // ARRANGE // ============================= @@ -69,7 +68,6 @@ class WithHeldTests : InstrumentedTest { val roomAlicePOV = aliceSession.getRoom(roomId)!! val bobUnverifiedSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) - // ============================= // ACT // ============================= @@ -88,6 +86,7 @@ class WithHeldTests : InstrumentedTest { val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!! + val megolmSessionId = eventBobPOV.root.content.toModel()!!.sessionId!! // ============================= // ASSERT // ============================= @@ -103,9 +102,23 @@ class WithHeldTests : InstrumentedTest { val type = (failure as MXCryptoError.Base).errorType val technicalMessage = failure.technicalMessage Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) - Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) + Assert.assertEquals("Cause should be unverified", WithHeldCode.UNAUTHORISED.value, technicalMessage) } + // Let's see if the reply we got from bob first session is unverified + testHelper.waitWithLatch { latch -> + testHelper.retryPeriodicallyWithLatch(latch) { + bobUnverifiedSession.cryptoService().getOutgoingRoomKeyRequests() + .firstOrNull { it.sessionId == megolmSessionId } + ?.results + ?.firstOrNull { it.fromDevice == bobSession.sessionParams.deviceId } + ?.result + ?.let { + it as? RequestResult.Failure + } + ?.code == WithHeldCode.UNVERIFIED + } + } // enable back sending to unverified aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false) @@ -130,7 +143,7 @@ class WithHeldTests : InstrumentedTest { val type = (failure as MXCryptoError.Base).errorType val technicalMessage = failure.technicalMessage Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) - Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage) + Assert.assertEquals("Cause should be unverified", WithHeldCode.UNAUTHORISED.value, technicalMessage) } testHelper.signOutAndClose(aliceSession) @@ -139,8 +152,10 @@ class WithHeldTests : InstrumentedTest { } @Test - @Ignore("This test will be ignored until it is fixed") fun test_WithHeldNoOlm() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = testData.firstSession val bobSession = testData.secondSession!! @@ -220,8 +235,10 @@ class WithHeldTests : InstrumentedTest { } @Test - @Ignore("This test will be ignored until it is fixed") fun test_WithHeldKeyRequest() { + val testHelper = CommonTestHelper(context()) + val cryptoTestHelper = CryptoTestHelper(testHelper) + val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val aliceSession = testData.firstSession val bobSession = testData.secondSession!! @@ -267,5 +284,8 @@ class WithHeldTests : InstrumentedTest { wc?.code == WithHeldCode.UNAUTHORISED } } + + testHelper.signOutAndClose(aliceSession) + testHelper.signOutAndClose(bobSecondSession) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt index 6097bf8c93..df3b2ffe27 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.crypto.verification.qrcode import androidx.test.ext.junit.runners.AndroidJUnit4 import org.amshove.kluent.shouldBe import org.junit.FixMethodOrder -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -40,7 +39,6 @@ import kotlin.coroutines.resume @RunWith(AndroidJUnit4::class) @FixMethodOrder(MethodSorters.JVM) -@Ignore("This test is flaky ; see issue #5449") class VerificationTest : InstrumentedTest { data class ExpectedResult( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt index 9a686de2e1..9507ddda65 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt @@ -31,5 +31,11 @@ data class MXCryptoConfig constructor( * If set to false, the request will be forwarded to the application layer; in this * case the application can decide to prompt the user. */ - val discardRoomKeyRequestsFromUntrustedDevices: Boolean = true + val discardRoomKeyRequestsFromUntrustedDevices: Boolean = true, + + /** + * Currently megolm keys are requested to the sender device and to all of our devices. + * You can limit request only to your sessions by turning this setting to `true` + */ + val limitRoomKeyRequestsToMyDevices: Boolean = false, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index d6d1248de7..b8c08d23dc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningServic import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse @@ -35,8 +36,6 @@ import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event @@ -76,6 +75,15 @@ interface CryptoService { fun setGlobalBlacklistUnverifiedDevices(block: Boolean) + /** + * Enable or disable key gossiping. + * Default is true. + * If set to false this device won't send key_request nor will accept key forwarded + */ + fun enableKeyGossiping(enable: Boolean) + + fun isKeyGossipingEnabled(): Boolean + fun setRoomUnBlacklistUnverifiedDevices(roomId: String) fun getDeviceTrackingStatus(userId: String): Int @@ -94,8 +102,6 @@ interface CryptoService { fun reRequestRoomKeyForEvent(event: Event) - fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) - fun addRoomKeysRequestListener(listener: GossipingRequestListener) fun removeRoomKeysRequestListener(listener: GossipingRequestListener) @@ -142,14 +148,20 @@ interface CryptoService { fun addNewSessionListener(newSessionListener: NewSessionListener) fun removeSessionListener(listener: NewSessionListener) - fun getOutgoingRoomKeyRequests(): List - fun getOutgoingRoomKeyRequestsPaged(): LiveData> + fun getOutgoingRoomKeyRequests(): List + fun getOutgoingRoomKeyRequestsPaged(): LiveData> fun getIncomingRoomKeyRequests(): List fun getIncomingRoomKeyRequestsPaged(): LiveData> - fun getGossipingEventsTrail(): LiveData> - fun getGossipingEvents(): List + /** + * Can be called by the app layer to accept a request manually + * Use carefully as it is prone to social attacks + */ + suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest) + + fun getGossipingEventsTrail(): LiveData> + fun getGossipingEvents(): List // For testing shared session fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/OutgoingKeyRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/OutgoingKeyRequest.kt new file mode 100644 index 0000000000..855f17a34f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/OutgoingKeyRequest.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.api.session.crypto + +import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody +import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode + +data class RequestReply( + val userId: String, + val fromDevice: String?, + val result: RequestResult +) + +sealed class RequestResult { + data class Success(val chainIndex: Int) : RequestResult() + data class Failure(val code: WithHeldCode) : RequestResult() +} + +data class OutgoingKeyRequest( + var requestBody: RoomKeyRequestBody?, + // recipients for the request map of users to list of deviceId + val recipients: Map>, + val fromIndex: Int, + // 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 + val requestId: String, // current state of this request + val state: OutgoingRoomKeyRequestState, + val results: List +) { + /** + * Used only for log. + * + * @return the room id. + */ + val roomId = requestBody?.roomId + + /** + * Used only for log. + * + * @return the session id + */ + val sessionId = requestBody?.sessionId +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingGossipingRequestState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/OutgoingRoomKeyRequestState.kt similarity index 57% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingGossipingRequestState.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/OutgoingRoomKeyRequestState.kt index 8c1bdf6768..6e80bdc133 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingGossipingRequestState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/OutgoingRoomKeyRequestState.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +14,20 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.session.crypto.model +package org.matrix.android.sdk.api.session.crypto -enum class OutgoingGossipingRequestState { +enum class OutgoingRoomKeyRequestState { UNSENT, - SENDING, SENT, - CANCELLING, - CANCELLED, - FAILED_TO_SEND, - FAILED_TO_CANCEL + SENT_THEN_CANCELED, + CANCELLATION_PENDING, + CANCELLATION_PENDING_AND_WILL_RESEND; + + companion object { + fun pendingStates() = setOf( + UNSENT, + CANCELLATION_PENDING_AND_WILL_RESEND, + CANCELLATION_PENDING + ) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt index 3cd36c2ce8..24d3cf4004 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt @@ -16,9 +16,8 @@ package org.matrix.android.sdk.api.session.crypto.keyshare -import org.matrix.android.sdk.api.session.crypto.model.IncomingRequestCancellation import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest +import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest /** * Room keys events listener @@ -35,12 +34,12 @@ interface GossipingRequestListener { * Returns the secret value to be shared * @return true if is handled */ - fun onSecretShareRequest(request: IncomingSecretShareRequest): Boolean + fun onSecretShareRequest(request: SecretShareRequest): Boolean /** * A room key request cancellation has been received. * * @param request the cancellation request */ - fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) + fun onRequestCancelled(request: IncomingRoomKeyRequest) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/AuditTrail.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/AuditTrail.kt new file mode 100644 index 0000000000..861f3bd30b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/AuditTrail.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.api.session.crypto.model + +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode + +enum class TrailType { + OutgoingKeyForward, + IncomingKeyForward, + OutgoingKeyWithheld, + IncomingKeyRequest, + Unknown +} + +interface AuditInfo { + val roomId: String + val sessionId: String + val senderKey: String + val alg: String + val userId: String + val deviceId: String +} + +@JsonClass(generateAdapter = true) +data class ForwardInfo( + override val roomId: String, + override val sessionId: String, + override val senderKey: String, + override val alg: String, + override val userId: String, + override val deviceId: String, + val chainIndex: Long? +) : AuditInfo + +object UnknownInfo : AuditInfo { + override val roomId: String = "" + override val sessionId: String = "" + override val senderKey: String = "" + override val alg: String = "" + override val userId: String = "" + override val deviceId: String = "" +} + +@JsonClass(generateAdapter = true) +data class WithheldInfo( + override val roomId: String, + override val sessionId: String, + override val senderKey: String, + override val alg: String, + val code: WithHeldCode, + override val userId: String, + override val deviceId: String +) : AuditInfo + +@JsonClass(generateAdapter = true) +data class IncomingKeyRequestInfo( + override val roomId: String, + override val sessionId: String, + override val senderKey: String, + override val alg: String, + override val userId: String, + override val deviceId: String, + val requestId: String +) : AuditInfo + +data class AuditTrail( + val ageLocalTs: Long, + val type: TrailType, + val info: AuditInfo +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRequestCancellation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRequestCancellation.kt deleted file mode 100755 index ad11ef9a5e..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRequestCancellation.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.api.session.crypto.model - -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon -import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation - -/** - * IncomingRequestCancellation describes the incoming room key cancellation. - */ -data class IncomingRequestCancellation( - /** - * The user id - */ - override val userId: String? = null, - - /** - * The device id - */ - override val deviceId: String? = null, - - /** - * The request id - */ - override val requestId: String? = null, - override val localCreationTimestamp: Long? -) : IncomingShareRequestCommon { - companion object { - /** - * Factory - * - * @param event the event - * @param currentTimeMillis the current time in milliseconds - */ - fun fromEvent(event: Event, currentTimeMillis: Long): IncomingRequestCancellation? { - return event.getClearContent() - .toModel() - ?.let { - IncomingRequestCancellation( - userId = event.senderId, - deviceId = it.requestingDeviceId, - requestId = it.requestId, - localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis - ) - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt index 0b2c32284b..0a28478a10 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt @@ -16,9 +16,7 @@ package org.matrix.android.sdk.api.session.crypto.model -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon +import org.matrix.android.sdk.internal.util.time.Clock /** * IncomingRoomKeyRequest class defines the incoming room keys request. @@ -27,38 +25,25 @@ data class IncomingRoomKeyRequest( /** * The user id */ - override val userId: String? = null, + val userId: String? = null, /** * The device id */ - override val deviceId: String? = null, + val deviceId: String? = null, /** * The request id */ - override val requestId: String? = null, + val requestId: String? = null, /** * The request body */ val requestBody: RoomKeyRequestBody? = null, - val state: GossipingRequestState = GossipingRequestState.NONE, - - /** - * The runnable to call to accept to share the keys - */ - @Transient - var share: Runnable? = null, - - /** - * The runnable to call to ignore the key share request. - */ - @Transient - var ignore: Runnable? = null, - override val localCreationTimestamp: Long? -) : IncomingShareRequestCommon { + val localCreationTimestamp: Long? +) { companion object { /** * Factory @@ -66,18 +51,36 @@ data class IncomingRoomKeyRequest( * @param event the event * @param currentTimeMillis the current time in milliseconds */ - fun fromEvent(event: Event, currentTimeMillis: Long): IncomingRoomKeyRequest? { - return event.getClearContent() - .toModel() + fun fromEvent(trail: AuditTrail): IncomingRoomKeyRequest? { + return trail + .takeIf { it.type == TrailType.IncomingKeyRequest } + ?.let { + it.info as? IncomingKeyRequestInfo + } ?.let { IncomingRoomKeyRequest( - userId = event.senderId, - deviceId = it.requestingDeviceId, + userId = it.userId, + deviceId = it.deviceId, requestId = it.requestId, - requestBody = it.body ?: RoomKeyRequestBody(), - localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis + requestBody = RoomKeyRequestBody( + algorithm = it.alg, + roomId = it.roomId, + senderKey = it.senderKey, + sessionId = it.sessionId + ), + localCreationTimestamp = trail.ageLocalTs ) } } + + internal fun fromRestRequest(senderId: String, request: RoomKeyShareRequest, clock: Clock): IncomingRoomKeyRequest? { + return IncomingRoomKeyRequest( + userId = senderId, + deviceId = request.requestingDeviceId, + requestId = request.requestId, + requestBody = request.body, + localCreationTimestamp = clock.epochMillis() + ) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingSecretShareRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingSecretShareRequest.kt deleted file mode 100755 index 80f70c83f3..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingSecretShareRequest.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.api.session.crypto.model - -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon - -/** - * IncomingSecretShareRequest class defines the incoming secret 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 - * @param currentTimeMillis the current time in milliseconds - */ - fun fromEvent(event: Event, currentTimeMillis: Long): IncomingSecretShareRequest? { - return event.getClearContent() - .toModel() - ?.let { - IncomingSecretShareRequest( - userId = event.senderId, - deviceId = it.requestingDeviceId, - requestId = it.requestId, - secretName = it.secretName, - localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis - ) - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingRoomKeyRequest.kt deleted file mode 100755 index 5f35cc908f..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingRoomKeyRequest.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.api.session.crypto.model - -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest - -/** - * Represents an outgoing room key request - */ -@JsonClass(generateAdapter = true) -data class OutgoingRoomKeyRequest( - // RequestBody - val requestBody: RoomKeyRequestBody?, - // list of recipients for the request - override val 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 val requestId: String, // current state of this request - override val state: OutgoingGossipingRequestState - // transaction id for the cancellation, if any - // override var cancellationTxnId: String? = null -) : OutgoingGossipingRequest { - - /** - * Used only for log. - * - * @return the room id. - */ - val roomId: String? - get() = requestBody?.roomId - - /** - * Used only for log. - * - * @return the session id - */ - val sessionId: String? - get() = requestBody?.sessionId -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyWithHeldContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyWithHeldContent.kt index a577daf9e4..1eac1d6b2d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyWithHeldContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyWithHeldContent.kt @@ -52,7 +52,13 @@ data class RoomKeyWithHeldContent( /** * A human-readable reason for why the key was not sent. The receiving client should only use this string if it does not understand the code. */ - @Json(name = "reason") val reason: String? = null + @Json(name = "reason") val reason: String? = null, + + /** + * the device ID of the device sending the m.room_key.withheld message + * MSC3735 + */ + @Json(name = "from_device") val fromDevice: String? = null ) { val code: WithHeldCode? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt index 721a2bc8af..3bb8fad810 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SharedSecretStorageService.kt @@ -131,7 +131,7 @@ interface SharedSecretStorageService { fun checkShouldBeAbleToAccessSecrets(secretNames: List, keyId: String?): IntegrityResult - fun requestSecret(name: String, myOtherDeviceId: String) + suspend fun requestSecret(name: String, myOtherDeviceId: String) data class KeyRef( val keyId: String?, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt deleted file mode 100644 index aaf23d17b3..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.crypto - -import android.content.Context -import androidx.work.WorkerParameters -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.failure.shouldBeRetried -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId -import org.matrix.android.sdk.internal.session.SessionComponent -import org.matrix.android.sdk.internal.util.time.Clock -import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker -import org.matrix.android.sdk.internal.worker.SessionWorkerParams -import javax.inject.Inject - -internal class CancelGossipRequestWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) : - SessionSafeCoroutineWorker(context, params, sessionManager, Params::class.java) { - - @JsonClass(generateAdapter = true) - internal data class Params( - override val sessionId: String, - val requestId: String, - val recipients: Map>, - // The txnId for the sendToDevice request. Nullable for compatibility reasons, but MUST always be provided - // to use the same value if this worker is retried. - val txnId: String? = null, - override val lastFailureMessage: String? = null - ) : SessionWorkerParams { - companion object { - fun fromRequest(sessionId: String, request: OutgoingGossipingRequest): Params { - return Params( - sessionId = sessionId, - requestId = request.requestId, - recipients = request.recipients, - txnId = createUniqueTxnId(), - lastFailureMessage = null - ) - } - } - } - - @Inject lateinit var sendToDeviceTask: SendToDeviceTask - @Inject lateinit var cryptoStore: IMXCryptoStore - @Inject lateinit var credentials: Credentials - @Inject lateinit var clock: Clock - - override fun injectWith(injector: SessionComponent) { - injector.inject(this) - } - - override suspend fun doSafeWork(params: Params): Result { - // params.txnId should be provided in all cases now. But Params can be deserialized by - // the WorkManager from data serialized in a previous version of the application, so without the txnId field. - // So if not present, we create a txnId - val txnId = params.txnId ?: createUniqueTxnId() - 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 = clock.epochMillis() - }) - - 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 = txnId - ) - ) - cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLED) - return Result.success() - } catch (throwable: Throwable) { - return if (throwable.shouldBeRetried()) { - Result.retry() - } else { - cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.FAILED_TO_CANCEL) - buildErrorResult(params, throwable.localizedMessage ?: "error") - } - } - } - - override fun buildErrorParams(params: Params, message: String): Params { - return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 8c09da72de..fd4bf6adfd 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -42,12 +42,14 @@ import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.NewSessionListener +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse @@ -57,15 +59,13 @@ import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody +import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest +import org.matrix.android.sdk.api.session.crypto.model.TrailType import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent -import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility @@ -76,7 +76,6 @@ import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption -import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService @@ -91,6 +90,7 @@ import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService +import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.UserId @@ -156,9 +156,10 @@ internal class DefaultCryptoService @Inject constructor( private val crossSigningService: DefaultCrossSigningService, // - private val incomingGossipingRequestManager: IncomingGossipingRequestManager, + private val incomingKeyRequestManager: IncomingKeyRequestManager, + private val secretShareManager: SecretShareManager, // - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, // Actions private val setDeviceVerificationAction: SetDeviceVerificationAction, private val megolmSessionDataImporter: MegolmSessionDataImporter, @@ -178,6 +179,7 @@ internal class DefaultCryptoService @Inject constructor( private val taskExecutor: TaskExecutor, private val cryptoCoroutineScope: CoroutineScope, private val eventDecryptor: EventDecryptor, + private val verificationMessageProcessor: VerificationMessageProcessor, private val liveEventManager: Lazy ) : CryptoService { @@ -192,7 +194,7 @@ internal class DefaultCryptoService @Inject constructor( } } - fun onLiveEvent(roomId: String, event: Event) { + fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean) { // handle state events if (event.isStateEvent()) { when (event.type) { @@ -201,9 +203,18 @@ internal class DefaultCryptoService @Inject constructor( EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) } } + + // handle verification + if (!isInitialSync) { + if (event.type != null && verificationMessageProcessor.shouldProcess(event.type)) { + cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) { + verificationMessageProcessor.process(event) + } + } + } } - val gossipingBuffer = mutableListOf() +// val gossipingBuffer = mutableListOf() override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) { setDeviceNameTask @@ -379,27 +390,8 @@ internal class DefaultCryptoService @Inject constructor( // Open the store cryptoStore.open() - runCatching { -// if (isInitialSync) { -// // refresh the devices list for each known room members -// deviceListManager.invalidateAllDeviceLists() -// deviceListManager.refreshOutdatedDeviceLists() -// } else { - - // Why would we do that? it will be called at end of syn - incomingGossipingRequestManager.processReceivedGossipingRequests() -// } - }.fold( - { - isStarting.set(false) - isStarted.set(true) - }, - { - isStarting.set(false) - isStarted.set(false) - Timber.tag(loggerTag.value).e(it, "Start failed") - } - ) + isStarting.set(false) + isStarted.set(true) } /** @@ -407,7 +399,8 @@ internal class DefaultCryptoService @Inject constructor( */ fun close() = runBlocking(coroutineDispatchers.crypto) { cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) - incomingGossipingRequestManager.close() + incomingKeyRequestManager.close() + outgoingKeyRequestManager.close() olmDevice.release() cryptoStore.close() } @@ -472,15 +465,28 @@ internal class DefaultCryptoService @Inject constructor( } oneTimeKeysUploader.maybeUploadOneTimeKeys() - incomingGossipingRequestManager.processReceivedGossipingRequests() } - } - tryOrNull { - gossipingBuffer.toList().let { - cryptoStore.saveGossipingEvents(it) + // Process pending key requests + try { + if (toDevices.isEmpty()) { + // this is not blocking + outgoingKeyRequestManager.requireProcessAllPendingKeyRequests() + } else { + Timber.tag(loggerTag.value) + .w("Don't process key requests yet as there might be more to_device to catchup") + } + } catch (failure: Throwable) { + // just for safety but should not throw + Timber.tag(loggerTag.value).w("failed to process pending request") + } + + try { + incomingKeyRequestManager.processIncomingRequests() + } catch (failure: Throwable) { + // just for safety but should not throw + Timber.tag(loggerTag.value).w("failed to process incoming room key requests") } - gossipingBuffer.clear() } } } @@ -594,7 +600,7 @@ internal class DefaultCryptoService @Inject constructor( // (for now at least. Maybe we should alert the user somehow?) val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId) - if (existingAlgorithm == algorithm && roomEncryptorsStore.get(roomId) != null) { + if (existingAlgorithm == algorithm) { // ignore Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in $roomId") return false @@ -787,19 +793,25 @@ internal class DefaultCryptoService @Inject constructor( cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { when (event.getClearType()) { EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> { - gossipingBuffer.add(event) // Keys are imported directly, not waiting for end of sync onRoomKeyEvent(event) } - EventType.REQUEST_SECRET, + EventType.REQUEST_SECRET -> { + secretShareManager.handleSecretRequest(event) + } EventType.ROOM_KEY_REQUEST -> { - // save audit trail - gossipingBuffer.add(event) - // Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete) - incomingGossipingRequestManager.onGossipingRequestEvent(event) + event.getClearContent().toModel()?.let { req -> + // We'll always get these because we send room key requests to + // '*' (ie. 'all devices') which includes the sending device, + // so ignore requests from ourself because apart from it being + // very silly, it won't work because an Olm session cannot send + // messages to itself. + if (req.requestingDeviceId != deviceId) { // ignore self requests + event.senderId?.let { incomingKeyRequestManager.addNewIncomingRequest(it, req) } + } + } } EventType.SEND_SECRET -> { - gossipingBuffer.add(event) onSecretSendReceived(event) } EventType.ROOM_KEY_WITHHELD -> { @@ -837,50 +849,38 @@ internal class DefaultCryptoService @Inject constructor( val withHeldContent = event.getClearContent().toModel() ?: return Unit.also { Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields") } - Timber.tag(loggerTag.value).i("onKeyWithHeldReceived() received from:${event.senderId}, content <$withHeldContent>") - val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm) - if (alg is IMXWithHeldExtension) { - alg.onRoomKeyWithHeldEvent(withHeldContent) - } else { - Timber.tag(loggerTag.value).e("onKeyWithHeldReceived() from:${event.senderId}: Unable to handle WithHeldContent for ${withHeldContent.algorithm}") - return + val senderId = event.senderId ?: return Unit.also { + Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields") } + withHeldContent.sessionId ?: return + withHeldContent.algorithm ?: return + withHeldContent.roomId ?: return + withHeldContent.senderKey ?: return + outgoingKeyRequestManager.onRoomKeyWithHeld( + sessionId = withHeldContent.sessionId, + algorithm = withHeldContent.algorithm, + roomId = withHeldContent.roomId, + senderKey = withHeldContent.senderKey, + fromDevice = withHeldContent.fromDevice, + event = Event( + type = EventType.ROOM_KEY_WITHHELD, + senderId = senderId, + content = event.getClearContent() + ) + ) } - private fun onSecretSendReceived(event: Event) { - Timber.tag(loggerTag.value).i("GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}") - if (!event.isEncrypted()) { - // secret send messages must be encrypted - Timber.tag(loggerTag.value).e("GOSSIP onSecretSend() :Received unencrypted secret send event") - return - } - - // Was that sent by us? - if (event.senderId != userId) { - Timber.tag(loggerTag.value).e("GOSSIP 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.tag(loggerTag.value).i("GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") - return - } - - if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) { - // TODO Ask to application layer? - Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK") + private suspend fun onSecretSendReceived(event: Event) { + secretShareManager.onSecretSendReceived(event) { secretName, secretValue -> + handleSDKLevelGossip(secretName, secretValue) } } /** * Returns true if handled by SDK, otherwise should be sent to application layer */ - private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean { + private fun handleSDKLevelGossip(secretName: String?, + secretValue: String): Boolean { return when (secretName) { MASTER_KEY_SSSS_NAME -> { crossSigningService.onSecretMSKGossip(secretValue) @@ -1095,6 +1095,12 @@ internal class DefaultCryptoService @Inject constructor( cryptoStore.setGlobalBlacklistUnverifiedDevices(block) } + override fun enableKeyGossiping(enable: Boolean) { + cryptoStore.enableKeyGossiping(enable) + } + + override fun isKeyGossipingEnabled() = cryptoStore.isKeyGossipingEnabled() + /** * Tells whether the client should ever send encrypted messages to unverified devices. * The default value is false. @@ -1158,52 +1164,17 @@ internal class DefaultCryptoService @Inject constructor( setRoomBlacklistUnverifiedDevices(roomId, false) } -// TODO Check if this method is still necessary - /** - * Cancel any earlier room key request - * - * @param requestBody requestBody - */ - override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) { - outgoingGossipingRequestManager.cancelRoomKeyRequest(requestBody) - } - /** * Re request the encryption keys required to decrypt an event. * * @param event the event to decrypt again. */ override fun reRequestRoomKeyForEvent(event: Event) { - val wireContent = event.content.toModel() ?: return Unit.also { - Timber.tag(loggerTag.value).e("reRequestRoomKeyForEvent Failed to re-request key, null content") - } - - val requestBody = RoomKeyRequestBody( - algorithm = wireContent.algorithm, - roomId = event.roomId, - senderKey = wireContent.senderKey, - sessionId = wireContent.sessionId - ) - - outgoingGossipingRequestManager.resendRoomKeyRequest(requestBody) + outgoingKeyRequestManager.requestKeyForEvent(event, true) } override fun requestRoomKeyForEvent(event: Event) { - val wireContent = event.content.toModel() ?: return Unit.also { - Timber.tag(loggerTag.value).e("requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}") - } - - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { -// if (!isStarted()) { -// Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init") -// internalStart(false) -// } - roomDecryptorProvider - .getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm) - ?.requestKeysForEvent(event, false) ?: run { - Timber.tag(loggerTag.value).v("requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}") - } - } + outgoingKeyRequestManager.requestKeyForEvent(event, false) } /** @@ -1212,7 +1183,8 @@ internal class DefaultCryptoService @Inject constructor( * @param listener listener */ override fun addRoomKeysRequestListener(listener: GossipingRequestListener) { - incomingGossipingRequestManager.addRoomKeysRequestListener(listener) + incomingKeyRequestManager.addRoomKeysRequestListener(listener) + secretShareManager.addListener(listener) } /** @@ -1221,42 +1193,10 @@ internal class DefaultCryptoService @Inject constructor( * @param listener listener */ override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { - incomingGossipingRequestManager.removeRoomKeysRequestListener(listener) + incomingKeyRequestManager.removeRoomKeysRequestListener(listener) + secretShareManager.removeListener(listener) } -// private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) { -// val deviceKey = deviceInfo.identityKey() -// -// val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0 -// val now = clock.epochMillis() -// if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { -// Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") -// return -// } -// -// Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") -// lastNewSessionForcedDates.setObject(senderId, deviceKey, now) -// -// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { -// ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) -// -// // Now send a blank message on that session so the other side knows about it. -// // (The keyshare request is sent in the clear so that won't do) -// // We send this first such that, as long as the toDevice messages arrive in the -// // same order we sent them, the other end will get this first, set up the new session, -// // then get the keyshare request and send the key over this new session (because it -// // is the session it has most recently received a message on). -// val payloadJson = mapOf("type" to EventType.DUMMY) -// -// val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) -// val sendToDeviceMap = MXUsersDevicesMap() -// sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) -// Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}") -// val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) -// sendToDeviceTask.execute(sendToDeviceParams) -// } -// } - /** * Provides the list of unknown devices * @@ -1302,27 +1242,41 @@ internal class DefaultCryptoService @Inject constructor( return "DefaultCryptoService of $userId ($deviceId)" } - override fun getOutgoingRoomKeyRequests(): List { + override fun getOutgoingRoomKeyRequests(): List { return cryptoStore.getOutgoingRoomKeyRequests() } - override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { + override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { return cryptoStore.getOutgoingRoomKeyRequestsPaged() } - override fun getIncomingRoomKeyRequestsPaged(): LiveData> { - return cryptoStore.getIncomingRoomKeyRequestsPaged() - } - override fun getIncomingRoomKeyRequests(): List { - return cryptoStore.getIncomingRoomKeyRequests() + return cryptoStore.getGossipingEvents() + .mapNotNull { + IncomingRoomKeyRequest.fromEvent(it) + } } - override fun getGossipingEventsTrail(): LiveData> { + override fun getIncomingRoomKeyRequestsPaged(): LiveData> { + return cryptoStore.getGossipingEventsTrail(TrailType.IncomingKeyRequest) { + IncomingRoomKeyRequest.fromEvent(it) + ?: IncomingRoomKeyRequest(localCreationTimestamp = 0L) + } + } + + /** + * If you registered a `GossipingRequestListener`, you will be notified of key request + * that was not accepted by the SDK. You can call back this manually to accept anyhow. + */ + override suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest) { + incomingKeyRequestManager.manuallyAcceptRoomKeyRequest(request) + } + + override fun getGossipingEventsTrail(): LiveData> { return cryptoStore.getGossipingEventsTrail() } - override fun getGossipingEvents(): List { + override fun getGossipingEvents(): List { return cryptoStore.getGossipingEvents() } @@ -1346,8 +1300,8 @@ internal class DefaultCryptoService @Inject constructor( loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) } catch (failure: Throwable) { Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members") - callback.onFailure(failure) - return@launch + // we probably shouldn't block sending on that (but questionable) + // but some members won't be able to decrypt } val userIds = getRoomUserIds(roomId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt index 535999373b..f546b35fcf 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt @@ -315,10 +315,19 @@ internal class DeviceListManager @Inject constructor( } else { Timber.v("## CRYPTO | downloadKeys() : starts") val t0 = clock.epochMillis() - val result = doKeyDownloadForUsers(downloadUsers) - Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms") - result.also { - it.addEntriesFromMap(stored) + try { + val result = doKeyDownloadForUsers(downloadUsers) + Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms") + result.also { + it.addEntriesFromMap(stored) + } + } catch (failure: Throwable) { + Timber.w(failure, "## CRYPTO | downloadKeys() : doKeyDownloadForUsers failed after ${clock.epochMillis() - t0} ms") + if (forceDownload) { + throw failure + } else { + stored + } } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt deleted file mode 100644 index a2c85e5ceb..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingWorkManager.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.crypto - -import androidx.work.BackoffPolicy -import androidx.work.Data -import androidx.work.ExistingWorkPolicy -import androidx.work.ListenableWorker -import androidx.work.OneTimeWorkRequest -import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.internal.di.WorkManagerProvider -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.CancelableWork -import org.matrix.android.sdk.internal.worker.startChain -import java.util.UUID -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -@SessionScope -internal class GossipingWorkManager @Inject constructor( - private val workManagerProvider: WorkManagerProvider -) { - - inline fun createWork(data: Data, startChain: Boolean): OneTimeWorkRequest { - return workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) - .startChain(startChain) - .setInputData(data) - .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) - .build() - } - - // Prevent sending queue to stay broken after app restart - // The unique queue id will stay the same as long as this object is instantiated - private val queueSuffixApp = UUID.randomUUID() - - fun postWork(workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable { - workManagerProvider.workManager - .beginUniqueWork(this::class.java.name + "_$queueSuffixApp", policy, workRequest) - .enqueue() - - return CancelableWork(workManagerProvider.workManager, workRequest.id) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt deleted file mode 100644 index 1612caba9f..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt +++ /dev/null @@ -1,475 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.crypto - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.api.crypto.MXCryptoConfig -import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME -import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey -import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener -import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject -import org.matrix.android.sdk.api.session.crypto.model.IncomingRequestCancellation -import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.util.toBase64NoPadding -import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption -import org.matrix.android.sdk.internal.crypto.model.rest.GossipingDefaultContent -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId -import org.matrix.android.sdk.internal.di.SessionId -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.util.time.Clock -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import timber.log.Timber -import java.util.concurrent.Executors -import javax.inject.Inject - -@SessionScope -internal class IncomingGossipingRequestManager @Inject constructor( - @SessionId private val sessionId: String, - private val credentials: Credentials, - private val cryptoStore: IMXCryptoStore, - private val cryptoConfig: MXCryptoConfig, - private val gossipingWorkManager: GossipingWorkManager, - private val roomEncryptorsStore: RoomEncryptorsStore, - private val roomDecryptorProvider: RoomDecryptorProvider, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope, - private val clock: Clock, -) { - - private val executor = Executors.newSingleThreadExecutor() - - // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations - // we received in the current sync. - private val receivedGossipingRequests = ArrayList() - private val receivedRequestCancellations = ArrayList() - - // the listeners - private val gossipingRequestListeners: MutableSet = HashSet() - - init { - receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests()) - } - - fun close() { - executor.shutdownNow() - } - - // Recently verified devices (map of deviceId and timestamp) - private val recentlyVerifiedDevices = HashMap() - - /** - * Called when a session has been verified. - * This information can be used by the manager to decide whether or not to fullfil gossiping requests - */ - fun onVerificationCompleteForDevice(deviceId: String) { - // For now we just keep an in memory cache - synchronized(recentlyVerifiedDevices) { - recentlyVerifiedDevices[deviceId] = clock.epochMillis() - } - } - - private fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean { - val verifTimestamp: Long? - synchronized(recentlyVerifiedDevices) { - verifTimestamp = recentlyVerifiedDevices[deviceId] - } - if (verifTimestamp == null) return false - - val age = clock.epochMillis() - verifTimestamp - - return age < FIVE_MINUTES_IN_MILLIS - } - - /** - * Called when we get an m.room_key_request event - * It must be called on CryptoThread - * - * @param event the announcement event. - */ - fun onGossipingRequestEvent(event: Event) { - val roomKeyShare = event.getClearContent().toModel() - Timber.i("## CRYPTO | GOSSIP onGossipingRequestEvent received type ${event.type} from user:${event.senderId}, content:$roomKeyShare") - // val ageLocalTs = event.unsignedData?.age?.let { clock.epochMillis() - it } - when (roomKeyShare?.action) { - GossipingToDeviceObject.ACTION_SHARE_REQUEST -> { - if (event.getClearType() == EventType.REQUEST_SECRET) { - IncomingSecretShareRequest.fromEvent(event, clock.epochMillis())?.let { - if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) { - // ignore, it was sent by me as * - Timber.v("## GOSSIP 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, clock.epochMillis())?.let { - if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) { - // ignore, it was sent by me as * - Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo") - } else { -// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) - receivedGossipingRequests.add(it) - } - } - } - } - GossipingToDeviceObject.ACTION_SHARE_CANCELLATION -> { - IncomingRequestCancellation.fromEvent(event, clock.epochMillis())?.let { - receivedRequestCancellations.add(it) - } - } - else -> { - Timber.e("## GOSSIP onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}") - } - } - } - - /** - * 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 processReceivedGossipingRequests() { - val roomKeyRequestsToProcess = receivedGossipingRequests.toList() - receivedGossipingRequests.clear() - - Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : ${roomKeyRequestsToProcess.size} request to process") - - var receivedRequestCancellations: List? = null - - synchronized(this.receivedRequestCancellations) { - if (this.receivedRequestCancellations.isNotEmpty()) { - receivedRequestCancellations = this.receivedRequestCancellations.toList() - this.receivedRequestCancellations.clear() - } - } - - executor.execute { - cryptoStore.storeIncomingGossipingRequests(roomKeyRequestsToProcess) - for (request in roomKeyRequestsToProcess) { - if (request is IncomingRoomKeyRequest) { - processIncomingRoomKeyRequest(request) - } else if (request is IncomingSecretShareRequest) { - processIncomingSecretShareRequest(request) - } - } - - receivedRequestCancellations?.forEach { request -> - Timber.v("## CRYPTO | GOSSIP 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 ?: return - val deviceId = request.deviceId ?: return - val body = request.requestBody ?: return - val roomId = body.roomId ?: return - val alg = body.algorithm ?: return - - Timber.v("## CRYPTO | GOSSIP processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}") - if (credentials.userId != userId) { - handleKeyRequestFromOtherUser(body, request, alg, roomId, userId, deviceId) - 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("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId") - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - return - } - if (!decryptor.hasKeysForKeyRequest(request)) { - Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}") - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - return - } - - if (credentials.deviceId == deviceId && credentials.userId == userId) { - Timber.v("## CRYPTO | GOSSIP 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("## CRYPTO | GOSSIP processReceivedGossipingRequests() : device is already verified: sharing keys") - request.share?.run() - return - } - - if (device.isBlocked) { - Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : device is blocked -> ignored") - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - return - } - } - - // As per config we automatically discard untrusted devices request - if (cryptoConfig.discardRoomKeyRequestsFromUntrustedDevices) { - Timber.v("## CRYPTO | 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 handleKeyRequestFromOtherUser(body: RoomKeyRequestBody, - request: IncomingRoomKeyRequest, - alg: String, - roomId: String, - userId: String, - deviceId: String) { - Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request from other user") - val senderKey = body.senderKey ?: return Unit - .also { Timber.w("missing senderKey") } - .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } - val sessionId = body.sessionId ?: return Unit - .also { Timber.w("missing sessionId") } - .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } - - if (alg != MXCRYPTO_ALGORITHM_MEGOLM) { - return Unit - .also { Timber.w("Only megolm is accepted here") } - .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } - } - - val roomEncryptor = roomEncryptorsStore.get(roomId) ?: return Unit - .also { Timber.w("no room Encryptor") } - .also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) } - - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - if (roomEncryptor is IMXGroupEncryption) { - val isSuccess = roomEncryptor.reshareKey(sessionId, userId, deviceId, senderKey) - - if (isSuccess) { - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) - } else { - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.UNABLE_TO_PROCESS) - } - } else { - Timber.e("## CRYPTO | handleKeyRequestFromOtherUser() from:$userId: Unable to handle IMXGroupEncryption.reshareKey for $alg") - } - } - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.RE_REQUESTED) - } - - private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) { - val secretName = request.secretName ?: return Unit.also { - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Missing secret name") - } - - val userId = request.userId - if (userId == null || credentials.userId != userId) { - Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from other users") - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - return - } - - val deviceId = request.deviceId - ?: return Unit.also { - Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Malformed request, no ") - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - } - - val device = cryptoStore.getUserDevice(userId, deviceId) - ?: return Unit.also { - Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}") - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - } - - if (!device.isVerified || device.isBlocked) { - Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device") - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - return - } - - val isDeviceLocallyVerified = cryptoStore.getUserDevice(userId, deviceId)?.trustLevel?.isLocallyVerified() - - when (secretName) { - MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master - SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned - USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user - KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey - ?.let { - extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding() - } - else -> null - }?.let { secretValue -> - Timber.i("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted") - if (isDeviceLocallyVerified == true && hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId)) { - val params = SendGossipWorker.Params( - sessionId = sessionId, - secretValue = secretValue, - requestUserId = request.userId, - requestDeviceId = request.deviceId, - requestId = request.requestId, - txnId = createUniqueTxnId() - ) - - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING) - val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true) - gossipingWorkManager.postWork(workRequest) - } else { - Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Can't share secret $secretName with $device, verification too old") - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - } - return - } - - Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : $secretName unknown at SDK level, asking to app layer") - - request.ignore = Runnable { - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) - } - - request.share = { secretValue -> - val params = SendGossipWorker.Params( - sessionId = userId, - secretValue = secretValue, - requestUserId = request.userId, - requestDeviceId = request.deviceId, - requestId = request.requestId, - txnId = createUniqueTxnId() - ) - - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING) - val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true) - gossipingWorkManager.postWork(workRequest) - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED) - } - - onShareRequest(request) - } - - /** - * Dispatch onRoomKeyRequest - * - * @param request the request - */ - private fun onRoomKeyRequest(request: IncomingRoomKeyRequest) { - synchronized(gossipingRequestListeners) { - for (listener in gossipingRequestListeners) { - try { - listener.onRoomKeyRequest(request) - } catch (e: Exception) { - Timber.e(e, "## CRYPTO | onRoomKeyRequest() failed") - } - } - } - } - - /** - * 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, "## CRYPTO | GOSSIP 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: IncomingRequestCancellation) { - synchronized(gossipingRequestListeners) { - for (listener in gossipingRequestListeners) { - try { - listener.onRoomKeyRequestCancellation(request) - } catch (e: Exception) { - Timber.e(e, "## CRYPTO | GOSSIP onRoomKeyRequestCancellation() failed") - } - } - } - } - - fun addRoomKeysRequestListener(listener: GossipingRequestListener) { - synchronized(gossipingRequestListeners) { - gossipingRequestListeners.add(listener) - } - } - - fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { - synchronized(gossipingRequestListeners) { - gossipingRequestListeners.remove(listener) - } - } - - companion object { - private const val FIVE_MINUTES_IN_MILLIS = 5 * 60 * 1000 - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt new file mode 100644 index 0000000000..13f2fb861a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt @@ -0,0 +1,463 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.crypto + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest +import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody +import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent +import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer +import org.matrix.android.sdk.internal.util.time.Clock +import timber.log.Timber +import java.util.concurrent.Executors +import javax.inject.Inject +import kotlin.system.measureTimeMillis + +private val loggerTag = LoggerTag("IncomingKeyRequestManager", LoggerTag.CRYPTO) + +@SessionScope +internal class IncomingKeyRequestManager @Inject constructor( + private val credentials: Credentials, + private val cryptoStore: IMXCryptoStore, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val olmDevice: MXOlmDevice, + private val cryptoConfig: MXCryptoConfig, + private val messageEncrypter: MessageEncrypter, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val sendToDeviceTask: SendToDeviceTask, + private val clock: Clock, +) { + + private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher) + val sequencer = SemaphoreCoroutineSequencer() + + private val incomingRequestBuffer = mutableListOf() + + // the listeners + private val gossipingRequestListeners: MutableSet = HashSet() + + enum class MegolmRequestAction { + Request, Cancel + } + + data class ValidMegolmRequestBody( + val requestId: String, + val requestingUserId: String, + val requestingDeviceId: String, + val roomId: String, + val senderKey: String, + val sessionId: String, + val action: MegolmRequestAction + ) { + fun shortDbgString() = "Request from $requestingUserId|$requestingDeviceId for session $sessionId in room $roomId" + } + + private fun RoomKeyShareRequest.toValidMegolmRequest(senderId: String): ValidMegolmRequestBody? { + val deviceId = requestingDeviceId ?: return null + val body = body ?: return null + val roomId = body.roomId ?: return null + val sessionId = body.sessionId ?: return null + val senderKey = body.senderKey ?: return null + val requestId = this.requestId ?: return null + if (body.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null + val action = when (this.action) { + "request" -> MegolmRequestAction.Request + "request_cancellation" -> MegolmRequestAction.Cancel + else -> null + } ?: return null + return ValidMegolmRequestBody( + requestId = requestId, + requestingUserId = senderId, + requestingDeviceId = deviceId, + roomId = roomId, + senderKey = senderKey, + sessionId = sessionId, + action = action + ) + } + + fun addNewIncomingRequest(senderId: String, request: RoomKeyShareRequest) { + if (!cryptoStore.isKeyGossipingEnabled()) { + Timber.tag(loggerTag.value) + .i("Ignore incoming key request as per crypto config in room ${request.body?.roomId}") + return + } + outgoingRequestScope.launch { + // It is important to handle requests in order + sequencer.post { + val validMegolmRequest = request.toValidMegolmRequest(senderId) ?: return@post Unit.also { + Timber.tag(loggerTag.value).w("Received key request for unknown algorithm ${request.body?.algorithm}") + } + + // is there already one like that? + val existing = incomingRequestBuffer.firstOrNull { it == validMegolmRequest } + if (existing == null) { + when (validMegolmRequest.action) { + MegolmRequestAction.Request -> { + // just add to the buffer + incomingRequestBuffer.add(validMegolmRequest) + } + MegolmRequestAction.Cancel -> { + // ignore, we can't cancel as it's not known (probably already processed) + // still notify app layer if it was passed up previously + IncomingRoomKeyRequest.fromRestRequest(senderId, request, clock)?.let { iReq -> + outgoingRequestScope.launch(coroutineDispatchers.computation) { + val listenersCopy = synchronized(gossipingRequestListeners) { + gossipingRequestListeners.toList() + } + listenersCopy.onEach { + tryOrNull { + withContext(coroutineDispatchers.main) { + it.onRequestCancelled(iReq) + } + } + } + } + } + } + } + } else { + when (validMegolmRequest.action) { + MegolmRequestAction.Request -> { + // it's already in buffer, nop keep existing + } + MegolmRequestAction.Cancel -> { + // discard the request in buffer + incomingRequestBuffer.remove(existing) + outgoingRequestScope.launch(coroutineDispatchers.computation) { + val listenersCopy = synchronized(gossipingRequestListeners) { + gossipingRequestListeners.toList() + } + listenersCopy.onEach { + IncomingRoomKeyRequest.fromRestRequest(senderId, request, clock)?.let { iReq -> + withContext(coroutineDispatchers.main) { + tryOrNull { it.onRequestCancelled(iReq) } + } + } + } + } + } + } + } + } + } + } + + fun processIncomingRequests() { + outgoingRequestScope.launch { + sequencer.post { + measureTimeMillis { + Timber.tag(loggerTag.value).v("processIncomingKeyRequests : ${incomingRequestBuffer.size} request to process") + incomingRequestBuffer.forEach { + // should not happen, we only store requests + if (it.action != MegolmRequestAction.Request) return@forEach + try { + handleIncomingRequest(it) + } catch (failure: Throwable) { + // ignore and continue, should not happen + Timber.tag(loggerTag.value).w(failure, "processIncomingKeyRequests : failed to process request $it") + } + } + incomingRequestBuffer.clear() + }.let { duration -> + Timber.tag(loggerTag.value).v("Finish processing incoming key request in $duration ms") + } + } + } + } + + private suspend fun handleIncomingRequest(request: ValidMegolmRequestBody) { + // We don't want to download keys, if we don't know the device yet we won't share any how? + val requestingDevice = + cryptoStore.getUserDevice(request.requestingUserId, request.requestingDeviceId) + ?: return Unit.also { + Timber.tag(loggerTag.value).d("Ignoring key request: ${request.shortDbgString()}") + } + + cryptoStore.saveIncomingKeyRequestAuditTrail( + request.requestId, + request.roomId, + request.sessionId, + request.senderKey, + MXCRYPTO_ALGORITHM_MEGOLM, + request.requestingUserId, + request.requestingDeviceId + ) + + val roomAlgorithm = // withContext(coroutineDispatchers.crypto) { + cryptoStore.getRoomAlgorithm(request.roomId) +// } + if (roomAlgorithm != MXCRYPTO_ALGORITHM_MEGOLM) { + // strange we received a request for a room that is not encrypted + // maybe a broken state? + Timber.tag(loggerTag.value).w("Received a key request in a room with unsupported alg:$roomAlgorithm , req:${request.shortDbgString()}") + return + } + + // Is it for one of our sessions? + if (request.requestingUserId == credentials.userId) { + Timber.tag(loggerTag.value).v("handling request from own user: megolm session ${request.sessionId}") + + if (request.requestingDeviceId == credentials.deviceId) { + // ignore it's a remote echo + return + } + // If it's verified we share from the early index we know + // if not we check if it was originaly shared or not + if (requestingDevice.isVerified) { + // we share from the earliest known chain index + shareMegolmKey(request, requestingDevice, null) + } else { + shareIfItWasPreviouslyShared(request, requestingDevice) + } + } else { + if (cryptoConfig.limitRoomKeyRequestsToMyDevices) { + Timber.tag(loggerTag.value).v("Ignore request from other user as per crypto config: ${request.shortDbgString()}") + return + } + Timber.tag(loggerTag.value).v("handling request from other user: megolm session ${request.sessionId}") + if (requestingDevice.isBlocked) { + // it's blocked, so send a withheld code + sendWithheldForRequest(request, WithHeldCode.BLACKLISTED) + } else { + shareIfItWasPreviouslyShared(request, requestingDevice) + } + } + } + + private suspend fun shareIfItWasPreviouslyShared(request: ValidMegolmRequestBody, requestingDevice: CryptoDeviceInfo) { + // we don't reshare unless it was previously shared with + val wasSessionSharedWithUser = withContext(coroutineDispatchers.crypto) { + cryptoStore.getSharedSessionInfo(request.roomId, request.sessionId, requestingDevice) + } + if (wasSessionSharedWithUser.found && wasSessionSharedWithUser.chainIndex != null) { + // we share from the index it was previously shared with + shareMegolmKey(request, requestingDevice, wasSessionSharedWithUser.chainIndex.toLong()) + } else { + val isOwnDevice = requestingDevice.userId == credentials.userId + sendWithheldForRequest(request, if (isOwnDevice) WithHeldCode.UNVERIFIED else WithHeldCode.UNAUTHORISED) + // if it's our device we could delegate to the app layer to decide + if (isOwnDevice) { + outgoingRequestScope.launch(coroutineDispatchers.computation) { + val listenersCopy = synchronized(gossipingRequestListeners) { + gossipingRequestListeners.toList() + } + val iReq = IncomingRoomKeyRequest( + userId = requestingDevice.userId, + deviceId = requestingDevice.deviceId, + requestId = request.requestId, + requestBody = RoomKeyRequestBody( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + senderKey = request.senderKey, + sessionId = request.sessionId, + roomId = request.roomId + ), + localCreationTimestamp = clock.epochMillis() + ) + listenersCopy.onEach { + withContext(coroutineDispatchers.main) { + tryOrNull { it.onRoomKeyRequest(iReq) } + } + } + } + } + } + } + + private suspend fun sendWithheldForRequest(request: ValidMegolmRequestBody, code: WithHeldCode) { + Timber.tag(loggerTag.value) + .w("Send withheld $code for req: ${request.shortDbgString()}") + val withHeldContent = RoomKeyWithHeldContent( + roomId = request.roomId, + senderKey = request.senderKey, + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + sessionId = request.sessionId, + codeString = code.value, + fromDevice = credentials.deviceId + ) + + val params = SendToDeviceTask.Params( + EventType.ROOM_KEY_WITHHELD, + MXUsersDevicesMap().apply { + setObject(request.requestingUserId, request.requestingDeviceId, withHeldContent) + } + ) + try { + withContext(coroutineDispatchers.io) { + sendToDeviceTask.execute(params) + Timber.tag(loggerTag.value) + .d("Send withheld $code req: ${request.shortDbgString()}") + } + + cryptoStore.saveWithheldAuditTrail( + roomId = request.roomId, + sessionId = request.sessionId, + senderKey = request.senderKey, + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + code = code, + userId = request.requestingUserId, + deviceId = request.requestingDeviceId + ) + } catch (failure: Throwable) { + // Ignore it's not that important? + // do we want to fallback to a worker? + Timber.tag(loggerTag.value) + .w("Failed to send withheld $code req: ${request.shortDbgString()} reason:${failure.localizedMessage}") + } + } + + suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest) { + request.requestId ?: return + request.deviceId ?: return + request.userId ?: return + request.requestBody?.roomId ?: return + request.requestBody.senderKey ?: return + request.requestBody.sessionId ?: return + val validReq = ValidMegolmRequestBody( + requestId = request.requestId, + requestingDeviceId = request.deviceId, + requestingUserId = request.userId, + roomId = request.requestBody.roomId, + senderKey = request.requestBody.senderKey, + sessionId = request.requestBody.sessionId, + action = MegolmRequestAction.Request + ) + val requestingDevice = + cryptoStore.getUserDevice(request.userId, request.deviceId) + ?: return Unit.also { + Timber.tag(loggerTag.value).d("Ignoring key request: ${validReq.shortDbgString()}") + } + + shareMegolmKey(validReq, requestingDevice, null) + } + + private suspend fun shareMegolmKey(validRequest: ValidMegolmRequestBody, + requestingDevice: CryptoDeviceInfo, + chainIndex: Long?): Boolean { + Timber.tag(loggerTag.value) + .d("try to re-share Megolm Key at index $chainIndex for ${validRequest.shortDbgString()}") + + val devicesByUser = mapOf(validRequest.requestingUserId to listOf(requestingDevice)) + val usersDeviceMap = try { + ensureOlmSessionsForDevicesAction.handle(devicesByUser) + } catch (failure: Throwable) { + Timber.tag(loggerTag.value) + .w("Failed to establish olm session") + sendWithheldForRequest(validRequest, WithHeldCode.NO_OLM) + return false + } + + val olmSessionResult = usersDeviceMap.getObject(requestingDevice.userId, requestingDevice.deviceId) + if (olmSessionResult?.sessionId == null) { + Timber.tag(loggerTag.value) + .w("reshareKey: no session with this device, probably because there were no one-time keys") + sendWithheldForRequest(validRequest, WithHeldCode.NO_OLM) + return false + } + val sessionHolder = try { + olmDevice.getInboundGroupSession(validRequest.sessionId, validRequest.senderKey, validRequest.roomId) + } catch (failure: Throwable) { + Timber.tag(loggerTag.value) + .e(failure, "shareKeysWithDevice: failed to get session ${validRequest.requestingUserId}") + // It's unavailable + sendWithheldForRequest(validRequest, WithHeldCode.UNAVAILABLE) + return false + } + + val export = sessionHolder.mutex.withLock { + sessionHolder.wrapper.exportKeys(chainIndex) + } ?: return false.also { + Timber.tag(loggerTag.value) + .e("shareKeysWithDevice: failed to export group session ${validRequest.sessionId}") + } + + val payloadJson = mapOf( + "type" to EventType.FORWARDED_ROOM_KEY, + "content" to export + ) + + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(requestingDevice)) + val sendToDeviceMap = MXUsersDevicesMap() + sendToDeviceMap.setObject(requestingDevice.userId, requestingDevice.deviceId, encodedPayload) + Timber.tag(loggerTag.value).d("reshareKey() : try sending session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}") + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + return try { + sendToDeviceTask.execute(sendToDeviceParams) + Timber.tag(loggerTag.value) + .i("successfully re-shared session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}") + cryptoStore.saveForwardKeyAuditTrail( + validRequest.roomId, + validRequest.sessionId, + validRequest.senderKey, + MXCRYPTO_ALGORITHM_MEGOLM, + requestingDevice.userId, + requestingDevice.deviceId, + chainIndex + ) + true + } catch (failure: Throwable) { + Timber.tag(loggerTag.value) + .e(failure, "fail to re-share session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}") + false + } + } + + fun addRoomKeysRequestListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.add(listener) + } + } + + fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.remove(listener) + } + } + + fun close() { + try { + outgoingRequestScope.cancel("User Terminate") + incomingRequestBuffer.clear() + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).w("Failed to shutDown request manager") + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt deleted file mode 100644 index 97c369db3e..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.crypto - -internal interface IncomingShareRequestCommon { - /** - * The user id - */ - val userId: String? - - /** - * The device id - */ - val deviceId: String? - - /** - * The request id - */ - val requestId: String? - - val localCreationTimestamp: Long? -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index 7eec83abdd..68a1519670 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -43,7 +43,6 @@ import org.matrix.olm.OlmOutboundGroupSession import org.matrix.olm.OlmSession import org.matrix.olm.OlmUtility import timber.log.Timber -import java.net.URLEncoder import javax.inject.Inject private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO) @@ -331,14 +330,6 @@ internal class MXOlmDevice @Inject constructor( Timber.tag(loggerTag.value).e(e, "## createInboundSession() : removeOneTimeKeys failed") } - Timber.tag(loggerTag.value).v("## createInboundSession() : ciphertext: $ciphertext") - try { - val sha256 = olmUtility!!.sha256(URLEncoder.encode(ciphertext, "utf-8")) - Timber.tag(loggerTag.value).v("## createInboundSession() :ciphertext: SHA256: $sha256") - } catch (e: Exception) { - Timber.tag(loggerTag.value).e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext") - } - val olmMessage = OlmMessage() olmMessage.mCipherText = ciphertext olmMessage.mType = messageType.toLong() @@ -589,6 +580,13 @@ internal class MXOlmDevice @Inject constructor( // Inbound group session + sealed interface AddSessionResult { + data class Imported(val ratchetIndex: Int) : AddSessionResult + abstract class Failure : AddSessionResult + object NotImported : Failure() + data class NotImportedHigherIndex(val newIndex: Int) : Failure() + } + /** * Add an inbound group session to the session store. * @@ -607,7 +605,7 @@ internal class MXOlmDevice @Inject constructor( senderKey: String, forwardingCurve25519KeyChain: List, keysClaimed: Map, - exportFormat: Boolean): Boolean { + exportFormat: Boolean): AddSessionResult { val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat) val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } val existingSession = existingSessionHolder?.wrapper @@ -615,7 +613,7 @@ internal class MXOlmDevice @Inject constructor( if (existingSession != null) { Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session") try { - val existingFirstKnown = existingSession.firstKnownIndex ?: return false.also { + val existingFirstKnown = existingSession.firstKnownIndex ?: return AddSessionResult.NotImported.also { // This is quite unexpected, could throw if native was released? Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session") candidateSession.olmInboundGroupSession?.releaseSession() @@ -626,12 +624,12 @@ internal class MXOlmDevice @Inject constructor( if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId") candidateSession.olmInboundGroupSession?.releaseSession() - return false + return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt()) } } catch (failure: Throwable) { Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}") candidateSession.olmInboundGroupSession?.releaseSession() - return false + return AddSessionResult.NotImported } } @@ -641,19 +639,19 @@ internal class MXOlmDevice @Inject constructor( val candidateOlmInboundSession = candidateSession.olmInboundGroupSession if (null == candidateOlmInboundSession) { Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session ") - return false + return AddSessionResult.NotImported } try { if (candidateOlmInboundSession.sessionIdentifier() != sessionId) { Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") candidateOlmInboundSession.releaseSession() - return false + return AddSessionResult.NotImported } } catch (e: Throwable) { candidateOlmInboundSession.releaseSession() Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed") - return false + return AddSessionResult.NotImported } candidateSession.senderKey = senderKey @@ -667,7 +665,7 @@ internal class MXOlmDevice @Inject constructor( inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey) } - return true + return AddSessionResult.Imported(candidateSession.firstKnownIndex?.toInt() ?: 0) } /** @@ -790,7 +788,7 @@ internal class MXOlmDevice @Inject constructor( if (timelineSet.contains(messageIndexKey)) { val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex) - Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason") + Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason") throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt deleted file mode 100644 index 2438e01102..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.crypto - -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState - -internal interface OutgoingGossipingRequest { - val recipients: Map> - val requestId: String - val state: OutgoingGossipingRequestState - // transaction id for the cancellation, if any - // var cancellationTxnId: String? -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt deleted file mode 100755 index e6f6ac5053..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.crypto - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId -import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper -import org.matrix.android.sdk.internal.di.SessionId -import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import timber.log.Timber -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 gossipingWorkManager: GossipingWorkManager) { - - /** - * 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("## CRYPTO - GOSSIP 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("## CRYPTO - GOSSIP sendSecretShareRequest() : we are already sending 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.computation) { - 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.computation) { - 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("## CRYPTO - GOSSIP cancelRoomKeyRequest() Unknown request $requestBody") - } - - sendOutgoingRoomKeyRequestCancellation(req, andResend) - } - - /** - * Send the outgoing key request. - * - * @param request the request - */ - private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) { - Timber.v("## CRYPTO - GOSSIP sendOutgoingGossipingRequest() : Requesting keys $request") - - val params = SendGossipRequestWorker.Params( - sessionId = sessionId, - keyShareRequest = request as? OutgoingRoomKeyRequest, - secretShareRequest = request as? OutgoingSecretRequest, - txnId = createUniqueTxnId() - ) - cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.SENDING) - val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true) - gossipingWorkManager.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("## CRYPTO - sendOutgoingRoomKeyRequestCancellation $request") - val params = CancelGossipRequestWorker.Params.fromRequest(sessionId, request) - cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.CANCELLING) - - val workRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(params), true) - gossipingWorkManager.postWork(workRequest) - - if (resend) { - val reSendParams = SendGossipRequestWorker.Params( - sessionId = sessionId, - keyShareRequest = request.copy(requestId = RequestIdHelper.createUniqueRequestId()), - txnId = createUniqueTxnId() - ) - val reSendWorkRequest = gossipingWorkManager.createWork(WorkerParamsFactory.toData(reSendParams), true) - gossipingWorkManager.postWork(reSendWorkRequest) - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt new file mode 100755 index 0000000000..09a9868428 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt @@ -0,0 +1,518 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.crypto + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.crypto.MXCryptoConfig +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest +import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState +import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject +import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody +import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.util.fromBase64 +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer +import timber.log.Timber +import java.util.Stack +import java.util.concurrent.Executors +import javax.inject.Inject +import kotlin.system.measureTimeMillis + +private val loggerTag = LoggerTag("OutgoingKeyRequestManager", LoggerTag.CRYPTO) + +/** + * This class is responsible for sending key requests to other devices when a message failed to decrypt. + * It's lifecycle is based on the sync pulse: + * - You can post queries for session, or report when you got a session + * - At the end of the sync (onSyncComplete) it will then process all the posted request and send to devices + * If a request failed it will be retried at the end of the next sync + */ +@SessionScope +internal class OutgoingKeyRequestManager @Inject constructor( + @SessionId private val sessionId: String, + @UserId private val myUserId: String, + private val cryptoStore: IMXCryptoStore, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoConfig: MXCryptoConfig, + private val inboundGroupSessionStore: InboundGroupSessionStore, + private val sendToDeviceTask: SendToDeviceTask, + private val deviceListManager: DeviceListManager, + private val perSessionBackupQueryRateLimiter: PerSessionBackupQueryRateLimiter) { + + private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher) + private val sequencer = SemaphoreCoroutineSequencer() + + // We only have one active key request per session, so we don't request if it's already requested + // But it could make sense to check more the backup, as it's evolving. + // We keep a stack as we consider that the key requested last is more likely to be on screen? + private val requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup = Stack>() + + fun requestKeyForEvent(event: Event, force: Boolean) { + val (targets, body) = getRoomKeyRequestTargetForEvent(event) ?: return + val index = ratchetIndexForMessage(event) ?: 0 + postRoomKeyRequest(body, targets, index, force) + } + + private fun getRoomKeyRequestTargetForEvent(event: Event): Pair>, RoomKeyRequestBody>? { + val sender = event.senderId ?: return null + val encryptedEventContent = event.content.toModel() ?: return null.also { + Timber.tag(loggerTag.value).e("getRoomKeyRequestTargetForEvent Failed to re-request key, null content") + } + if (encryptedEventContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null + + val senderDevice = encryptedEventContent.deviceId + val recipients = if (cryptoConfig.limitRoomKeyRequestsToMyDevices) { + mapOf( + myUserId to listOf("*") + ) + } else { + if (event.senderId == myUserId) { + mapOf( + myUserId 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( + myUserId to listOf("*"), + + // We might not have deviceId in the future due to https://github.com/matrix-org/matrix-spec-proposals/pull/3700 + // so in this case query to all + sender to listOf(senderDevice ?: "*") + ) + } + } + + val requestBody = RoomKeyRequestBody( + roomId = event.roomId, + algorithm = encryptedEventContent.algorithm, + senderKey = encryptedEventContent.senderKey, + sessionId = encryptedEventContent.sessionId + ) + return recipients to requestBody + } + + private fun ratchetIndexForMessage(event: Event): Int? { + val encryptedContent = event.content.toModel() ?: return null + if (encryptedContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null + return encryptedContent.ciphertext?.fromBase64()?.inputStream()?.reader()?.let { + tryOrNull { + val megolmVersion = it.read() + if (megolmVersion != 3) return@tryOrNull null + /** Int tag */ + if (it.read() != 8) return@tryOrNull null + it.read() + } + } + } + + fun postRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int, force: Boolean = false) { + outgoingRequestScope.launch { + sequencer.post { + internalQueueRequest(requestBody, recipients, fromIndex, force) + } + } + } + + /** + * Typically called when we the session as been imported or received meanwhile + */ + fun postCancelRequestForSessionIfNeeded(sessionId: String, roomId: String, senderKey: String, fromIndex: Int) { + outgoingRequestScope.launch { + sequencer.post { + internalQueueCancelRequest(sessionId, roomId, senderKey, fromIndex) + } + } + } + + fun onSelfCrossSigningTrustChanged(newTrust: Boolean) { + if (newTrust) { + // we were previously not cross signed, but we are now + // so there is now more chances to get better replies for existing request + // Let's forget about sent request so that next time we try to decrypt we will resend requests + // We don't resend all because we don't want to generate a bulk of traffic + outgoingRequestScope.launch { + sequencer.post { + cryptoStore.deleteOutgoingRoomKeyRequestInState(OutgoingRoomKeyRequestState.SENT) + } + + sequencer.post { + delay(1000) + perSessionBackupQueryRateLimiter.refreshBackupInfoIfNeeded(true) + } + } + } + } + + fun onRoomKeyForwarded(sessionId: String, + algorithm: String, + roomId: String, + senderKey: String, + fromDevice: String?, + fromIndex: Int, + event: Event) { + Timber.tag(loggerTag.value).d("Key forwarded for $sessionId from ${event.senderId}|$fromDevice at index $fromIndex") + outgoingRequestScope.launch { + sequencer.post { + cryptoStore.updateOutgoingRoomKeyReply( + roomId = roomId, + sessionId = sessionId, + algorithm = algorithm, + senderKey = senderKey, + fromDevice = fromDevice, + // strip out encrypted stuff as it's just a trail? + event = event.copy( + type = event.getClearType(), + content = mapOf( + "chain_index" to fromIndex + ) + ) + ) + } + } + } + + fun onRoomKeyWithHeld(sessionId: String, + algorithm: String, + roomId: String, + senderKey: String, + fromDevice: String?, + event: Event) { + outgoingRequestScope.launch { + sequencer.post { + Timber.tag(loggerTag.value).d("Withheld received for $sessionId from ${event.senderId}|$fromDevice") + Timber.tag(loggerTag.value).v("Withheld content ${event.getClearContent()}") + + // We want to store withheld code from the sender of the message (owner of the megolm session), not from + // other devices that might gossip the key. If not the initial reason might be overridden + // by a request to one of our session. + event.getClearContent().toModel()?.let { withheld -> + withContext(coroutineDispatchers.crypto) { + tryOrNull { + deviceListManager.downloadKeys(listOf(event.senderId ?: ""), false) + } + cryptoStore.getUserDeviceList(event.senderId ?: "") + .also { devices -> + Timber.tag(loggerTag.value) + .v("Withheld Devices for ${event.senderId} are ${devices.orEmpty().joinToString { it.identityKey() ?: "" }}") + } + ?.firstOrNull { + it.identityKey() == senderKey + } + }.also { + Timber.tag(loggerTag.value).v("Withheld device for sender key $senderKey is from ${it?.shortDebugString()}") + }?.let { + if (it.userId == event.senderId) { + if (fromDevice != null) { + if (it.deviceId == fromDevice) { + Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}") + cryptoStore.addWithHeldMegolmSession(withheld) + } + } else { + Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}") + cryptoStore.addWithHeldMegolmSession(withheld) + } + } + } + } + + // Here we store the replies from a given request + cryptoStore.updateOutgoingRoomKeyReply( + roomId = roomId, + sessionId = sessionId, + algorithm = algorithm, + senderKey = senderKey, + fromDevice = fromDevice, + event = event + ) + } + } + } + + /** + * Should be called after a sync, ideally if no catchup sync needed (as keys might arrive in those) + */ + fun requireProcessAllPendingKeyRequests() { + outgoingRequestScope.launch { + sequencer.post { + internalProcessPendingKeyRequests() + } + } + } + + private fun internalQueueCancelRequest(sessionId: String, roomId: String, senderKey: String, localKnownChainIndex: Int) { + // do we have known requests for that session?? + Timber.tag(loggerTag.value).v("Cancel Key Request if needed for $sessionId") + val knownRequest = cryptoStore.getOutgoingRoomKeyRequest( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM, + roomId = roomId, + sessionId = sessionId, + senderKey = senderKey + ) + if (knownRequest.isEmpty()) return Unit.also { + Timber.tag(loggerTag.value).v("Handle Cancel Key Request for $sessionId -- Was not currently requested") + } + if (knownRequest.size > 1) { + // It's worth logging, there should be only one + Timber.tag(loggerTag.value).w("Found multiple requests for same sessionId $sessionId") + } + knownRequest.forEach { request -> + when (request.state) { + OutgoingRoomKeyRequestState.UNSENT -> { + if (request.fromIndex >= localKnownChainIndex) { + // we have a good index we can cancel + cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) + } + } + OutgoingRoomKeyRequestState.SENT -> { + // It was already sent, and index satisfied we can cancel + if (request.fromIndex >= localKnownChainIndex) { + cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING) + } + } + OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> { + // It is already marked to be cancelled + } + OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> { + if (request.fromIndex >= localKnownChainIndex) { + // we just want to cancel now + cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING) + } + } + OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> { + // was already canceled + // if we need a better index, should we resend? + } + } + } + } + + fun close() { + try { + outgoingRequestScope.cancel("User Terminate") + requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.clear() + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).w("Failed to shutDown request manager") + } + } + + private fun internalQueueRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int, force: Boolean) { + if (!cryptoStore.isKeyGossipingEnabled()) { + // we might want to try backup? + if (requestBody.roomId != null && requestBody.sessionId != null) { + requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(requestBody.roomId to requestBody.sessionId) + } + Timber.tag(loggerTag.value).d("discarding request for ${requestBody.sessionId} as gossiping is disabled") + return + } + + Timber.tag(loggerTag.value).d("Queueing key request for ${requestBody.sessionId} force:$force") + val existing = cryptoStore.getOutgoingRoomKeyRequest(requestBody) + Timber.tag(loggerTag.value).v("Queueing key request exiting is ${existing?.state}") + when (existing?.state) { + null -> { + // create a new one + cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex) + } + OutgoingRoomKeyRequestState.UNSENT -> { + // nothing it's new or not yet handled + } + OutgoingRoomKeyRequestState.SENT -> { + // it was already requested + Timber.tag(loggerTag.value).d("The session ${requestBody.sessionId} is already requested") + if (force) { + // update to UNSENT + Timber.tag(loggerTag.value).d(".. force to request ${requestBody.sessionId}") + cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND) + } else { + if (existing.roomId != null && existing.sessionId != null) { + requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(existing.roomId to existing.sessionId) + } + } + } + OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> { + // request is canceled only if I got the keys so what to do here... + if (force) { + cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND) + } + } + OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> { + // It's already going to resend + } + OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> { + if (force) { + cryptoStore.deleteOutgoingRoomKeyRequest(existing.requestId) + cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex) + } + } + } + + if (existing != null && existing.fromIndex >= fromIndex) { + // update the required index + cryptoStore.updateOutgoingRoomKeyRequiredIndex(existing.requestId, fromIndex) + } + } + + private suspend fun internalProcessPendingKeyRequests() { + val toProcess = cryptoStore.getOutgoingRoomKeyRequests(OutgoingRoomKeyRequestState.pendingStates()) + Timber.tag(loggerTag.value).v("Processing all pending key requests (found ${toProcess.size} pending)") + + measureTimeMillis { + toProcess.forEach { + when (it.state) { + OutgoingRoomKeyRequestState.UNSENT -> handleUnsentRequest(it) + OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> handleRequestToCancel(it) + OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> handleRequestToCancelWillResend(it) + OutgoingRoomKeyRequestState.SENT_THEN_CANCELED, + OutgoingRoomKeyRequestState.SENT -> { + // these are filtered out + } + } + } + }.let { + Timber.tag(loggerTag.value).v("Finish processing pending key request in $it ms") + } + + val maxBackupCallsBySync = 60 + var currentCalls = 0 + measureTimeMillis { + while (requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.isNotEmpty() && currentCalls < maxBackupCallsBySync) { + requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.pop().let { (roomId, sessionId) -> + // we want to rate limit that somehow :/ + perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId) + } + currentCalls++ + } + }.let { + Timber.tag(loggerTag.value).v("Finish querying backup in $it ms") + } + } + + private suspend fun handleUnsentRequest(request: OutgoingKeyRequest) { + // In order to avoid generating to_device traffic, we can first check if the key is backed up + Timber.tag(loggerTag.value).v("Handling unsent request for megolm session ${request.sessionId} in ${request.roomId}") + val sessionId = request.sessionId ?: return + val roomId = request.roomId ?: return + if (perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)) { + // let's see what's the index + val knownIndex = tryOrNull { + inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "")?.wrapper?.firstKnownIndex + } + if (knownIndex != null && knownIndex <= request.fromIndex) { + // we found the key in backup with good enough index, so we can just mark as cancelled, no need to send request + Timber.tag(loggerTag.value).v("Megolm session $sessionId successfully restored from backup, do not send request") + cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) + return + } + } + + // we need to send the request + val toDeviceContent = RoomKeyShareRequest( + requestingDeviceId = cryptoStore.getDeviceId(), + requestId = request.requestId, + action = GossipingToDeviceObject.ACTION_SHARE_REQUEST, + body = request.requestBody + ) + val contentMap = MXUsersDevicesMap() + request.recipients.forEach { userToDeviceMap -> + userToDeviceMap.value.forEach { deviceId -> + contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) + } + } + + val params = SendToDeviceTask.Params( + eventType = EventType.ROOM_KEY_REQUEST, + contentMap = contentMap, + transactionId = request.requestId + ) + try { + withContext(coroutineDispatchers.io) { + sendToDeviceTask.executeRetry(params, 3) + } + Timber.tag(loggerTag.value).d("Key request sent for $sessionId in room $roomId to ${request.recipients}") + // The request was sent, so update state + cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT) + // TODO update the audit trail + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).v("Failed to request $sessionId targets:${request.recipients}") + } + } + + private suspend fun handleRequestToCancel(request: OutgoingKeyRequest): Boolean { + Timber.tag(loggerTag.value).v("handleRequestToCancel for megolm session ${request.sessionId}") + // we have to cancel this + val toDeviceContent = RoomKeyShareRequest( + requestingDeviceId = cryptoStore.getDeviceId(), + requestId = request.requestId, + action = GossipingToDeviceObject.ACTION_SHARE_CANCELLATION + ) + val contentMap = MXUsersDevicesMap() + request.recipients.forEach { userToDeviceMap -> + userToDeviceMap.value.forEach { deviceId -> + contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) + } + } + + val params = SendToDeviceTask.Params( + eventType = EventType.ROOM_KEY_REQUEST, + contentMap = contentMap, + transactionId = request.requestId + ) + return try { + withContext(coroutineDispatchers.io) { + sendToDeviceTask.executeRetry(params, 3) + } + // The request cancellation was sent, we don't delete yet because we want + // to keep trace of the sent replies + cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT_THEN_CANCELED) + true + } catch (failure: Throwable) { + Timber.tag(loggerTag.value).v("Failed to cancel request ${request.requestId} for session $sessionId targets:${request.recipients}") + false + } + } + + private suspend fun handleRequestToCancelWillResend(request: OutgoingKeyRequest) { + if (handleRequestToCancel(request)) { + // this will create a new unsent request with no replies that will be process in the following call + cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId) + request.requestBody?.let { cryptoStore.getOrAddOutgoingRoomKeyRequest(it, request.recipients, request.fromIndex) } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt deleted file mode 100755 index 2ba2f5c817..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.crypto - -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState - -/** - * Represents an outgoing room key request - */ -@JsonClass(generateAdapter = true) -internal 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/org/matrix/android/sdk/internal/crypto/PerSessionBackupQueryRateLimiter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PerSessionBackupQueryRateLimiter.kt new file mode 100644 index 0000000000..0c059e7ca9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/PerSessionBackupQueryRateLimiter.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.crypto + +import dagger.Lazy +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult +import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo +import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult +import org.matrix.android.sdk.api.util.awaitCallback +import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.util.time.Clock +import timber.log.Timber +import javax.inject.Inject + +// I keep the same name as OutgoingGossipingRequestManager to ease filtering of logs +private val loggerTag = LoggerTag("OutgoingGossipingRequestManager", LoggerTag.CRYPTO) + +/** + * Used to try to get the key for UISI messages before sending room key request. + * We are adding some rate limiting to avoid querying too much for a key not in backup. + * Nonetheless the backup can be updated so we might want to retry from time to time. + */ +internal class PerSessionBackupQueryRateLimiter @Inject constructor( + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val keysBackupService: Lazy, + private val cryptoStore: IMXCryptoStore, + private val clock: Clock, +) { + + companion object { + val MIN_TRY_BACKUP_PERIOD_MILLIS = 60 * 60_000 // 1 hour + } + + data class Info( + val megolmSessionId: String, + val roomId: String + ) + + data class LastTry( + val backupVersion: String, + val timestamp: Long + ) + + /** + * Remember what we already tried (a key not in backup or some server issue) + * We might want to retry from time to time as the backup could have been updated + */ + private val lastFailureMap = mutableMapOf() + + private var backupVersion: KeysVersionResult? = null + private var savedKeyBackupKeyInfo: SavedKeyBackupKeyInfo? = null + var backupWasCheckedFromServer: Boolean = false + val now = clock.epochMillis() + + fun refreshBackupInfoIfNeeded(force: Boolean = false) { + if (backupWasCheckedFromServer && !force) return + Timber.tag(loggerTag.value).v("Checking if can access a backup") + backupWasCheckedFromServer = true + val knownBackupSecret = cryptoStore.getKeyBackupRecoveryKeyInfo() + ?: return Unit.also { + Timber.tag(loggerTag.value).v("We don't have the backup secret!") + } + this.backupVersion = keysBackupService.get().keysBackupVersion + this.savedKeyBackupKeyInfo = knownBackupSecret + } + + suspend fun tryFromBackupIfPossible(sessionId: String, roomId: String): Boolean { + Timber.tag(loggerTag.value).v("tryFromBackupIfPossible for session:$sessionId in $roomId") + refreshBackupInfoIfNeeded() + val currentVersion = backupVersion + if (savedKeyBackupKeyInfo?.version == null || + currentVersion == null || + currentVersion.version != savedKeyBackupKeyInfo?.version) { + // We can't access the backup + Timber.tag(loggerTag.value).v("Can't get backup version info") + return false + } + val cacheKey = Info(sessionId, roomId) + val lastTry = lastFailureMap[cacheKey] + val shouldQuery = + lastTry == null || + lastTry.backupVersion != currentVersion.version || + (now - lastTry.timestamp) > MIN_TRY_BACKUP_PERIOD_MILLIS + + if (!shouldQuery) return false + + val successfullyImported = withContext(coroutineDispatchers.io) { + try { + awaitCallback { + keysBackupService.get().restoreKeysWithRecoveryKey( + currentVersion, + savedKeyBackupKeyInfo?.recoveryKey ?: "", + roomId, + sessionId, + null, + it + ) + }.successfullyNumberOfImportedKeys + } catch (failure: Throwable) { + // Fail silently + Timber.tag(loggerTag.value).v("getFromBackup failed ${failure.localizedMessage}") + 0 + } + } + if (successfullyImported == 1) { + Timber.tag(loggerTag.value).v("Found key in backup session:$sessionId in $roomId") + lastFailureMap.remove(cacheKey) + return true + } else { + Timber.tag(loggerTag.value).v("Failed to find key in backup session:$sessionId in $roomId") + lastFailureMap[cacheKey] = LastTry(currentVersion.version, clock.epochMillis()) + return false + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt index ba97d96133..1a8c160d9c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomEncryptorsStore.kt @@ -16,12 +16,21 @@ package org.matrix.android.sdk.internal.crypto +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting +import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory +import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.session.SessionScope import javax.inject.Inject @SessionScope -internal class RoomEncryptorsStore @Inject constructor() { +internal class RoomEncryptorsStore @Inject constructor( + private val cryptoStore: IMXCryptoStore, + private val megolmEncryptionFactory: MXMegolmEncryptionFactory, + private val olmEncryptionFactory: MXOlmEncryptionFactory, +) { // MXEncrypting instance for each room. private val roomEncryptors = mutableMapOf() @@ -34,7 +43,18 @@ internal class RoomEncryptorsStore @Inject constructor() { fun get(roomId: String): IMXEncrypting? { return synchronized(roomEncryptors) { - roomEncryptors[roomId] + val cache = roomEncryptors[roomId] + if (cache != null) { + return@synchronized cache + } else { + val alg: IMXEncrypting? = when (cryptoStore.getRoomAlgorithm(roomId)) { + MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId) + MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId) + else -> null + } + alg?.let { roomEncryptors.put(roomId, it) } + return@synchronized alg + } } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt new file mode 100644 index 0000000000..6fb6914206 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SecretShareManager.kt @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.crypto + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey +import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener +import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.util.toBase64NoPadding +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.time.Clock +import timber.log.Timber +import javax.inject.Inject + +private val loggerTag = LoggerTag("SecretShareManager", LoggerTag.CRYPTO) + +@SessionScope +internal class SecretShareManager @Inject constructor( + private val credentials: Credentials, + private val cryptoStore: IMXCryptoStore, + private val cryptoCoroutineScope: CoroutineScope, + private val messageEncrypter: MessageEncrypter, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val sendToDeviceTask: SendToDeviceTask, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val clock: Clock, +) { + + companion object { + private const val SECRET_SHARE_WINDOW_DURATION = 5 * 60 * 1000 // 5 minutes + } + + /** + * Secret gossiping only occurs during a limited window period after interactive verification. + * We keep track of recent verification in memory for that purpose (no need to persist) + */ + private val recentlyVerifiedDevices = mutableMapOf() + private val verifMutex = Mutex() + + /** + * Secrets are exchanged as part of interactive verification, + * so we can just store in memory. + */ + private val outgoingSecretRequests = mutableListOf() + + // the listeners + private val gossipingRequestListeners: MutableSet = HashSet() + + fun addListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.add(listener) + } + } + + fun removeListener(listener: GossipingRequestListener) { + synchronized(gossipingRequestListeners) { + gossipingRequestListeners.remove(listener) + } + } + + /** + * Called when a session has been verified. + * This information can be used by the manager to decide whether or not to fullfill gossiping requests. + * This should be called as fast as possible after a successful self interactive verification + */ + fun onVerificationCompleteForDevice(deviceId: String) { + // For now we just keep an in memory cache + cryptoCoroutineScope.launch { + verifMutex.withLock { + recentlyVerifiedDevices[deviceId] = clock.epochMillis() + } + } + } + + suspend fun handleSecretRequest(toDevice: Event) { + val request = toDevice.getClearContent().toModel() + ?: return Unit.also { + Timber.tag(loggerTag.value) + .w("handleSecretRequest() : malformed request") + } + +// val (action, requestingDeviceId, requestId, secretName) = it + val secretName = request.secretName ?: return Unit.also { + Timber.tag(loggerTag.value) + .v("handleSecretRequest() : Missing secret name") + } + + val userId = toDevice.senderId ?: return Unit.also { + Timber.tag(loggerTag.value) + .v("handleSecretRequest() : Missing senderId") + } + + if (userId != credentials.userId) { + // secrets are only shared between our own devices + Timber.tag(loggerTag.value) + .e("Ignoring secret share request from other users $userId") + return + } + + val deviceId = request.requestingDeviceId + ?: return Unit.also { + Timber.tag(loggerTag.value) + .w("handleSecretRequest() : malformed request norequestingDeviceId ") + } + + val device = cryptoStore.getUserDevice(credentials.userId, deviceId) + ?: return Unit.also { + Timber.tag(loggerTag.value) + .e("Received secret share request from unknown device $deviceId") + } + + val isRequestingDeviceTrusted = device.isVerified + val isRecentInteractiveVerification = hasBeenVerifiedLessThanFiveMinutesFromNow(device.deviceId) + if (isRequestingDeviceTrusted && isRecentInteractiveVerification) { + // we can share the secret + + val secretValue = when (secretName) { + MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master + SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned + USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user + KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey + ?.let { + extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding() + } + else -> null + } + if (secretValue == null) { + Timber.tag(loggerTag.value) + .i("The secret is unknown $secretName, passing to app layer") + val toList = synchronized(gossipingRequestListeners) { gossipingRequestListeners.toList() } + toList.onEach { listener -> + listener.onSecretShareRequest(request) + } + return + } + + val payloadJson = mapOf( + "type" to EventType.SEND_SECRET, + "content" to mapOf( + "request_id" to request.requestId, + "secret" to secretValue + ) + ) + + // Is it possible that we don't have an olm session? + val devicesByUser = mapOf(device.userId to listOf(device)) + val usersDeviceMap = try { + ensureOlmSessionsForDevicesAction.handle(devicesByUser) + } catch (failure: Throwable) { + Timber.tag(loggerTag.value) + .w("Can't share secret ${request.secretName}: Failed to establish olm session") + return + } + + val olmSessionResult = usersDeviceMap.getObject(device.userId, device.deviceId) + if (olmSessionResult?.sessionId == null) { + Timber.tag(loggerTag.value) + .w("secret share: no session with this device $deviceId, probably because there were no one-time keys") + return + } + + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(device)) + val sendToDeviceMap = MXUsersDevicesMap() + sendToDeviceMap.setObject(device.userId, device.deviceId, encodedPayload) + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + try { + // raise the retries for secret + sendToDeviceTask.executeRetry(sendToDeviceParams, 6) + Timber.tag(loggerTag.value) + .i("successfully shared secret $secretName to ${device.shortDebugString()}") + // TODO add a trail for that in audit logs + } catch (failure: Throwable) { + Timber.tag(loggerTag.value) + .e(failure, "failed to send shared secret $secretName to ${device.shortDebugString()}") + } + } else { + Timber.tag(loggerTag.value) + .d(" Received secret share request from un-authorised device ${device.deviceId}") + } + } + + private suspend fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean { + val verifTimestamp = verifMutex.withLock { + recentlyVerifiedDevices[deviceId] + } ?: return false + + val age = clock.epochMillis() - verifTimestamp + + return age < SECRET_SHARE_WINDOW_DURATION + } + + suspend fun requestSecretTo(deviceId: String, secretName: String) { + val cryptoDeviceInfo = cryptoStore.getUserDevice(credentials.userId, deviceId) ?: return Unit.also { + Timber.tag(loggerTag.value) + .d("Can't request secret for $secretName unknown device $deviceId") + } + val toDeviceContent = SecretShareRequest( + requestingDeviceId = credentials.deviceId, + secretName = secretName, + requestId = createUniqueTxnId() + ) + + verifMutex.withLock { + outgoingSecretRequests.add(toDeviceContent) + } + + val contentMap = MXUsersDevicesMap() + contentMap.setObject(cryptoDeviceInfo.userId, cryptoDeviceInfo.deviceId, toDeviceContent) + + val params = SendToDeviceTask.Params( + eventType = EventType.REQUEST_SECRET, + contentMap = contentMap + ) + try { + withContext(coroutineDispatchers.io) { + sendToDeviceTask.executeRetry(params, 3) + } + Timber.tag(loggerTag.value) + .d("Secret request sent for $secretName to ${cryptoDeviceInfo.shortDebugString()}") + // TODO update the audit trail + } catch (failure: Throwable) { + Timber.tag(loggerTag.value) + .w("Failed to request secret $secretName to ${cryptoDeviceInfo.shortDebugString()}") + } + } + + suspend fun onSecretSendReceived(toDevice: Event, handleGossip: ((name: String, value: String) -> Boolean)) { + Timber.tag(loggerTag.value) + .i("onSecretSend() from ${toDevice.senderId} : onSecretSendReceived ${toDevice.content?.get("sender_key")}") + if (!toDevice.isEncrypted()) { + // secret send messages must be encrypted + Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event") + return + } + + // Was that sent by us? + if (toDevice.senderId != credentials.userId) { + Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}") + return + } + + val secretContent = toDevice.getClearContent().toModel() ?: return + + val existingRequest = verifMutex.withLock { + outgoingSecretRequests.firstOrNull { it.requestId == secretContent.requestId } + } + + // As per spec: + // Clients should ignore m.secret.send events received from devices that it did not send an m.secret.request event to. + if (existingRequest?.secretName == null) { + Timber.tag(loggerTag.value).i("onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") + return + } + // we don't need to cancel the request as we only request to one device + // just forget about the request now + verifMutex.withLock { + outgoingSecretRequests.remove(existingRequest) + } + + if (!handleGossip(existingRequest.secretName, secretContent.secretValue)) { + // TODO Ask to application layer? + Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK") + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt deleted file mode 100644 index 3b43ad672b..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.crypto - -import android.content.Context -import androidx.work.WorkerParameters -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.failure.shouldBeRetried -import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest -import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId -import org.matrix.android.sdk.internal.session.SessionComponent -import org.matrix.android.sdk.internal.util.time.Clock -import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker -import org.matrix.android.sdk.internal.worker.SessionWorkerParams -import timber.log.Timber -import javax.inject.Inject - -internal class SendGossipRequestWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) : - SessionSafeCoroutineWorker(context, params, sessionManager, Params::class.java) { - - @JsonClass(generateAdapter = true) - internal data class Params( - override val sessionId: String, - val keyShareRequest: OutgoingRoomKeyRequest? = null, - val secretShareRequest: OutgoingSecretRequest? = null, - // The txnId for the sendToDevice request. Nullable for compatibility reasons, but MUST always be provided - // to use the same value if this worker is retried. - val txnId: String? = null, - override val lastFailureMessage: String? = null - ) : SessionWorkerParams - - @Inject lateinit var sendToDeviceTask: SendToDeviceTask - @Inject lateinit var cryptoStore: IMXCryptoStore - @Inject lateinit var credentials: Credentials - @Inject lateinit var clock: Clock - - override fun injectWith(injector: SessionComponent) { - injector.inject(this) - } - - override suspend fun doSafeWork(params: Params): Result { - // params.txnId should be provided in all cases now. But Params can be deserialized by - // the WorkManager from data serialized in a previous version of the application, so without the txnId field. - // So if not present, we create a txnId - val txnId = params.txnId ?: createUniqueTxnId() - 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 = clock.epochMillis() - }) - - 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 = clock.epochMillis() - }) - - params.secretShareRequest.recipients.forEach { userToDeviceMap -> - userToDeviceMap.value.forEach { deviceId -> - contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent) - } - } - } - else -> { - return buildErrorResult(params, "Unknown empty gossiping request").also { - Timber.e("Unknown empty gossiping request: $params") - } - } - } - try { - cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENDING) - sendToDeviceTask.execute( - SendToDeviceTask.Params( - eventType = eventType, - contentMap = contentMap, - transactionId = txnId - ) - ) - cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENT) - return Result.success() - } catch (throwable: Throwable) { - return if (throwable.shouldBeRetried()) { - Result.retry() - } else { - cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.FAILED_TO_SEND) - buildErrorResult(params, throwable.localizedMessage ?: "error") - } - } - } - - override fun buildErrorParams(params: Params, message: String): Params { - return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt deleted file mode 100644 index 113d71d387..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.crypto - -import android.content.Context -import androidx.work.WorkerParameters -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.auth.data.Credentials -import org.matrix.android.sdk.api.failure.shouldBeRetried -import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter -import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId -import org.matrix.android.sdk.internal.session.SessionComponent -import org.matrix.android.sdk.internal.util.time.Clock -import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker -import org.matrix.android.sdk.internal.worker.SessionWorkerParams -import timber.log.Timber -import javax.inject.Inject - -internal class SendGossipWorker( - context: Context, - params: WorkerParameters, - sessionManager: SessionManager -) : SessionSafeCoroutineWorker(context, params, sessionManager, Params::class.java) { - - @JsonClass(generateAdapter = true) - internal data class Params( - override val sessionId: String, - val secretValue: String, - val requestUserId: String?, - val requestDeviceId: String?, - val requestId: String?, - // The txnId for the sendToDevice request. Nullable for compatibility reasons, but MUST always be provided - // to use the same value if this worker is retried. - val txnId: String? = null, - override val lastFailureMessage: String? = null - ) : SessionWorkerParams - - @Inject lateinit var sendToDeviceTask: SendToDeviceTask - @Inject lateinit var cryptoStore: IMXCryptoStore - @Inject lateinit var credentials: Credentials - @Inject lateinit var messageEncrypter: MessageEncrypter - @Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction - @Inject lateinit var clock: Clock - - override fun injectWith(injector: SessionComponent) { - injector.inject(this) - } - - override suspend fun doSafeWork(params: Params): Result { - // params.txnId should be provided in all cases now. But Params can be deserialized by - // the WorkManager from data serialized in a previous version of the application, so without the txnId field. - // So if not present, we create a txnId - val txnId = params.txnId ?: createUniqueTxnId() - val eventType: String = EventType.SEND_SECRET - - val toDeviceContent = SecretSendEventContent( - requestId = params.requestId ?: "", - secretValue = params.secretValue - ) - - val requestingUserId = params.requestUserId ?: "" - val requestingDeviceId = params.requestDeviceId ?: "" - val deviceInfo = cryptoStore.getUserDevice(requestingUserId, requestingDeviceId) - ?: return buildErrorResult(params, "Unknown deviceInfo, cannot send message").also { - cryptoStore.updateGossipingRequestState( - requestUserId = params.requestUserId, - requestDeviceId = params.requestDeviceId, - requestId = params.requestId, - state = GossipingRequestState.FAILED_TO_ACCEPTED - ) - Timber.e("Unknown deviceInfo, cannot send message, sessionId: ${params.requestDeviceId}") - } - - val sendToDeviceMap = MXUsersDevicesMap() - - val devicesByUser = mapOf(requestingUserId to listOf(deviceInfo)) - val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) - val olmSessionResult = usersDeviceMap.getObject(requestingUserId, requestingDeviceId) - if (olmSessionResult?.sessionId == null) { - // no session with this device, probably because there - // were no one-time keys. - return buildErrorResult(params, "no session with this device").also { - cryptoStore.updateGossipingRequestState( - requestUserId = params.requestUserId, - requestDeviceId = params.requestDeviceId, - requestId = params.requestId, - state = GossipingRequestState.FAILED_TO_ACCEPTED - ) - Timber.e("no session with this device $requestingDeviceId, probably because there were no one-time keys.") - } - } - - val payloadJson = mapOf( - "type" to EventType.SEND_SECRET, - "content" to toDeviceContent.toContent() - ) - - try { - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) - sendToDeviceMap.setObject(requestingUserId, requestingDeviceId, encodedPayload) - } catch (failure: Throwable) { - Timber.e("## Fail to encrypt gossip + ${failure.localizedMessage}") - } - - cryptoStore.saveGossipingEvent(Event( - type = eventType, - content = toDeviceContent.toContent(), - senderId = credentials.userId - ).also { - it.ageLocalTs = clock.epochMillis() - }) - - try { - sendToDeviceTask.execute( - SendToDeviceTask.Params( - eventType = EventType.ENCRYPTED, - contentMap = sendToDeviceMap, - transactionId = txnId - ) - ) - cryptoStore.updateGossipingRequestState( - requestUserId = params.requestUserId, - requestDeviceId = params.requestDeviceId, - requestId = params.requestId, - state = GossipingRequestState.ACCEPTED - ) - return Result.success() - } catch (throwable: Throwable) { - return if (throwable.shouldBeRetried()) { - Result.retry() - } else { - cryptoStore.updateGossipingRequestState( - requestUserId = params.requestUserId, - requestDeviceId = params.requestDeviceId, - requestId = params.requestId, - state = GossipingRequestState.FAILED_TO_ACCEPTED - ) - buildErrorResult(params, throwable.localizedMessage ?: "error") - } - } - } - - override fun buildErrorParams(params: Params, message: String): Params { - return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt index 86674b4ac5..22c4e59b18 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt @@ -17,12 +17,13 @@ package org.matrix.android.sdk.internal.crypto.actions import androidx.annotation.WorkerThread +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.MegolmSessionData -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager import org.matrix.android.sdk.internal.crypto.RoomDecryptorProvider import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore @@ -30,12 +31,13 @@ import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject -internal class MegolmSessionDataImporter @Inject constructor( - private val olmDevice: MXOlmDevice, - private val roomDecryptorProvider: RoomDecryptorProvider, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - private val cryptoStore: IMXCryptoStore, - private val clock: Clock, +private val loggerTag = LoggerTag("MegolmSessionDataImporter", LoggerTag.CRYPTO) + +internal class MegolmSessionDataImporter @Inject constructor(private val olmDevice: MXOlmDevice, + private val roomDecryptorProvider: RoomDecryptorProvider, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, + private val cryptoStore: IMXCryptoStore, + private val clock: Clock, ) { /** @@ -66,19 +68,23 @@ internal class MegolmSessionDataImporter @Inject constructor( if (null != decrypting) { try { val sessionId = megolmSessionData.sessionId - Timber.v("## importRoomKeys retrieve senderKey " + megolmSessionData.senderKey + " sessionId " + sessionId) + Timber.tag(loggerTag.value).v("## importRoomKeys retrieve senderKey ${megolmSessionData.senderKey} sessionId $sessionId") totalNumbersOfImportedKeys++ // cancel any outstanding room key requests for this session - val roomKeyRequestBody = RoomKeyRequestBody( - algorithm = megolmSessionData.algorithm, - roomId = megolmSessionData.roomId, - senderKey = megolmSessionData.senderKey, - sessionId = megolmSessionData.sessionId - ) - outgoingGossipingRequestManager.cancelRoomKeyRequest(roomKeyRequestBody) + Timber.tag(loggerTag.value).d("Imported megolm session $sessionId from backup=$fromBackup in ${megolmSessionData.roomId}") + outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded( + megolmSessionData.sessionId ?: "", + megolmSessionData.roomId ?: "", + megolmSessionData.senderKey ?: "", + tryOrNull { + olmInboundGroupSessionWrappers + .firstOrNull { it.olmInboundGroupSession?.sessionIdentifier() == megolmSessionData.sessionId } + ?.firstKnownIndex?.toInt() + } ?: 0 + ) // Have another go at decrypting events sent with this session when (decrypting) { @@ -87,7 +93,7 @@ internal class MegolmSessionDataImporter @Inject constructor( } } } catch (e: Exception) { - Timber.e(e, "## importRoomKeys() : onNewSession failed") + Timber.tag(loggerTag.value).e(e, "## importRoomKeys() : onNewSession failed") } } @@ -109,7 +115,7 @@ internal class MegolmSessionDataImporter @Inject constructor( val t1 = clock.epochMillis() - Timber.v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)") + Timber.tag(loggerTag.value).v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)") return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt index 2ea4e1dd29..d555062442 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt @@ -17,8 +17,6 @@ package org.matrix.android.sdk.internal.crypto.algorithms import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService @@ -44,23 +42,4 @@ internal interface IMXDecrypting { * @param event the key event. */ fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {} - - /** - * Determine if we have the keys necessary to respond to a room key request - * - * @param request keyRequest - * @return true if we have the keys and could (theoretically) share - */ - fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean = false - - /** - * Send the response to a room key request. - * - * @param request keyRequest - */ - fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {} - - fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue: String) {} - - fun requestKeysForEvent(event: Event, withHeld: Boolean) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt deleted file mode 100644 index 585bcdbbde..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.crypto.algorithms - -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent - -internal interface IMXWithHeldExtension { - fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index 8ba4f6b56b..d65b05f655 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -17,51 +17,32 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import dagger.Lazy -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.withLock -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.NewSessionListener import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent -import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent -import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting -import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.session.StreamEventsManager import timber.log.Timber private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO) -internal class MXMegolmDecryption(private val userId: String, - private val olmDevice: MXOlmDevice, - private val deviceListManager: DeviceListManager, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - private val messageEncrypter: MessageEncrypter, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val cryptoStore: IMXCryptoStore, - private val sendToDeviceTask: SendToDeviceTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope, - private val liveEventManager: Lazy -) : IMXDecrypting, IMXWithHeldExtension { +internal class MXMegolmDecryption( + private val olmDevice: MXOlmDevice, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, + private val cryptoStore: IMXCryptoStore, + private val liveEventManager: Lazy +) : IMXDecrypting { var newSessionListener: NewSessionListener? = null @@ -73,10 +54,7 @@ internal class MXMegolmDecryption(private val userId: String, @Throws(MXCryptoError::class) override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - // If cross signing is enabled, we don't send request until the keys are trusted - // There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once - val requestOnFail = cryptoStore.getMyCrossSigningInfo()?.isTrusted() == true - return decryptEvent(event, timeline, requestOnFail) + return decryptEvent(event, timeline, true) } @Throws(MXCryptoError::class) @@ -126,13 +104,14 @@ internal class MXMegolmDecryption(private val userId: String, if (throwable is MXCryptoError.OlmError) { // TODO Check the value of .message if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") { - // addEventToPendingList(event, timeline) - // The session might has been partially withheld (and only pass ratcheted) + // So we know that session, but it's ratcheted and we can't decrypt at that index + + if (requestKeysOnFail) { + requestKeysForEvent(event) + } + // Check if partially withheld val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) if (withHeldInfo != null) { - if (requestKeysOnFail) { - requestKeysForEvent(event, true) - } // Encapsulate as withHeld exception throw MXCryptoError.Base( MXCryptoError.ErrorType.KEYS_WITHHELD, @@ -141,10 +120,6 @@ internal class MXMegolmDecryption(private val userId: String, ) } - if (requestKeysOnFail) { - requestKeysForEvent(event, false) - } - throw MXCryptoError.Base( MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, "UNKNOWN_MESSAGE_INDEX", @@ -162,27 +137,22 @@ internal class MXMegolmDecryption(private val userId: String, ) } if (throwable is MXCryptoError.Base) { - if ( - /** if the session is unknown*/ - throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID - ) { + if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) { + // Check if it was withheld by sender to enrich error code val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) if (withHeldInfo != null) { if (requestKeysOnFail) { - requestKeysForEvent(event, true) + requestKeysForEvent(event) } // Encapsulate as withHeld exception throw MXCryptoError.Base( MXCryptoError.ErrorType.KEYS_WITHHELD, withHeldInfo.code?.value ?: "", - withHeldInfo.reason - ) - } else { - // This is un-used in Matrix Android SDK2, not sure if needed - // addEventToPendingList(event, timeline) - if (requestKeysOnFail) { - requestKeysForEvent(event, false) - } + withHeldInfo.reason) + } + + if (requestKeysOnFail) { + requestKeysForEvent(event) } } } @@ -198,55 +168,10 @@ internal class MXMegolmDecryption(private val userId: String, * * @param event the event */ - override fun requestKeysForEvent(event: Event, withHeld: Boolean) { - val sender = event.senderId ?: return - val encryptedEventContent = event.content.toModel() - val senderDevice = encryptedEventContent?.deviceId ?: return - - val recipients = if (event.senderId == userId || withHeld) { - mapOf( - userId to listOf("*") - ) - } else { - // for the case where you share the key with a device that has a broken olm session - // The other user might Re-shares a megolm session key with devices if the key has already been - // sent to them. - mapOf( - userId to listOf("*"), - sender to listOf(senderDevice) - ) - } - - val requestBody = RoomKeyRequestBody( - roomId = event.roomId, - algorithm = encryptedEventContent.algorithm, - senderKey = encryptedEventContent.senderKey, - sessionId = encryptedEventContent.sessionId - ) - - outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients) + private fun requestKeysForEvent(event: Event) { + outgoingKeyRequestManager.requestKeyForEvent(event, false) } -// /** -// * Add an event to the list of those we couldn't decrypt the first time we -// * saw them. -// * -// * @param event the event to try to decrypt later -// * @param timelineId the timeline identifier -// */ -// private fun addEventToPendingList(event: Event, timelineId: String) { -// val encryptedEventContent = event.content.toModel() ?: return -// val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}" -// -// val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() } -// val events = timeline.getOrPut(timelineId) { ArrayList() } -// -// if (event !in events) { -// Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}") -// events.add(event) -// } -// } - /** * Handle a key event. * @@ -266,6 +191,11 @@ internal class MXMegolmDecryption(private val userId: String, return } if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { + if (!cryptoStore.isKeyGossipingEnabled()) { + Timber.tag(loggerTag.value) + .i("onRoomKeyEvent(), ignore forward adding as per crypto config : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") + return + } Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") val forwardedRoomKeyContent = event.getClearContent().toModel() ?: return @@ -306,7 +236,7 @@ internal class MXMegolmDecryption(private val userId: String, } Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}") - val added = olmDevice.addInboundGroupSession( + val addSessionResult = olmDevice.addInboundGroupSession( roomKeyContent.sessionId, roomKeyContent.sessionKey, roomKeyContent.roomId, @@ -316,18 +246,47 @@ internal class MXMegolmDecryption(private val userId: String, exportFormat ) - if (added) { + when (addSessionResult) { + is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex + is MXOlmDevice.AddSessionResult.NotImportedHigherIndex -> addSessionResult.newIndex + else -> null + }?.let { index -> + if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { + val fromDevice = (event.content?.get("sender_key") as? String)?.let { senderDeviceIdentityKey -> + cryptoStore.getUserDeviceList(event.senderId ?: "") + ?.firstOrNull { + it.identityKey() == senderDeviceIdentityKey + } + }?.deviceId + + outgoingKeyRequestManager.onRoomKeyForwarded( + sessionId = roomKeyContent.sessionId, + algorithm = roomKeyContent.algorithm ?: "", + roomId = roomKeyContent.roomId, + senderKey = senderKey, + fromIndex = index, + fromDevice = fromDevice, + event = event) + + cryptoStore.saveIncomingForwardKeyAuditTrail( + roomId = roomKeyContent.roomId, + sessionId = roomKeyContent.sessionId, + senderKey = senderKey, + algorithm = roomKeyContent.algorithm ?: "", + userId = event.senderId ?: "", + deviceId = fromDevice ?: "", + chainIndex = index.toLong()) + + // The index is used to decide if we cancel sent request or if we wait for a better key + outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, senderKey, index) + } + } + + if (addSessionResult is MXOlmDevice.AddSessionResult.Imported) { + Timber.tag(loggerTag.value) + .d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}") defaultKeysBackupService.maybeBackupKeys() - val content = RoomKeyRequestBody( - algorithm = roomKeyContent.algorithm, - roomId = roomKeyContent.roomId, - sessionId = roomKeyContent.sessionId, - senderKey = senderKey - ) - - outgoingGossipingRequestManager.cancelRoomKeyRequest(content) - onNewSession(roomKeyContent.roomId, senderKey, roomKeyContent.sessionId) } } @@ -343,77 +302,4 @@ internal class MXMegolmDecryption(private val userId: String, Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey") newSessionListener?.onNewSession(roomId, senderKey, sessionId) } - - override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean { - val roomId = request.requestBody?.roomId ?: return false - val senderKey = request.requestBody.senderKey ?: return false - val sessionId = request.requestBody.sessionId ?: return false - return olmDevice.hasInboundSessionKeys(roomId, senderKey, sessionId) - } - - override fun shareKeysWithDevice(request: IncomingRoomKeyRequest) { - // sanity checks - if (request.requestBody == null) { - return - } - val userId = request.userId ?: return - - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - val body = request.requestBody - val sessionHolder = try { - olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session for request $body") - return@launch - } - - val export = sessionHolder.mutex.withLock { - sessionHolder.wrapper.exportKeys() - } ?: return@launch Unit.also { - Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session ${body.sessionId}") - } - - runCatching { deviceListManager.downloadKeys(listOf(userId), false) } - .mapCatching { - val deviceId = request.deviceId - val deviceInfo = cryptoStore.getUserDevice(userId, deviceId ?: "") - if (deviceInfo == null) { - throw RuntimeException() - } else { - val devicesByUser = mapOf(userId to listOf(deviceInfo)) - val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser) - val olmSessionResult = usersDeviceMap.getObject(userId, deviceId) - if (olmSessionResult?.sessionId == null) { - // no session with this device, probably because there - // were no one-time keys. - Timber.tag(loggerTag.value).e("no session with this device $deviceId, probably because there were no one-time keys.") - return@mapCatching - } - Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sharing session ${body.sessionId} with device $userId:$deviceId") - - val payloadJson = mapOf( - "type" to EventType.FORWARDED_ROOM_KEY, - "content" to export - ) - - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) - val sendToDeviceMap = MXUsersDevicesMap() - sendToDeviceMap.setObject(userId, deviceId, encodedPayload) - Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sending ${body.sessionId} to $userId:$deviceId") - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - try { - sendToDeviceTask.execute(sendToDeviceParams) - } catch (failure: Throwable) { - Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice() : Failed to send ${body.sessionId} to $userId:$deviceId") - } - } - } - } - } - - override fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - cryptoStore.addWithHeldMegolmSession(withHeldInfo) - } - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt index 8ad9f191d7..096773a959 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt @@ -17,46 +17,24 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import dagger.Lazy -import kotlinx.coroutines.CoroutineScope -import org.matrix.android.sdk.api.MatrixCoroutineDispatchers -import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.MXOlmDevice -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.StreamEventsManager import javax.inject.Inject internal class MXMegolmDecryptionFactory @Inject constructor( - @UserId private val userId: String, private val olmDevice: MXOlmDevice, - private val deviceListManager: DeviceListManager, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - private val messageEncrypter: MessageEncrypter, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, private val cryptoStore: IMXCryptoStore, - private val sendToDeviceTask: SendToDeviceTask, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val cryptoCoroutineScope: CoroutineScope, private val eventsManager: Lazy ) { fun create(): MXMegolmDecryption { return MXMegolmDecryption( - userId, olmDevice, - deviceListManager, - outgoingGossipingRequestManager, - messageEncrypter, - ensureOlmSessionsForDevicesAction, + outgoingKeyRequestManager, cryptoStore, - sendToDeviceTask, - coroutineDispatchers, - cryptoCoroutineScope, - eventsManager - ) + eventsManager) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 4ceef4fa5c..061c349645 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -28,7 +28,6 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.crypto.model.forEach import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode @@ -285,25 +284,14 @@ internal class MXMegolmEncryption( // attempted to share with) rather than the contentMap (those we did // share with), because we don't want to try to claim a one-time-key // for dead devices on every message. - val gossipingEventBuffer = arrayListOf() - for ((userId, devicesToShareWith) in devicesByUser) { + for ((_, devicesToShareWith) in devicesByUser) { for (deviceInfo in devicesToShareWith) { session.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex) - gossipingEventBuffer.add( - Event( - type = EventType.ROOM_KEY, - senderId = myUserId, - content = submap.apply { - this["session_key"] = "" - // we add a fake key for trail - this["_dest"] = "$userId|${deviceInfo.deviceId}" - } - )) + // XXX is it needed to add it to the audit trail? + // For now decided that no, we are more interested by forward trail } } - cryptoStore.saveGossipingEvents(gossipingEventBuffer) - if (haveTargets) { t0 = clock.epochMillis() Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target") @@ -346,7 +334,8 @@ internal class MXMegolmEncryption( senderKey = senderKey, algorithm = MXCRYPTO_ALGORITHM_MEGOLM, sessionId = sessionId, - codeString = code.value + codeString = code.value, + fromDevice = myDeviceId ) val params = SendToDeviceTask.Params( EventType.ROOM_KEY_WITHHELD, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt index 13eeefd1a6..1e66fe84c9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt @@ -265,8 +265,4 @@ internal class MXOlmDecryption( return res["payload"] } - - override fun requestKeysForEvent(event: Event, withHeld: Boolean) { - // nop - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt index e2887e5826..b8cdc922cc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.fromBase64 import org.matrix.android.sdk.internal.crypto.DeviceListManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask @@ -70,6 +71,7 @@ internal class DefaultCrossSigningService @Inject constructor( private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope, private val workManagerProvider: WorkManagerProvider, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository ) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener { @@ -785,7 +787,8 @@ 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() + // notify key requester + outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted) cryptoStore.updateUsersTrust { users.add(it) checkUserTrust(it).isVerified() @@ -800,19 +803,4 @@ internal class DefaultCrossSigningService @Inject constructor( } } } - -// 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/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt index e6352809c4..d2dae1c112 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -63,16 +63,11 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBack import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteBackupTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionDataTask -import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 @@ -112,16 +107,11 @@ internal class DefaultKeysBackupService @Inject constructor( // Tasks private val createKeysBackupVersionTask: CreateKeysBackupVersionTask, private val deleteBackupTask: DeleteBackupTask, - private val deleteRoomSessionDataTask: DeleteRoomSessionDataTask, - private val deleteRoomSessionsDataTask: DeleteRoomSessionsDataTask, - private val deleteSessionDataTask: DeleteSessionsDataTask, private val getKeysBackupLastVersionTask: GetKeysBackupLastVersionTask, private val getKeysBackupVersionTask: GetKeysBackupVersionTask, private val getRoomSessionDataTask: GetRoomSessionDataTask, private val getRoomSessionsDataTask: GetRoomSessionsDataTask, private val getSessionsDataTask: GetSessionsDataTask, - private val storeRoomSessionDataTask: StoreRoomSessionDataTask, - private val storeSessionsDataTask: StoreRoomSessionsDataTask, private val storeSessionDataTask: StoreSessionsDataTask, private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask, // Task executor @@ -168,58 +158,63 @@ internal class DefaultKeysBackupService @Inject constructor( override fun prepareKeysBackupVersion(password: String?, progressListener: ProgressListener?, callback: MatrixCallback) { - cryptoCoroutineScope.launch(coroutineDispatchers.main) { - runCatching { - withContext(coroutineDispatchers.crypto) { - val olmPkDecryption = OlmPkDecryption() - val signalableMegolmBackupAuthData = if (password != null) { - // Generate a private key from the password - val backgroundProgressListener = if (progressListener == null) { - null - } else { - object : ProgressListener { - override fun onProgress(progress: Int, total: Int) { - uiHandler.post { - try { - progressListener.onProgress(progress, total) - } catch (e: Exception) { - Timber.e(e, "prepareKeysBackupVersion: onProgress failure") - } + cryptoCoroutineScope.launch(coroutineDispatchers.io) { + try { + val olmPkDecryption = OlmPkDecryption() + val signalableMegolmBackupAuthData = if (password != null) { + // Generate a private key from the password + val backgroundProgressListener = if (progressListener == null) { + null + } else { + object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + uiHandler.post { + try { + progressListener.onProgress(progress, total) + } catch (e: Exception) { + Timber.e(e, "prepareKeysBackupVersion: onProgress failure") } } } } - - val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener) - SignalableMegolmBackupAuthData( - publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey), - privateKeySalt = generatePrivateKeyResult.salt, - privateKeyIterations = generatePrivateKeyResult.iterations - ) - } else { - val publicKey = olmPkDecryption.generateKey() - - SignalableMegolmBackupAuthData( - publicKey = publicKey - ) } - val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary()) - - val signedMegolmBackupAuthData = MegolmBackupAuthData( - publicKey = signalableMegolmBackupAuthData.publicKey, - privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt, - privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations, - signatures = objectSigner.signObject(canonicalJson) + val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener) + SignalableMegolmBackupAuthData( + publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey), + privateKeySalt = generatePrivateKeyResult.salt, + privateKeyIterations = generatePrivateKeyResult.iterations ) + } else { + val publicKey = olmPkDecryption.generateKey() - MegolmBackupCreationInfo( - algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, - authData = signedMegolmBackupAuthData, - recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey()) + SignalableMegolmBackupAuthData( + publicKey = publicKey ) } - }.foldToCallback(callback) + + val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary()) + + val signedMegolmBackupAuthData = MegolmBackupAuthData( + publicKey = signalableMegolmBackupAuthData.publicKey, + privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt, + privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations, + signatures = objectSigner.signObject(canonicalJson) + ) + + val creationInfo = MegolmBackupCreationInfo( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, + authData = signedMegolmBackupAuthData, + recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey()) + ) + uiHandler.post { + callback.onSuccess(creationInfo) + } + } catch (failure: Throwable) { + uiHandler.post { + callback.onFailure(failure) + } + } } } @@ -267,41 +262,39 @@ internal class DefaultKeysBackupService @Inject constructor( } override fun deleteBackup(version: String, callback: MatrixCallback?) { - cryptoCoroutineScope.launch(coroutineDispatchers.main) { - withContext(coroutineDispatchers.crypto) { - // If we're currently backing up to this backup... stop. - // (We start using it automatically in createKeysBackupVersion so this is symmetrical). - if (keysBackupVersion != null && version == keysBackupVersion?.version) { - resetKeysBackupData() - keysBackupVersion = null - keysBackupStateManager.state = KeysBackupState.Unknown - } + cryptoCoroutineScope.launch(coroutineDispatchers.io) { + // If we're currently backing up to this backup... stop. + // (We start using it automatically in createKeysBackupVersion so this is symmetrical). + if (keysBackupVersion != null && version == keysBackupVersion?.version) { + resetKeysBackupData() + keysBackupVersion = null + keysBackupStateManager.state = KeysBackupState.Unknown + } - deleteBackupTask - .configureWith(DeleteBackupTask.Params(version)) { - this.callback = object : MatrixCallback { - private fun eventuallyRestartBackup() { - // Do not stay in KeysBackupState.Unknown but check what is available on the homeserver - if (state == KeysBackupState.Unknown) { - checkAndStartKeysBackup() - } - } - - override fun onSuccess(data: Unit) { - eventuallyRestartBackup() - - uiHandler.post { callback?.onSuccess(Unit) } - } - - override fun onFailure(failure: Throwable) { - eventuallyRestartBackup() - - uiHandler.post { callback?.onFailure(failure) } + deleteBackupTask + .configureWith(DeleteBackupTask.Params(version)) { + this.callback = object : MatrixCallback { + private fun eventuallyRestartBackup() { + // Do not stay in KeysBackupState.Unknown but check what is available on the homeserver + if (state == KeysBackupState.Unknown) { + checkAndStartKeysBackup() } } + + override fun onSuccess(data: Unit) { + eventuallyRestartBackup() + + uiHandler.post { callback?.onSuccess(Unit) } + } + + override fun onFailure(failure: Throwable) { + eventuallyRestartBackup() + + uiHandler.post { callback?.onFailure(failure) } + } } - .executeBy(taskExecutor) - } + } + .executeBy(taskExecutor) } } @@ -480,10 +473,11 @@ internal class DefaultKeysBackupService @Inject constructor( if (authData == null) { Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data") - - callback.onFailure(IllegalArgumentException("Missing element")) + uiHandler.post { + callback.onFailure(IllegalArgumentException("Missing element")) + } } else { - cryptoCoroutineScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.io) { val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) { // Get current signatures, or create an empty set val myUserSignatures = authData.signatures?.get(userId).orEmpty().toMutableMap() @@ -536,11 +530,15 @@ internal class DefaultKeysBackupService @Inject constructor( checkAndStartWithKeysBackupVersion(newKeysBackupVersion) - callback.onSuccess(data) + uiHandler.post { + callback.onSuccess(data) + } } override fun onFailure(failure: Throwable) { - callback.onFailure(failure) + uiHandler.post { + callback.onFailure(failure) + } } } } @@ -554,15 +552,14 @@ internal class DefaultKeysBackupService @Inject constructor( callback: MatrixCallback) { Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}") - cryptoCoroutineScope.launch(coroutineDispatchers.main) { - val isValid = withContext(coroutineDispatchers.crypto) { - isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion) - } + cryptoCoroutineScope.launch(coroutineDispatchers.io) { + val isValid = isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion) if (!isValid) { Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.") - - callback.onFailure(IllegalArgumentException("Invalid recovery key or password")) + uiHandler.post { + callback.onFailure(IllegalArgumentException("Invalid recovery key or password")) + } } else { trustKeysBackupVersion(keysBackupVersion, true, callback) } @@ -574,15 +571,14 @@ internal class DefaultKeysBackupService @Inject constructor( callback: MatrixCallback) { Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}") - cryptoCoroutineScope.launch(coroutineDispatchers.main) { - val recoveryKey = withContext(coroutineDispatchers.crypto) { - recoveryKeyFromPassword(password, keysBackupVersion, null) - } + cryptoCoroutineScope.launch(coroutineDispatchers.io) { + val recoveryKey = recoveryKeyFromPassword(password, keysBackupVersion, null) if (recoveryKey == null) { Timber.w("trustKeysBackupVersionWithPassphrase: Key backup is missing required data") - - callback.onFailure(IllegalArgumentException("Missing element")) + uiHandler.post { + callback.onFailure(IllegalArgumentException("Missing element")) + } } else { // Check trust using the recovery key trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback) @@ -593,30 +589,28 @@ internal class DefaultKeysBackupService @Inject constructor( override fun onSecretKeyGossip(secret: String) { Timber.i("## CrossSigning - onSecretKeyGossip") - cryptoCoroutineScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.io) { try { - when (val keysBackupLastVersionResult = getKeysBackupLastVersionTask.execute(Unit)) { - KeysBackupLastVersionResult.NoKeysBackup -> { - Timber.d("No keys backup found") - } - is KeysBackupLastVersionResult.KeysBackup -> { - val keysBackupVersion = keysBackupLastVersionResult.keysVersionResult - val recoveryKey = computeRecoveryKey(secret.fromBase64()) - if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) { - awaitCallback { - trustKeysBackupVersion(keysBackupVersion, true, it) - } - val importResult = awaitCallback { - restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it) - } - withContext(coroutineDispatchers.crypto) { - cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version) - } - Timber.i("onSecretKeyGossip: Recovered keys $importResult") - } else { - Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}") + val keysBackupVersion = getKeysBackupLastVersionTask.execute(Unit).toKeysVersionResult() + ?: return@launch Unit.also { + Timber.d("Failed to get backup last version") } + val recoveryKey = computeRecoveryKey(secret.fromBase64()) + if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) { + awaitCallback { + trustKeysBackupVersion(keysBackupVersion, true, it) } + // we don't want to start immediately downloading all as it can take very long + +// val importResult = awaitCallback { +// restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it) +// } + withContext(coroutineDispatchers.crypto) { + cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version) + } + Timber.i("onSecretKeyGossip: saved valid backup key") + } else { + Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}") } } catch (failure: Throwable) { Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}") @@ -679,9 +673,9 @@ internal class DefaultKeysBackupService @Inject constructor( callback: MatrixCallback) { Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}") - cryptoCoroutineScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.io) { runCatching { - val decryption = withContext(coroutineDispatchers.crypto) { + val decryption = withContext(coroutineDispatchers.computation) { // Check if the recovery is valid before going any further if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) { Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version") @@ -754,7 +748,19 @@ internal class DefaultKeysBackupService @Inject constructor( } result } - }.foldToCallback(callback) + }.foldToCallback(object : MatrixCallback { + override fun onSuccess(data: ImportRoomKeysResult) { + uiHandler.post { + callback.onSuccess(data) + } + } + + override fun onFailure(failure: Throwable) { + uiHandler.post { + callback.onFailure(failure) + } + } + }) } } @@ -766,7 +772,7 @@ internal class DefaultKeysBackupService @Inject constructor( callback: MatrixCallback) { Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}") - cryptoCoroutineScope.launch(coroutineDispatchers.main) { + cryptoCoroutineScope.launch(coroutineDispatchers.io) { runCatching { val progressListener = if (stepProgressListener != null) { object : ProgressListener { @@ -791,7 +797,19 @@ internal class DefaultKeysBackupService @Inject constructor( restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, it) } } - }.foldToCallback(callback) + }.foldToCallback(object : MatrixCallback { + override fun onSuccess(data: ImportRoomKeysResult) { + uiHandler.post { + callback.onSuccess(data) + } + } + + override fun onFailure(failure: Throwable) { + uiHandler.post { + callback.onFailure(failure) + } + } + }) } } @@ -817,12 +835,16 @@ internal class DefaultKeysBackupService @Inject constructor( ) } else if (roomId != null) { // Get all keys for the room - val data = getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version)) + val data = withContext(coroutineDispatchers.io) { + getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version)) + } // Convert to KeysBackupData KeysBackupData(mutableMapOf(roomId to data)) } else { // Get all keys - getSessionsDataTask.execute(GetSessionsDataTask.Params(version)) + withContext(coroutineDispatchers.io) { + getSessionsDataTask.execute(GetSessionsDataTask.Params(version)) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt index dc50afe67d..1ded9c6c7e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt @@ -40,7 +40,7 @@ import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec import org.matrix.android.sdk.api.session.securestorage.SsssPassphrase import org.matrix.android.sdk.api.util.fromBase64 import org.matrix.android.sdk.api.util.toBase64NoPadding -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.keysbackup.generatePrivateKeyWithPassword import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256 import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption @@ -57,7 +57,7 @@ import kotlin.experimental.and internal class DefaultSharedSecretStorageService @Inject constructor( @UserId private val userId: String, private val accountDataService: SessionAccountDataService, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, + private val secretShareManager: SecretShareManager, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope ) : SharedSecretStorageService { @@ -380,10 +380,7 @@ 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)) - ) + override suspend fun requestSecret(name: String, myOtherDeviceId: String) { + secretShareManager.requestSecretTo(myOtherDeviceId, name) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt index 8bedb78808..d720777ae1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -19,23 +19,22 @@ package org.matrix.android.sdk.internal.crypto.store import androidx.lifecycle.LiveData import androidx.paging.PagedList import org.matrix.android.sdk.api.session.crypto.NewSessionListener +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest +import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody +import org.matrix.android.sdk.api.session.crypto.model.TrailType import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent +import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon -import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper @@ -82,6 +81,15 @@ internal interface IMXCryptoStore { */ fun setGlobalBlacklistUnverifiedDevices(block: Boolean) + /** + * Enable or disable key gossiping. + * Default is true. + * If set to false this device won't send key_request nor will accept key forwarded + */ + fun enableKeyGossiping(enable: Boolean) + + fun isKeyGossipingEnabled(): Boolean + /** * Provides the rooms ids list in which the messages are not encrypted for the unverified devices. * @@ -125,18 +133,6 @@ internal interface IMXCryptoStore { */ fun getDeviceTrackingStatuses(): Map - /** - * @return the pending IncomingRoomKeyRequest requests - */ - fun getPendingIncomingRoomKeyRequests(): List - - fun getPendingIncomingGossipingRequests(): List - - fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) - - fun storeIncomingGossipingRequests(requests: List) -// fun getPendingIncomingSecretShareRequests(): List - /** * Indicate if the store contains data for the passed account. * @@ -377,7 +373,9 @@ internal interface IMXCryptoStore { * @param requestBody the request body * @return an OutgoingRoomKeyRequest instance or null */ - fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? + fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest? + fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest? + fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List /** * Look for an existing outgoing room key request, and if none is found, add a new one. @@ -385,39 +383,59 @@ internal interface IMXCryptoStore { * @param request the request * @return either the same instance as passed in, or the existing one. */ - fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>): OutgoingRoomKeyRequest? + fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>, fromIndex: Int): OutgoingKeyRequest + fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) + fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) + fun updateOutgoingRoomKeyReply( + roomId: String, + sessionId: String, + algorithm: String, + senderKey: String, + fromDevice: String?, + event: Event) - fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map>): OutgoingSecretRequest? + fun deleteOutgoingRoomKeyRequest(requestId: String) + fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) - fun saveGossipingEvent(event: Event) = saveGossipingEvents(listOf(event)) + fun saveIncomingKeyRequestAuditTrail( + requestId: String, + roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + fromUser: String, + fromDevice: String + ) - fun saveGossipingEvents(events: List) + fun saveWithheldAuditTrail( + roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + code: WithHeldCode, + userId: String, + deviceId: String + ) - fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) { - updateGossipingRequestState( - requestUserId = request.userId, - requestDeviceId = request.deviceId, - requestId = request.requestId, - state = state - ) - } + fun saveForwardKeyAuditTrail( + roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + userId: String, + deviceId: String, + chainIndex: Long? + ) - fun updateGossipingRequestState(requestUserId: String?, - requestDeviceId: String?, - requestId: String?, - state: GossipingRequestState) - - /** - * Search an IncomingRoomKeyRequest - * - * @param userId the user id - * @param deviceId the device id - * @param requestId the request id - * @return an IncomingRoomKeyRequest if it exists, else null - */ - fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? - - fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) + fun saveIncomingForwardKeyAuditTrail( + roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + userId: String, + deviceId: String, + chainIndex: Long? + ) fun addNewSessionListener(listener: NewSessionListener) @@ -477,17 +495,15 @@ internal interface IMXCryptoStore { fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap // Dev tools - fun getOutgoingRoomKeyRequests(): List - fun getOutgoingRoomKeyRequestsPaged(): LiveData> - fun getOutgoingSecretKeyRequests(): List - fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? - fun getIncomingRoomKeyRequests(): List - fun getIncomingRoomKeyRequestsPaged(): LiveData> - fun getGossipingEventsTrail(): LiveData> - fun getGossipingEvents(): List + fun getOutgoingRoomKeyRequests(): List + fun getOutgoingRoomKeyRequestsPaged(): LiveData> + fun getGossipingEventsTrail(): LiveData> + fun getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData> + fun getGossipingEvents(): List fun setDeviceKeysUploaded(uploaded: Boolean) fun areDeviceKeysUploaded(): Boolean fun tidyUpDataBase() fun logDbUsageInfo() + fun getOutgoingRoomKeyRequests(inStates: Set): List } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index a509315e7a..d5750a2e2a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -24,37 +24,40 @@ import com.zhuinden.monarchy.Monarchy import io.realm.Realm import io.realm.RealmConfiguration import io.realm.Sort +import io.realm.kotlin.createObject import io.realm.kotlin.where import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.NewSessionListener +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest +import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest +import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo +import org.matrix.android.sdk.api.session.crypto.model.IncomingKeyRequestInfo import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody +import org.matrix.android.sdk.api.session.crypto.model.TrailType +import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent -import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional -import org.matrix.android.sdk.internal.crypto.GossipRequestType -import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon -import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailMapper import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper @@ -63,10 +66,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity @@ -74,8 +73,8 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSess import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity @@ -89,7 +88,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.query.get import org.matrix.android.sdk.internal.crypto.store.db.query.getById import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper -import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.tools.RealmDebugTools import org.matrix.android.sdk.internal.di.CryptoDatabase import org.matrix.android.sdk.internal.di.DeviceId @@ -106,6 +104,8 @@ import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import javax.inject.Inject +private val loggerTag = LoggerTag("RealmCryptoStore", LoggerTag.CRYPTO) + @SessionScope internal class RealmCryptoStore @Inject constructor( @CryptoDatabase private val realmConfiguration: RealmConfiguration, @@ -273,12 +273,13 @@ internal class RealmCryptoStore @Inject constructor( } override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(DeviceInfoEntityFields.IDENTITY_KEY, identityKey) - .findFirst() - ?.let { deviceInfo -> - CryptoMapper.mapToModel(deviceInfo) + return doWithRealm(realmConfiguration) { realm -> + realm.where() + .contains(DeviceInfoEntityFields.KEYS_MAP_JSON, identityKey) + .findAll() + .mapNotNull { CryptoMapper.mapToModel(it) } + .firstOrNull { + it.identityKey() == identityKey } } } @@ -743,14 +744,23 @@ internal class RealmCryptoStore @Inject constructor( if (sessionIdentifier != null) { val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey) - val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { - primaryKey = key - sessionId = sessionIdentifier - senderKey = session.senderKey - putInboundGroupSession(session) - } + val existing = realm.where() + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findFirst() - realm.insertOrUpdate(realmOlmInboundGroupSession) + if (existing != null) { + // we want to keep the existing backup status + existing.putInboundGroupSession(session) + } else { + val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { + primaryKey = key + sessionId = sessionIdentifier + senderKey = session.senderKey + putInboundGroupSession(session) + } + + realm.insertOrUpdate(realmOlmInboundGroupSession) + } } } } @@ -879,18 +889,33 @@ internal class RealmCryptoStore @Inject constructor( return } - doRealmTransaction(realmConfiguration) { + doRealmTransaction(realmConfiguration) { realm -> olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> try { + val sessionIdentifier = olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier() val key = OlmInboundGroupSessionEntity.createPrimaryKey( - olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier(), + sessionIdentifier, olmInboundGroupSessionWrapper.senderKey ) - it.where() + val existing = realm.where() .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) .findFirst() - ?.backedUp = true + + if (existing != null) { + existing.backedUp = true + } else { + // ... might be in cache but not yet persisted, create a record to persist backedup state + val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { + primaryKey = key + sessionId = sessionIdentifier + senderKey = olmInboundGroupSessionWrapper.senderKey + putInboundGroupSession(olmInboundGroupSessionWrapper) + backedUp = true + } + + realm.insertOrUpdate(realmOlmInboundGroupSession) + } } catch (e: OlmException) { Timber.e(e, "OlmException") } @@ -929,6 +954,18 @@ internal class RealmCryptoStore @Inject constructor( } } + override fun enableKeyGossiping(enable: Boolean) { + doRealmTransaction(realmConfiguration) { + it.where().findFirst()?.globalEnableKeyGossiping = enable + } + } + + override fun isKeyGossipingEnabled(): Boolean { + return doWithRealm(realmConfiguration) { + it.where().findFirst()?.globalEnableKeyGossiping + } ?: true + } + override fun getGlobalBlacklistUnverifiedDevices(): Boolean { return doWithRealm(realmConfiguration) { it.where().findFirst()?.globalBlacklistUnverifiedDevices @@ -1010,12 +1047,13 @@ internal class RealmCryptoStore @Inject constructor( ?: defaultValue } - override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? { + override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest? { return monarchy.fetchAllCopiedSync { realm -> - realm.where() - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) - }.mapNotNull { - it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId) + .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId) + }.map { + it.toOutgoingKeyRequest() }.firstOrNull { it.requestBody?.algorithm == requestBody.algorithm && it.requestBody?.roomId == requestBody.roomId && @@ -1024,41 +1062,37 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? { + override fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest? { return monarchy.fetchAllCopiedSync { realm -> - realm.where() - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) - .equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName) - }.mapNotNull { - it.toOutgoingGossipingRequest() as? OutgoingSecretRequest + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) + }.map { + it.toOutgoingKeyRequest() }.firstOrNull() } - override fun getIncomingRoomKeyRequests(): List { + override fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List { + // TODO this annoying we have to load all return monarchy.fetchAllCopiedSync { realm -> - realm.where() - .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) - }.mapNotNull { - it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId) + .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId) + }.map { + it.toOutgoingKeyRequest() + }.filter { + it.requestBody?.algorithm == algorithm && + it.requestBody?.senderKey == senderKey } } - override fun getIncomingRoomKeyRequestsPaged(): LiveData> { + override fun getGossipingEventsTrail(): LiveData> { val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> - realm.where() - .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) - .sort(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Sort.DESCENDING) + realm.where().sort(AuditTrailEntityFields.AGE_LOCAL_TS, Sort.DESCENDING) } val dataSourceFactory = realmDataSourceFactory.map { - it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest - ?: IncomingRoomKeyRequest( - requestBody = null, - deviceId = "", - userId = "", - requestId = "", - state = GossipingRequestState.NONE, - localCreationTimestamp = 0 - ) + AuditTrailMapper.map(it) + // mm we can't map not null... + ?: createUnknownTrail() } return monarchy.findAllPagedWithChanges( realmDataSourceFactory, @@ -1073,12 +1107,33 @@ internal class RealmCryptoStore @Inject constructor( ) } - override fun getGossipingEventsTrail(): LiveData> { + private fun createUnknownTrail() = AuditTrail( + clock.epochMillis(), + TrailType.Unknown, + IncomingKeyRequestInfo( + "", + "", + "", + "", + "", + "", + "", + ) + ) + + override fun getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData> { val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> - realm.where().sort(GossipingEventEntityFields.AGE_LOCAL_TS, Sort.DESCENDING) + realm.where() + .equalTo(AuditTrailEntityFields.TYPE, type.name) + .sort(AuditTrailEntityFields.AGE_LOCAL_TS, Sort.DESCENDING) } - val dataSourceFactory = realmDataSourceFactory.map { it.toModel() } - val trail = monarchy.findAllPagedWithChanges( + val dataSourceFactory = realmDataSourceFactory.map { entity -> + (AuditTrailMapper.map(entity) + // mm we can't map not null... + ?: createUnknownTrail() + ).let { mapper.invoke(it) } + } + return monarchy.findAllPagedWithChanges( realmDataSourceFactory, LivePagedListBuilder( dataSourceFactory, @@ -1089,28 +1144,36 @@ internal class RealmCryptoStore @Inject constructor( .build() ) ) - return trail } - override fun getGossipingEvents(): List { + override fun getGossipingEvents(): List { return monarchy.fetchAllCopiedSync { realm -> - realm.where() - }.map { - it.toModel() + realm.where() + }.mapNotNull { + AuditTrailMapper.map(it) } } - override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map>): OutgoingRoomKeyRequest? { + override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, + recipients: Map>, + fromIndex: Int): OutgoingKeyRequest { // Insert the request and return the one passed in parameter - var request: OutgoingRoomKeyRequest? = null + lateinit var request: OutgoingKeyRequest doRealmTransaction(realmConfiguration) { realm -> - val existing = realm.where() - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + val existing = realm.where() + .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId) + .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId) .findAll() - .mapNotNull { - it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest - }.firstOrNull { + .map { + it.toOutgoingKeyRequest() + }.also { + if (it.size > 1) { + // there should be one or zero but not more, worth warning + Timber.tag(loggerTag.value).w("There should not be more than one active key request per session") + } + } + .firstOrNull { it.requestBody?.algorithm == requestBody.algorithm && it.requestBody?.sessionId == requestBody.sessionId && it.requestBody?.senderKey == requestBody.senderKey && @@ -1118,13 +1181,14 @@ internal class RealmCryptoStore @Inject constructor( } if (existing == null) { - request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply { + request = realm.createObject(OutgoingKeyRequestEntity::class.java).apply { this.requestId = RequestIdHelper.createUniqueRequestId() this.setRecipients(recipients) - this.requestState = OutgoingGossipingRequestState.UNSENT - this.type = GossipRequestType.KEY - this.requestedInfoStr = requestBody.toJson() - }.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + this.requestedIndex = fromIndex + this.requestState = OutgoingRoomKeyRequestState.UNSENT + this.setRequestBody(requestBody) + this.creationTimeStamp = clock.epochMillis() + }.toOutgoingKeyRequest() } else { request = existing } @@ -1132,284 +1196,175 @@ internal class RealmCryptoStore @Inject constructor( return request } - override fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map>): OutgoingSecretRequest? { - var request: OutgoingSecretRequest? = null - - // Insert the request and return the one passed in parameter + override fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) { 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 = RequestIdHelper.createUniqueRequestId() - this.requestedInfoStr = secretName - }.toOutgoingGossipingRequest() as? OutgoingSecretRequest - } else { - request = existing - } - } - - return request - } - - override fun saveGossipingEvents(events: List) { - monarchy.writeAsync { realm -> - val now = clock.epochMillis() - events.forEach { event -> - 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 - } - realm.insertOrUpdate(entity) - } - } - } - -// 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(requestUserId: String?, - requestDeviceId: String?, - requestId: String?, - state: GossipingRequestState) { - doRealmTransaction(realmConfiguration) { realm -> - realm.where() - .equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, requestUserId) - .equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, requestDeviceId) - .equalTo(IncomingGossipingRequestEntityFields.REQUEST_ID, requestId) - .findAll().forEach { - it.requestState = state - } - } - } - - 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 doWithRealm(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(): List { - return doWithRealm(realmConfiguration) { - it.where() - .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) - .equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name) - .findAll() - .map { entity -> - IncomingRoomKeyRequest( - userId = entity.otherUserId, - deviceId = entity.otherDeviceId, - requestId = entity.requestId, - requestBody = entity.getRequestedKeyInfo(), - localCreationTimestamp = entity.localCreationTimestamp - ) - } - } - } - - override fun getPendingIncomingGossipingRequests(): List { - return doWithRealm(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 - ) - } + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) + .findFirst()?.apply { + this.requestState = newState + if (newState == OutgoingRoomKeyRequestState.UNSENT) { + // clear the old replies + this.replies.deleteAllFromRealm() } } } } - 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 ?: clock.epochMillis() - 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 storeIncomingGossipingRequests(requests: List) { - doRealmTransactionAsync(realmConfiguration) { realm -> - requests.forEach { request -> - // 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 = request.localCreationTimestamp ?: clock.epochMillis() - 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 updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) + .findFirst()?.apply { + this.requestedIndex = newIndex } + } + } + + override fun updateOutgoingRoomKeyReply(roomId: String, + sessionId: String, + algorithm: String, + senderKey: String, + fromDevice: String?, + event: Event) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId) + .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId) + .findAll().firstOrNull { entity -> + entity.toOutgoingKeyRequest().let { + it.requestBody?.senderKey == senderKey && + it.requestBody?.algorithm == algorithm + } + }?.apply { + event.senderId?.let { addReply(it, fromDevice, event) } + } + } + } + + override fun deleteOutgoingRoomKeyRequest(requestId: String) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) + .findFirst()?.deleteOnCascade() + } + } + + override fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) { + doRealmTransaction(realmConfiguration) { realm -> + realm.where() + .equalTo(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, state.name) + .findAll() + // I delete like this because I want to cascade delete replies? + .onEach { it.deleteOnCascade() } + } + } + + override fun saveIncomingKeyRequestAuditTrail( + requestId: String, + roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + fromUser: String, + fromDevice: String) { + monarchy.writeAsync { realm -> + val now = clock.epochMillis() + realm.createObject().apply { + this.ageLocalTs = now + this.type = TrailType.IncomingKeyRequest.name + val info = IncomingKeyRequestInfo( + roomId = roomId, + sessionId = sessionId, + senderKey = senderKey, + alg = algorithm, + userId = fromUser, + deviceId = fromDevice, + requestId = requestId + ) + MoshiProvider.providesMoshi().adapter(IncomingKeyRequestInfo::class.java).toJson(info)?.let { + this.contentJson = it } } } } -// override fun getPendingIncomingSecretShareRequests(): List { -// return doRealmQueryAndCopyList(realmConfiguration) { -// it.where() -// .findAll() -// }.map { -// it.toIncomingSecretShareRequest() -// } -// } + override fun saveWithheldAuditTrail(roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + code: WithHeldCode, + userId: String, + deviceId: String) { + monarchy.writeAsync { realm -> + val now = clock.epochMillis() + realm.createObject().apply { + this.ageLocalTs = now + this.type = TrailType.OutgoingKeyWithheld.name + val info = WithheldInfo( + roomId = roomId, + sessionId = sessionId, + senderKey = senderKey, + alg = algorithm, + code = code, + userId = userId, + deviceId = deviceId + ) + MoshiProvider.providesMoshi().adapter(WithheldInfo::class.java).toJson(info)?.let { + this.contentJson = it + } + } + } + } + + override fun saveForwardKeyAuditTrail(roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + userId: String, + deviceId: String, + chainIndex: Long?) { + saveForwardKeyTrail(roomId, sessionId, senderKey, algorithm, userId, deviceId, chainIndex, false) + } + + override fun saveIncomingForwardKeyAuditTrail(roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + userId: String, + deviceId: String, + chainIndex: Long?) { + saveForwardKeyTrail(roomId, sessionId, senderKey, algorithm, userId, deviceId, chainIndex, true) + } + + private fun saveForwardKeyTrail(roomId: String, + sessionId: String, + senderKey: String, + algorithm: String, + userId: String, + deviceId: String, + chainIndex: Long?, + incoming: Boolean + ) { + monarchy.writeAsync { realm -> + val now = clock.epochMillis() + realm.createObject().apply { + this.ageLocalTs = now + this.type = if (incoming) TrailType.IncomingKeyForward.name else TrailType.OutgoingKeyForward.name + val info = ForwardInfo( + roomId = roomId, + sessionId = sessionId, + senderKey = senderKey, + alg = algorithm, + userId = userId, + deviceId = deviceId, + chainIndex = chainIndex + ) + MoshiProvider.providesMoshi().adapter(ForwardInfo::class.java).toJson(info)?.let { + this.contentJson = it + } + } + } + } /* ========================================================================================== * Cross Signing @@ -1513,37 +1468,34 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun getOutgoingRoomKeyRequests(): List { + override fun getOutgoingRoomKeyRequests(): List { return monarchy.fetchAllMappedSync({ realm -> realm - .where(OutgoingGossipingRequestEntity::class.java) - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .where(OutgoingKeyRequestEntity::class.java) }, { entity -> - entity.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + entity.toOutgoingKeyRequest() }) .filterNotNull() } - override fun getOutgoingSecretKeyRequests(): List { + override fun getOutgoingRoomKeyRequests(inStates: Set): List { return monarchy.fetchAllMappedSync({ realm -> realm - .where(OutgoingGossipingRequestEntity::class.java) - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) + .where(OutgoingKeyRequestEntity::class.java) + .`in`(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, inStates.map { it.name }.toTypedArray()) }, { entity -> - entity.toOutgoingGossipingRequest() as? OutgoingSecretRequest + entity.toOutgoingKeyRequest() }) .filterNotNull() } - override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { + override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> realm - .where(OutgoingGossipingRequestEntity::class.java) - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .where(OutgoingKeyRequestEntity::class.java) } val dataSourceFactory = realmDataSourceFactory.map { - it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest - ?: OutgoingRoomKeyRequest(requestBody = null, requestId = "?", recipients = emptyMap(), state = OutgoingGossipingRequestState.CANCELLED) + it.toOutgoingKeyRequest() } val trail = monarchy.findAllPagedWithChanges( realmDataSourceFactory, @@ -1722,26 +1674,20 @@ internal class RealmCryptoStore @Inject constructor( val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000 doRealmTransaction(realmConfiguration) { realm -> - // Only keep one week history - realm.where() - .lessThan(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, prevWeekTs) + // Clean the old ones? + realm.where() + .lessThan(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, prevWeekTs) .findAll() - .also { Timber.i("## Crypto Clean up ${it.size} IncomingGossipingRequestEntity") } + .also { Timber.i("## Crypto Clean up ${it.size} OutgoingKeyRequestEntity") } .deleteAllFromRealm() - // Clean the cancelled ones? - realm.where() - .equalTo(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, OutgoingGossipingRequestState.CANCELLED.name) - .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) - .findAll() - .also { Timber.i("## Crypto Clean up ${it.size} OutgoingGossipingRequestEntity") } - .deleteAllFromRealm() + // Only keep one month history - // Only keep one week history - realm.where() - .lessThan(GossipingEventEntityFields.AGE_LOCAL_TS, prevWeekTs) + val prevMonthTs = clock.epochMillis() - 4 * 7 * 24 * 60 * 60 * 1_000L + realm.where() + .lessThan(AuditTrailEntityFields.AGE_LOCAL_TS, prevMonthTs) .findAll() - .also { Timber.i("## Crypto Clean up ${it.size} GossipingEventEntityFields") } + .also { Timber.i("## Crypto Clean up ${it.size} AuditTrailEntity") } .deleteAllFromRealm() // Can we do something for WithHeldSessionEntity? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt index 32f24c5d2e..d621450ac3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo013 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015 +import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016 import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject @@ -50,7 +51,7 @@ internal class RealmCryptoStoreMigration @Inject constructor( // 0, 1, 2: legacy Riot-Android // 3: migrate to RiotX schema // 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6) - val schemaVersion = 15L + val schemaVersion = 16L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion") @@ -70,5 +71,6 @@ internal class RealmCryptoStoreMigration @Inject constructor( if (oldVersion < 13) MigrateCryptoTo013(realm).perform() if (oldVersion < 14) MigrateCryptoTo014(realm).perform() if (oldVersion < 15) MigrateCryptoTo015(realm).perform() + if (oldVersion < 16) MigrateCryptoTo016(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt index b4c94a7fd1..b43030e343 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreModule.kt @@ -17,19 +17,19 @@ package org.matrix.android.sdk.internal.crypto.store.db import io.realm.annotations.RealmModule +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.KeyRequestReplyEntity import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity -import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity @@ -51,9 +51,9 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEnti KeyInfoEntity::class, CrossSigningInfoEntity::class, TrustLevelEntity::class, - GossipingEventEntity::class, - IncomingGossipingRequestEntity::class, - OutgoingGossipingRequestEntity::class, + AuditTrailEntity::class, + OutgoingKeyRequestEntity::class, + KeyRequestReplyEntity::class, MyDeviceLastSeenInfoEntity::class, WithHeldSessionEntity::class, SharedSessionEntity::class, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt index e1d7598767..8ec2932a8f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt @@ -17,9 +17,6 @@ package org.matrix.android.sdk.internal.crypto.store.db.migration import io.realm.DynamicRealm -import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields -import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields import org.matrix.android.sdk.internal.util.database.RealmMigrator internal class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) { @@ -29,38 +26,37 @@ internal class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) 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) + .addField("type", String::class.java) + .addIndex("type") + .addField("content", String::class.java) + .addField("sender", String::class.java) + .addIndex("sender") + .addField("decryptionResultJson", String::class.java) + .addField("decryptionErrorCode", String::class.java) + .addField("ageLocalTs", Long::class.java) + .setNullable("ageLocalTs", true) + .addField("sendStateStr", 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) + .addField("requestId", String::class.java) + .addIndex("requestId") + .addField("typeStr", String::class.java) + .addIndex("typeStr") + .addField("otherUserId", String::class.java) + .addField("requestedInfoStr", String::class.java) + .addField("otherDeviceId", String::class.java) + .addField("requestStateStr", String::class.java) + .addField("localCreationTimestamp", Long::class.java) + .setNullable("localCreationTimestamp", 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) + .addField("requestId", String::class.java) + .addIndex("requestId") + .addField("recipientsData", String::class.java) + .addField("requestedInfoStr", String::class.java) + .addField("typeStr", String::class.java) + .addIndex("typeStr") + .addField("requestStateStr", String::class.java) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo016.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo016.kt new file mode 100644 index 0000000000..5a14ebf85a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo016.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.crypto.store.db.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.KeyRequestReplyEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateCryptoTo016(realm: DynamicRealm) : RealmMigrator(realm, 16) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.remove("OutgoingGossipingRequestEntity") + realm.schema.remove("IncomingGossipingRequestEntity") + realm.schema.remove("GossipingEventEntity") + + // No need to migrate existing request, just start fresh + + val replySchema = realm.schema.create("KeyRequestReplyEntity") + .addField(KeyRequestReplyEntityFields.SENDER_ID, String::class.java) + .addField(KeyRequestReplyEntityFields.FROM_DEVICE, String::class.java) + .addField(KeyRequestReplyEntityFields.EVENT_JSON, String::class.java) + + realm.schema.create("OutgoingKeyRequestEntity") + .addField(OutgoingKeyRequestEntityFields.REQUEST_ID, String::class.java) + .addIndex(OutgoingKeyRequestEntityFields.REQUEST_ID) + .addField(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, String::class.java) + .addIndex(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID) + .addRealmListField(OutgoingKeyRequestEntityFields.REPLIES.`$`, replySchema) + .addField(OutgoingKeyRequestEntityFields.RECIPIENTS_DATA, String::class.java) + .addField(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, String::class.java) + .addIndex(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR) + .addField(OutgoingKeyRequestEntityFields.REQUESTED_INFO_STR, String::class.java) + .addField(OutgoingKeyRequestEntityFields.ROOM_ID, String::class.java) + .addIndex(OutgoingKeyRequestEntityFields.ROOM_ID) + .addField(OutgoingKeyRequestEntityFields.REQUESTED_INDEX, Integer::class.java) + .addField(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, Long::class.java) + .setNullable(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, true) + + realm.schema.create("AuditTrailEntity") + .addField(AuditTrailEntityFields.AGE_LOCAL_TS, Long::class.java) + .setNullable(AuditTrailEntityFields.AGE_LOCAL_TS, true) + .addField(AuditTrailEntityFields.CONTENT_JSON, String::class.java) + .addField(AuditTrailEntityFields.TYPE, String::class.java) + .addIndex(AuditTrailEntityFields.TYPE) + + realm.schema.get("CryptoMetadataEntity") + ?.addField(CryptoMetadataEntityFields.GLOBAL_ENABLE_KEY_GOSSIPING, Boolean::class.java) + ?.transform { + // set the default value to true + it.setBoolean(CryptoMetadataEntityFields.GLOBAL_ENABLE_KEY_GOSSIPING, true) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/GossipingRequestState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/AuditTrailEntity.kt similarity index 60% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/GossipingRequestState.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/AuditTrailEntity.kt index d9a6f4fcba..2e0e9c8c8b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/GossipingRequestState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/AuditTrailEntity.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,18 +14,15 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.session.crypto.model +package org.matrix.android.sdk.internal.crypto.store.db.model -enum class GossipingRequestState { - NONE, - PENDING, - REJECTED, - ACCEPTING, - ACCEPTED, - FAILED_TO_ACCEPTED, +import io.realm.RealmObject +import io.realm.annotations.Index - // USER_REJECTED, - UNABLE_TO_PROCESS, - CANCELLED_BY_REQUESTER, - RE_REQUESTED +internal open class AuditTrailEntity( + var ageLocalTs: Long? = null, + @Index var type: String? = null, + var contentJson: String? = null +) : RealmObject() { + companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/AuditTrailMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/AuditTrailMapper.kt new file mode 100644 index 0000000000..80ae4a8d0d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/AuditTrailMapper.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.crypto.store.db.model + +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail +import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo +import org.matrix.android.sdk.api.session.crypto.model.IncomingKeyRequestInfo +import org.matrix.android.sdk.api.session.crypto.model.TrailType +import org.matrix.android.sdk.api.session.crypto.model.UnknownInfo +import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo +import org.matrix.android.sdk.internal.di.MoshiProvider + +internal object AuditTrailMapper { + + fun map(entity: AuditTrailEntity): AuditTrail? { + val contentJson = entity.contentJson ?: return null + return when (entity.type) { + TrailType.OutgoingKeyForward.name -> { + val info = tryOrNull { + MoshiProvider.providesMoshi().adapter(ForwardInfo::class.java).fromJson(contentJson) + } ?: return null + AuditTrail( + ageLocalTs = entity.ageLocalTs ?: 0, + type = TrailType.OutgoingKeyForward, + info = info + ) + } + TrailType.OutgoingKeyWithheld.name -> { + val info = tryOrNull { + MoshiProvider.providesMoshi().adapter(WithheldInfo::class.java).fromJson(contentJson) + } ?: return null + AuditTrail( + ageLocalTs = entity.ageLocalTs ?: 0, + type = TrailType.OutgoingKeyWithheld, + info = info + ) + } + TrailType.IncomingKeyRequest.name -> { + val info = tryOrNull { + MoshiProvider.providesMoshi().adapter(IncomingKeyRequestInfo::class.java).fromJson(contentJson) + } ?: return null + AuditTrail( + ageLocalTs = entity.ageLocalTs ?: 0, + type = TrailType.IncomingKeyRequest, + info = info + ) + } + TrailType.IncomingKeyForward.name -> { + val info = tryOrNull { + MoshiProvider.providesMoshi().adapter(ForwardInfo::class.java).fromJson(contentJson) + } ?: return null + AuditTrail( + ageLocalTs = entity.ageLocalTs ?: 0, + type = TrailType.IncomingKeyForward, + info = info + ) + } + else -> { + AuditTrail( + ageLocalTs = entity.ageLocalTs ?: 0, + type = TrailType.Unknown, + info = UnknownInfo + ) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt index 35ae86db8b..63ed0e537e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt @@ -33,6 +33,8 @@ internal open class CryptoMetadataEntity( var deviceSyncToken: String? = null, // Settings for blacklisting unverified devices. var globalBlacklistUnverifiedDevices: Boolean = false, + // setting to enable or disable key gossiping + var globalEnableKeyGossiping: Boolean = true, // The keys backup version currently used. Null means no backup. var backupVersion: String? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt deleted file mode 100644 index a024e092b7..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.crypto.store.db.model - -import com.squareup.moshi.JsonDataException -import io.realm.RealmObject -import io.realm.annotations.Index -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.room.send.SendState -import org.matrix.android.sdk.internal.database.mapper.ContentMapper -import org.matrix.android.sdk.internal.di.MoshiProvider -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/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt deleted file mode 100644 index f05c8853c8..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.crypto.store.db.model - -import io.realm.RealmObject -import io.realm.annotations.Index -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody -import org.matrix.android.sdk.internal.crypto.GossipRequestType -import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon - -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 tryOrNull { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY - } - set(value) { - typeStr = value.name - } - - private var requestStateStr: String = GossipingRequestState.NONE.name - - var requestState: GossipingRequestState - get() { - return tryOrNull { 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/org/matrix/android/sdk/internal/crypto/store/db/model/KeyRequestReplyEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeyRequestReplyEntity.kt new file mode 100644 index 0000000000..0c7cf79e78 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/KeyRequestReplyEntity.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.crypto.store.db.model + +import io.realm.RealmObject +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.di.MoshiProvider + +internal open class KeyRequestReplyEntity( + var senderId: String? = null, + var fromDevice: String? = null, + var eventJson: String? = null +) : RealmObject() { + companion object + + fun getEvent(): Event? { + return eventJson?.let { + MoshiProvider.providesMoshi().adapter(Event::class.java).fromJson(it) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt deleted file mode 100644 index 0e1278967e..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.crypto.store.db.model - -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Types -import io.realm.RealmObject -import io.realm.annotations.Index -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody -import org.matrix.android.sdk.internal.crypto.GossipRequestType -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest -import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest -import org.matrix.android.sdk.internal.di.MoshiProvider - -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 tryOrNull { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY - } - set(value) { - typeStr = value.name - } - - private var requestStateStr: String = OutgoingGossipingRequestState.UNSENT.name - - var requestState: OutgoingGossipingRequestState - get() { - return tryOrNull { 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().orEmpty(), - requestId = requestId ?: "", - state = requestState - ) - } - GossipRequestType.SECRET -> { - OutgoingSecretRequest( - secretName = getRequestedSecretName(), - recipients = getRecipients().orEmpty(), - 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/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt new file mode 100644 index 0000000000..7a8ba18809 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.crypto.store.db.model + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Types +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.Index +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest +import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState +import org.matrix.android.sdk.api.session.crypto.RequestReply +import org.matrix.android.sdk.api.session.crypto.RequestResult +import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.internal.di.MoshiProvider + +internal open class OutgoingKeyRequestEntity( + @Index var requestId: String? = null, + var requestedIndex: Int? = null, + var recipientsData: String? = null, + var requestedInfoStr: String? = null, + var creationTimeStamp: Long? = null, + // de-normalization for better query (if not have to query all and parse json) + @Index var roomId: String? = null, + @Index var megolmSessionId: String? = null, + + var replies: RealmList = RealmList() +) : RealmObject() { + + @Index private var requestStateStr: String = OutgoingRoomKeyRequestState.UNSENT.name + + companion object { + + private val recipientsDataMapper: JsonAdapter>> = + MoshiProvider + .providesMoshi() + .adapter( + Types.newParameterizedType(Map::class.java, String::class.java, List::class.java) + ) + } + + private fun getRequestedKeyInfo(): RoomKeyRequestBody? = RoomKeyRequestBody.fromJson(requestedInfoStr) + + fun setRequestBody(body: RoomKeyRequestBody) { + requestedInfoStr = body.toJson() + roomId = body.roomId + megolmSessionId = body.sessionId + } + + var requestState: OutgoingRoomKeyRequestState + get() { + return tryOrNull { OutgoingRoomKeyRequestState.valueOf(requestStateStr) } + ?: OutgoingRoomKeyRequestState.UNSENT + } + set(value) { + requestStateStr = value.name + } + + private fun getRecipients(): Map>? { + return this.recipientsData?.let { recipientsDataMapper.fromJson(it) } + } + + fun setRecipients(recipients: Map>) { + this.recipientsData = recipientsDataMapper.toJson(recipients) + } + + fun addReply(userId: String, fromDevice: String?, event: Event) { + val newReply = KeyRequestReplyEntity( + senderId = userId, + fromDevice = fromDevice, + eventJson = MoshiProvider.providesMoshi().adapter(Event::class.java).toJson(event) + ) + replies.add(newReply) + } + + fun toOutgoingKeyRequest(): OutgoingKeyRequest { + return OutgoingKeyRequest( + requestBody = getRequestedKeyInfo(), + recipients = getRecipients().orEmpty(), + requestId = requestId ?: "", + fromIndex = requestedIndex ?: 0, + state = requestState, + results = replies.mapNotNull { entity -> + val userId = entity.senderId ?: return@mapNotNull null + val result = entity.eventJson?.let { + MoshiProvider.providesMoshi().adapter(Event::class.java).fromJson(it) + }?.let { event -> + eventToResult(event) + } ?: return@mapNotNull null + RequestReply( + userId = userId, + fromDevice = entity.fromDevice, + result = result + ) + } + ) + } + + private fun eventToResult(event: Event): RequestResult? { + return when (event.getClearType()) { + EventType.ROOM_KEY_WITHHELD -> { + event.content.toModel()?.code?.let { + RequestResult.Failure(it) + } + } + EventType.FORWARDED_ROOM_KEY -> { + RequestResult.Success((event.content?.get("chain_index") as? Number)?.toInt() ?: 0) + } + else -> null + } + } +} + +internal fun OutgoingKeyRequestEntity.deleteOnCascade() { + replies.deleteAllFromRealm() + deleteFromRealm() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt index 1e395796a9..394c618968 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt @@ -15,6 +15,7 @@ */ package org.matrix.android.sdk.internal.crypto.tasks +import dagger.Lazy import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult @@ -39,7 +40,7 @@ internal interface EncryptEventTask : Task { internal class DefaultEncryptEventTask @Inject constructor( private val localEchoRepository: LocalEchoRepository, - private val cryptoService: CryptoService + private val cryptoService: Lazy ) : EncryptEventTask { override suspend fun execute(params: EncryptEventTask.Params): Event { // don't want to wait for any query @@ -59,7 +60,7 @@ internal class DefaultEncryptEventTask @Inject constructor( // try { // let it throws awaitCallback { - cryptoService.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) + cryptoService.get().encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) }.let { result -> val modifiedContent = HashMap(result.eventContent) params.keepKeys?.forEach { toKeep -> @@ -80,7 +81,7 @@ internal class DefaultEncryptEventTask @Inject constructor( ).toContent(), forwardingCurve25519KeyChain = emptyList(), senderCurve25519Key = result.eventContent["sender_key"] as? String, - claimedEd25519Key = cryptoService.getMyDevice().fingerprint() + claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint() ) } else { null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt index 14674ef258..f1b1663756 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt @@ -15,6 +15,7 @@ */ package org.matrix.android.sdk.internal.crypto.tasks +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.network.GlobalErrorReceiver @@ -46,7 +47,9 @@ internal class DefaultSendEventTask @Inject constructor( params.event.roomId ?.takeIf { params.encrypt } ?.let { roomId -> - loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) + tryOrNull { + loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) + } } val event = handleEncryption(params) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt index 44aa6d9db1..7c52c62d92 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt @@ -47,7 +47,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( localEchoRepository.updateSendState(localId, event.roomId, SendState.SENDING) val response = executeRequest(globalErrorReceiver) { roomAPI.send( - localId, + txId = localId, roomId = event.roomId ?: "", content = event.content, eventType = event.type ?: "" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt index 8ecb68560b..a9d5cf1191 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultIncomingSASDefaultVerificationTransaction.kt @@ -23,8 +23,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerific import org.matrix.android.sdk.api.session.crypto.verification.SasMode import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import timber.log.Timber @@ -35,8 +35,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction( override val deviceId: String?, private val cryptoStore: IMXCryptoStore, crossSigningService: CrossSigningService, - outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - incomingGossipingRequestManager: IncomingGossipingRequestManager, + outgoingKeyRequestManager: OutgoingKeyRequestManager, + secretShareManager: SecretShareManager, deviceFingerprint: String, transactionId: String, otherUserID: String, @@ -47,8 +47,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction( deviceId, cryptoStore, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, deviceFingerprint, transactionId, otherUserID, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt index 4bdf414295..ddb8ed232b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt @@ -20,8 +20,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import timber.log.Timber @@ -32,8 +32,8 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( deviceId: String?, cryptoStore: IMXCryptoStore, crossSigningService: CrossSigningService, - outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - incomingGossipingRequestManager: IncomingGossipingRequestManager, + outgoingKeyRequestManager: OutgoingKeyRequestManager, + secretShareManager: SecretShareManager, deviceFingerprint: String, transactionId: String, otherUserId: String, @@ -44,8 +44,8 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( deviceId, cryptoStore, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, deviceFingerprint, transactionId, otherUserId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt index d62ca5503d..cd503d6ed8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt @@ -59,9 +59,9 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent import org.matrix.android.sdk.api.session.room.model.message.ValidVerificationDone import org.matrix.android.sdk.internal.crypto.DeviceListManager -import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel @@ -95,8 +95,8 @@ 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 incomingGossipingRequestManager: IncomingGossipingRequestManager, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, + private val secretShareManager: SecretShareManager, private val myDeviceInfoHolder: Lazy, private val deviceListManager: DeviceListManager, private val setDeviceVerificationAction: SetDeviceVerificationAction, @@ -551,8 +551,8 @@ internal class DefaultVerificationService @Inject constructor( deviceId, cryptoStore, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, myDeviceInfoHolder.get().myDevice.fingerprint()!!, startReq.transactionId, otherUserId, @@ -771,8 +771,15 @@ internal class DefaultVerificationService @Inject constructor( return } + val roomId = event.roomId + if (roomId == null) { + Timber.e("## SAS Verification missing roomId for event") + // TODO cancel? + return + } + handleReadyReceived(event.senderId, readyReq) { - verificationTransportRoomMessageFactory.createTransport(event.roomId!!, it) + verificationTransportRoomMessageFactory.createTransport(roomId, it) } } @@ -814,21 +821,15 @@ internal class DefaultVerificationService @Inject constructor( getExistingTransaction(userId, doneReq.transactionId) ?: getOldTransaction(userId, doneReq.transactionId) ?.let { vt -> - val otherDeviceId = vt.otherDeviceId + val otherDeviceId = vt.otherDeviceId ?: return@let if (!crossSigningService.canCrossSign()) { - outgoingGossipingRequestManager.sendSecretShareRequest( - MASTER_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")) - ) - outgoingGossipingRequestManager.sendSecretShareRequest( - SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")) - ) - outgoingGossipingRequestManager.sendSecretShareRequest( - USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")) - ) + cryptoCoroutineScope.launch { + secretShareManager.requestSecretTo(otherDeviceId, MASTER_KEY_SSSS_NAME) + secretShareManager.requestSecretTo(otherDeviceId, SELF_SIGNING_KEY_SSSS_NAME) + secretShareManager.requestSecretTo(otherDeviceId, USER_SIGNING_KEY_SSSS_NAME) + secretShareManager.requestSecretTo(otherDeviceId, KEYBACKUP_SECRET_SSSS_NAME) + } } - outgoingGossipingRequestManager.sendSecretShareRequest( - KEYBACKUP_SECRET_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")) - ) } } } @@ -922,8 +923,8 @@ internal class DefaultVerificationService @Inject constructor( otherUserId = senderId, otherDeviceId = readyReq.fromDevice, crossSigningService = crossSigningService, - outgoingGossipingRequestManager = outgoingGossipingRequestManager, - incomingGossipingRequestManager = incomingGossipingRequestManager, + outgoingKeyRequestManager = outgoingKeyRequestManager, + secretShareManager = secretShareManager, cryptoStore = cryptoStore, qrCodeData = qrCodeData, userId = userId, @@ -1124,8 +1125,8 @@ internal class DefaultVerificationService @Inject constructor( deviceId, cryptoStore, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, myDeviceInfoHolder.get().myDevice.fingerprint()!!, txID, otherUserId, @@ -1188,6 +1189,7 @@ internal class DefaultVerificationService @Inject constructor( } .distinct() + requestsForUser.add(verificationRequest) transport.sendVerificationRequest(methodValues, validLocalId, otherUserId, roomId, null) { syncedId, info -> // We need to update with the syncedID updatePendingRequest( @@ -1199,7 +1201,6 @@ internal class DefaultVerificationService @Inject constructor( ) } - requestsForUser.add(verificationRequest) dispatchRequestAdded(verificationRequest) return verificationRequest @@ -1323,8 +1324,8 @@ internal class DefaultVerificationService @Inject constructor( deviceId, cryptoStore, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, myDeviceInfoHolder.get().myDevice.fingerprint()!!, transactionId, otherUserId, @@ -1465,8 +1466,8 @@ internal class DefaultVerificationService @Inject constructor( otherUserId = otherUserId, otherDeviceId = otherDeviceId, crossSigningService = crossSigningService, - outgoingGossipingRequestManager = outgoingGossipingRequestManager, - incomingGossipingRequestManager = incomingGossipingRequestManager, + outgoingKeyRequestManager = outgoingKeyRequestManager, + secretShareManager = secretShareManager, cryptoStore = cryptoStore, qrCodeData = qrCodeData, userId = userId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt index 692ddd0148..1837b22445 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt @@ -20,8 +20,8 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningServic import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState -import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import timber.log.Timber @@ -31,8 +31,8 @@ import timber.log.Timber internal abstract class DefaultVerificationTransaction( private val setDeviceVerificationAction: SetDeviceVerificationAction, private val crossSigningService: CrossSigningService, - private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - private val incomingGossipingRequestManager: IncomingGossipingRequestManager, + private val outgoingKeyRequestManager: OutgoingKeyRequestManager, + private val secretShareManager: SecretShareManager, private val userId: String, override val transactionId: String, override val otherUserId: String, @@ -86,7 +86,7 @@ internal abstract class DefaultVerificationTransaction( } if (otherUserId == userId) { - incomingGossipingRequestManager.onVerificationCompleteForDevice(otherDeviceId!!) + secretShareManager.onVerificationCompleteForDevice(otherDeviceId!!) // If me it's reasonable to sign and upload the device signature // Notice that i might not have the private keys, so may not be able to do it diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt index 00069ba122..4e82a6f406 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt @@ -23,8 +23,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.SasMode import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.extensions.toUnsignedInt @@ -42,8 +42,8 @@ internal abstract class SASDefaultVerificationTransaction( open val deviceId: String?, private val cryptoStore: IMXCryptoStore, crossSigningService: CrossSigningService, - outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - incomingGossipingRequestManager: IncomingGossipingRequestManager, + outgoingKeyRequestManager: OutgoingKeyRequestManager, + secretShareManager: SecretShareManager, private val deviceFingerprint: String, transactionId: String, otherUserId: String, @@ -52,8 +52,8 @@ internal abstract class SASDefaultVerificationTransaction( ) : DefaultVerificationTransaction( setDeviceVerificationAction, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, userId, transactionId, otherUserId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt deleted file mode 100644 index 0a175ae3ca..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.crypto.verification - -import android.content.Context -import androidx.work.Data -import androidx.work.WorkerParameters -import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.failure.shouldBeRetried -import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask -import org.matrix.android.sdk.internal.session.SessionComponent -import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker -import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository -import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker -import org.matrix.android.sdk.internal.worker.SessionWorkerParams -import timber.log.Timber -import javax.inject.Inject - -/** - * Possible previous worker: None - * Possible next worker : None - */ -internal class SendVerificationMessageWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) : - SessionSafeCoroutineWorker(context, params, sessionManager, Params::class.java) { - - @JsonClass(generateAdapter = true) - internal data class Params( - override val sessionId: String, - val eventId: String, - override val lastFailureMessage: String? = null - ) : SessionWorkerParams - - @Inject lateinit var sendVerificationMessageTask: SendVerificationMessageTask - @Inject lateinit var localEchoRepository: LocalEchoRepository - @Inject lateinit var cancelSendTracker: CancelSendTracker - - override fun injectWith(injector: SessionComponent) { - injector.inject(this) - } - - override suspend fun doSafeWork(params: Params): Result { - val localEvent = localEchoRepository.getUpToDateEcho(params.eventId) ?: return buildErrorResult(params, "Event not found") - val localEventId = localEvent.eventId ?: "" - val roomId = localEvent.roomId ?: "" - - if (cancelSendTracker.isCancelRequestedFor(localEventId, roomId)) { - return Result.success() - .also { - cancelSendTracker.markCancelled(localEventId, roomId) - Timber.e("## SendEvent: Event sending has been cancelled $localEventId") - } - } - - return try { - val resultEventId = sendVerificationMessageTask.execute( - SendVerificationMessageTask.Params( - event = localEvent - ) - ) - - Result.success(Data.Builder().putString(localEventId, resultEventId).build()) - } catch (throwable: Throwable) { - if (throwable.shouldBeRetried()) { - Result.retry() - } else { - buildErrorResult(params, throwable.localizedMessage ?: "error") - } - } - } - - override fun buildErrorParams(params: Params, message: String): Params { - return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt index ec4e1aa65c..303b6c5839 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt @@ -15,13 +15,9 @@ */ package org.matrix.android.sdk.internal.crypto.verification -import io.realm.Realm -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent @@ -29,22 +25,18 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent -import org.matrix.android.sdk.internal.crypto.EventDecryptor -import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber import javax.inject.Inject internal class VerificationMessageProcessor @Inject constructor( - private val eventDecryptor: EventDecryptor, - private val clock: Clock, private val verificationService: DefaultVerificationService, @UserId private val userId: String, - @DeviceId private val deviceId: String? -) : EventInsertLiveProcessor { + @DeviceId private val deviceId: String?, + private val clock: Clock, +) { private val transactionsHandledByOtherDevice = ArrayList() @@ -60,40 +52,20 @@ internal class VerificationMessageProcessor @Inject constructor( EventType.ENCRYPTED ) - override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { - if (insertType != EventInsertType.INCREMENTAL_SYNC) { - return false - } - return allowedTypes.contains(eventType) && !LocalEcho.isLocalEchoId(eventId) + fun shouldProcess(eventType: String): Boolean { + return allowedTypes.contains(eventType) } - override suspend fun process(realm: Realm, event: Event) { - Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") + suspend fun process(event: Event) { + Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.getClearType()} 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. - if (!VerificationService.isValidRequest(event.ageLocalTs ?: event.originServerTs, clock.epochMillis())) return Unit.also { - Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated") + if (event.ageLocalTs != null && !VerificationService.isValidRequest(event.ageLocalTs, clock.epochMillis())) return Unit.also { + Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated age:$event.ageLocalTs ms") } - // decrypt if needed? - if (event.isEncrypted() && event.mxDecryptionResult == null) { - // TODO use a global event decryptor? attache to session and that listen to new sessionId? - // for now decrypt sync - try { - val result = eventDecryptor.decryptEvent(event, "") - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - } catch (e: MXCryptoError) { - Timber.e("## SAS Failed to decrypt event: ${event.eventId}") - verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event) - } - } Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") // Relates to is not encrypted @@ -102,7 +74,6 @@ internal class VerificationMessageProcessor @Inject constructor( if (event.senderId == userId) { // If it's send from me, we need to keep track of Requests or Start // done from another device of mine - if (EventType.MESSAGE == event.getClearType()) { val msgType = event.getClearContent().toModel()?.msgType if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { @@ -137,6 +108,8 @@ internal class VerificationMessageProcessor @Inject constructor( transactionsHandledByOtherDevice.remove(it) verificationService.onRoomRequestHandledByOtherDevice(event) } + } else if (EventType.ENCRYPTED == event.getClearType()) { + verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event) } Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt index 325a6f0ba2..e32828af23 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt @@ -15,14 +15,8 @@ */ package org.matrix.android.sdk.internal.crypto.verification -import androidx.lifecycle.Observer -import androidx.work.BackoffPolicy -import androidx.work.Data -import androidx.work.ExistingWorkPolicy -import androidx.work.Operation -import androidx.work.WorkInfo import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest @@ -45,27 +39,28 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS -import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer import org.matrix.android.sdk.internal.util.time.Clock -import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import timber.log.Timber -import java.util.UUID -import java.util.concurrent.TimeUnit +import java.util.concurrent.Executors internal class VerificationTransportRoomMessage( - private val workManagerProvider: WorkManagerProvider, - private val sessionId: String, + private val sendVerificationMessageTask: SendVerificationMessageTask, private val userId: String, private val userDeviceId: String?, private val roomId: String, private val localEchoEventFactory: LocalEchoEventFactory, private val tx: DefaultVerificationTransaction?, - private val coroutineScope: CoroutineScope, + cryptoCoroutineScope: CoroutineScope, private val clock: Clock, ) : VerificationTransport { + private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + private val verificationSenderScope = CoroutineScope(cryptoCoroutineScope.coroutineContext + dispatcher) + private val sequencer = SemaphoreCoroutineSequencer() + override fun sendToOther(type: String, verificationInfo: VerificationInfo, nextState: VerificationTxState, @@ -79,70 +74,22 @@ internal class VerificationTransportRoomMessage( content = verificationInfo.toEventContent()!! ) - val workerParams = WorkerParamsFactory.toData( - SendVerificationMessageWorker.Params( - sessionId = sessionId, - eventId = event.eventId ?: "" - ) - ) - val enqueueInfo = enqueueSendWork(workerParams) - - // I cannot just listen to the given work request, because when used in a uniqueWork, - // The callback is called while it is still Running ... - -// Futures.addCallback(enqueueInfo.first.result, object : FutureCallback { -// override fun onSuccess(result: Operation.State.SUCCESS?) { -// if (onDone != null) { -// onDone() -// } else { -// tx?.state = nextState -// } -// } -// -// override fun onFailure(t: Throwable) { -// Timber.e("## SAS verification [${tx?.transactionId}] failed to send toDevice in state : ${tx?.state}, reason: ${t.localizedMessage}") -// tx?.cancel(onErrorReason) -// } -// }, listenerExecutor) - - val workLiveData = workManagerProvider.workManager - .getWorkInfosForUniqueWorkLiveData(uniqueQueueName()) - - val observer = object : Observer> { - override fun onChanged(workInfoList: List?) { - workInfoList - ?.firstOrNull { it.id == enqueueInfo.second } - ?.let { wInfo -> - when (wInfo.state) { - WorkInfo.State.FAILED -> { - tx?.cancel(onErrorReason) - workLiveData.removeObserver(this) - } - WorkInfo.State.SUCCEEDED -> { - if (SessionSafeCoroutineWorker.hasFailed(wInfo.outputData)) { - Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}") - tx?.cancel(onErrorReason) - } else { - if (onDone != null) { - onDone() - } else { - tx?.state = nextState - } - } - workLiveData.removeObserver(this) - } - else -> { - // nop - } - } - } + verificationSenderScope.launch { + sequencer.post { + try { + val params = SendVerificationMessageTask.Params(event) + sendVerificationMessageTask.executeRetry(params, 5) + // Do I need to update local echo state to sent? + if (onDone != null) { + onDone() + } else { + tx?.state = nextState + } + } catch (failure: Throwable) { + tx?.cancel(onErrorReason) + } } } - - // TODO listen to DB to get synced info - coroutineScope.launch(Dispatchers.Main) { - workLiveData.observeForever(observer) - } } override fun sendVerificationRequest(supportedMethods: List, @@ -173,60 +120,24 @@ internal class VerificationTransportRoomMessage( val content = info.toContent() val event = createEventAndLocalEcho( - localId, - EventType.MESSAGE, - roomId, - content + localId = localId, + type = EventType.MESSAGE, + roomId = roomId, + content = content ) - val workerParams = WorkerParamsFactory.toData( - SendVerificationMessageWorker.Params( - sessionId = sessionId, - eventId = event.eventId ?: "" - ) - ) - - val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) - .setInputData(workerParams) - .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) - .build() - - workManagerProvider.workManager - .beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest) - .enqueue() - - // I cannot just listen to the given work request, because when used in a uniqueWork, - // The callback is called while it is still Running ... - - val workLiveData = workManagerProvider.workManager - .getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork") - - val observer = object : Observer> { - override fun onChanged(workInfoList: List?) { - workInfoList - ?.filter { it.state == WorkInfo.State.SUCCEEDED } - ?.firstOrNull { it.id == workRequest.id } - ?.let { wInfo -> - if (SessionSafeCoroutineWorker.hasFailed(wInfo.outputData)) { - callback(null, null) - } else { - val eventId = wInfo.outputData.getString(localId) - if (eventId != null) { - callback(eventId, validInfo) - } else { - callback(null, null) - } - } - workLiveData.removeObserver(this) - } + verificationSenderScope.launch { + val params = SendVerificationMessageTask.Params(event) + sequencer.post { + try { + val eventId = sendVerificationMessageTask.executeRetry(params, 5) + // Do I need to update local echo state to sent? + callback(eventId, validInfo) + } catch (failure: Throwable) { + callback(null, null) + } } } - - // TODO listen to DB to get synced info - coroutineScope.launch(Dispatchers.Main) { - workLiveData.observeForever(observer) - } } override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String?, code: CancelCode) { @@ -236,13 +147,17 @@ internal class VerificationTransportRoomMessage( roomId = roomId, content = MessageVerificationCancelContent.create(transactionId, code).toContent() ) - val workerParams = WorkerParamsFactory.toData( - SendVerificationMessageWorker.Params( - sessionId = sessionId, - eventId = event.eventId ?: "" - ) - ) - enqueueSendWork(workerParams) + + verificationSenderScope.launch { + sequencer.post { + try { + val params = SendVerificationMessageTask.Params(event) + sendVerificationMessageTask.executeRetry(params, 5) + } catch (failure: Throwable) { + Timber.w(failure, "Failed to cancel verification transaction") + } + } + } } override fun done(transactionId: String, @@ -258,47 +173,21 @@ internal class VerificationTransportRoomMessage( ) ).toContent() ) - val workerParams = WorkerParamsFactory.toData( - SendVerificationMessageWorker.Params( - sessionId = sessionId, - eventId = event.eventId ?: "" - ) - ) - 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) - } + verificationSenderScope.launch { + sequencer.post { + try { + val params = SendVerificationMessageTask.Params(event) + sendVerificationMessageTask.executeRetry(params, 5) + } catch (failure: Throwable) { + Timber.w(failure, "Failed to complete (done) verification") + // should we call onDone? + } finally { + onDone?.invoke() + } } } - - // TODO listen to DB to get synced info - coroutineScope.launch(Dispatchers.Main) { - workLiveData.observeForever(observer) - } } - private fun enqueueSendWork(workerParams: Data): Pair { - val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) - .setInputData(workerParams) - .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS) - .build() - return workManagerProvider.workManager - .beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND_OR_REPLACE, 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/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt index b1b7ad7a98..345948e608 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessageFactory.kt @@ -16,39 +16,35 @@ package org.matrix.android.sdk.internal.crypto.verification +import kotlinx.coroutines.CoroutineScope +import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask import org.matrix.android.sdk.internal.di.DeviceId -import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory -import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject internal class VerificationTransportRoomMessageFactory @Inject constructor( - private val workManagerProvider: WorkManagerProvider, - @SessionId - private val sessionId: String, + private val sendVerificationMessageTask: SendVerificationMessageTask, @UserId private val userId: String, @DeviceId private val deviceId: String?, private val localEchoEventFactory: LocalEchoEventFactory, - private val taskExecutor: TaskExecutor, + private val cryptoCoroutineScope: CoroutineScope, private val clock: Clock, ) { fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage { return VerificationTransportRoomMessage( - workManagerProvider, - sessionId, - userId, - deviceId, - roomId, - localEchoEventFactory, - tx, - taskExecutor.executorScope, - clock + sendVerificationMessageTask = sendVerificationMessageTask, + userId = userId, + userDeviceId = deviceId, + roomId = roomId, + localEchoEventFactory = localEchoEventFactory, + tx = tx, + cryptoCoroutineScope = cryptoCoroutineScope, + clock = clock, ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt index 1793cd123b..b1ad4d87b6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt @@ -22,8 +22,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerification import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.util.fromBase64 -import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager -import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager +import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager +import org.matrix.android.sdk.internal.crypto.SecretShareManager import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64Safe import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore @@ -37,8 +37,8 @@ internal class DefaultQrCodeVerificationTransaction( override val otherUserId: String, override var otherDeviceId: String?, private val crossSigningService: CrossSigningService, - outgoingGossipingRequestManager: OutgoingGossipingRequestManager, - incomingGossipingRequestManager: IncomingGossipingRequestManager, + outgoingKeyRequestManager: OutgoingKeyRequestManager, + secretShareManager: SecretShareManager, private val cryptoStore: IMXCryptoStore, // Not null only if other user is able to scan QR code private val qrCodeData: QrCodeData?, @@ -48,8 +48,8 @@ internal class DefaultQrCodeVerificationTransaction( ) : DefaultVerificationTransaction( setDeviceVerificationAction, crossSigningService, - outgoingGossipingRequestManager, - incomingGossipingRequestManager, + outgoingKeyRequestManager, + secretShareManager, userId, transactionId, otherUserId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index fb9b2da6c1..050480e6c9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -21,12 +21,8 @@ import dagger.Component import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker import org.matrix.android.sdk.internal.crypto.CryptoModule -import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker -import org.matrix.android.sdk.internal.crypto.SendGossipWorker import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker -import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker import org.matrix.android.sdk.internal.di.MatrixComponent import org.matrix.android.sdk.internal.federation.FederationModule import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker @@ -133,14 +129,6 @@ internal interface SessionComponent { fun inject(worker: AddPusherWorker) - fun inject(worker: SendVerificationMessageWorker) - - fun inject(worker: SendGossipRequestWorker) - - fun inject(worker: CancelGossipRequestWorker) - - fun inject(worker: SendGossipWorker) - fun inject(worker: UpdateTrustWorker) @Component.Factory diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index 0aae9f3105..7ceb89e892 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -49,7 +49,6 @@ import org.matrix.android.sdk.api.util.md5 import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask -import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor import org.matrix.android.sdk.internal.database.EventInsertLiveObserver import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory @@ -318,10 +317,6 @@ internal abstract class SessionModule { @IntoSet abstract fun bindRoomCreateEventProcessor(processor: RoomCreateEventProcessor): EventInsertLiveProcessor - @Binds - @IntoSet - abstract fun bindVerificationMessageProcessor(processor: VerificationMessageProcessor): EventInsertLiveProcessor - @Binds @IntoSet abstract fun bindCallEventProcessor(processor: CallEventProcessor): EventInsertLiveProcessor diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt index 6306f3c6fe..4c631c2182 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.membership.joining import io.realm.RealmConfiguration import kotlinx.coroutines.TimeoutCancellationException +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure @@ -53,6 +54,7 @@ internal class DefaultJoinRoomTask @Inject constructor( private val readMarkersTask: SetReadMarkersTask, @SessionDatabase private val realmConfiguration: RealmConfiguration, + private val coroutineDispatcher: MatrixCoroutineDispatchers, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, private val globalErrorReceiver: GlobalErrorReceiver, private val clock: Clock, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 5437a015fd..a3be8b56a1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -382,7 +382,10 @@ internal class RoomSyncHandler @Inject constructor( val roomMemberContentsByUser = HashMap() val optimizedThreadSummaryMap = hashMapOf() - for (event in eventList) { + for (rawEvent in eventList) { + // It's annoying roomId is not there, but lot of code rely on it. + // And had to do it now as copy would delete all decryption results.. + val event = rawEvent.copy(roomId = roomId) if (event.eventId == null || event.senderId == null || event.type == null) { continue } @@ -454,7 +457,7 @@ internal class RoomSyncHandler @Inject constructor( } } // Give info to crypto module - cryptoService.onLiveEvent(roomEntity.roomId, event) + cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync) // Try to remove local echo event.unsignedData?.transactionId?.also { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt index 6fce748091..95b3662c67 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt @@ -22,11 +22,7 @@ import androidx.work.ListenableWorker import androidx.work.WorkerFactory import androidx.work.WorkerParameters import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker -import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker -import org.matrix.android.sdk.internal.crypto.SendGossipWorker import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker -import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker import org.matrix.android.sdk.internal.di.MatrixScope import org.matrix.android.sdk.internal.session.content.UploadContentWorker import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker @@ -56,8 +52,6 @@ internal class MatrixWorkerFactory @Inject constructor(private val sessionManage CheckFactoryWorker(appContext, workerParameters, true) AddPusherWorker::class.java.name -> AddPusherWorker(appContext, workerParameters, sessionManager) - CancelGossipRequestWorker::class.java.name -> - CancelGossipRequestWorker(appContext, workerParameters, sessionManager) GetGroupDataWorker::class.java.name -> GetGroupDataWorker(appContext, workerParameters, sessionManager) MultipleEventSendingDispatcherWorker::class.java.name -> @@ -66,12 +60,6 @@ internal class MatrixWorkerFactory @Inject constructor(private val sessionManage RedactEventWorker(appContext, workerParameters, sessionManager) SendEventWorker::class.java.name -> SendEventWorker(appContext, workerParameters, sessionManager) - SendGossipRequestWorker::class.java.name -> - SendGossipRequestWorker(appContext, workerParameters, sessionManager) - SendGossipWorker::class.java.name -> - SendGossipWorker(appContext, workerParameters, sessionManager) - SendVerificationMessageWorker::class.java.name -> - SendVerificationMessageWorker(appContext, workerParameters, sessionManager) SyncWorker::class.java.name -> SyncWorker(appContext, workerParameters, sessionManager) UpdateTrustWorker::class.java.name -> diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysrequest/KeyRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/keysrequest/KeyRequestHandler.kt index 0fbb18e63c..034c667aac 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysrequest/KeyRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysrequest/KeyRequestHandler.kt @@ -22,16 +22,17 @@ import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter import im.vector.app.features.popup.DefaultVectorAlert import im.vector.app.features.popup.PopupAlertManager +import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo -import org.matrix.android.sdk.api.session.crypto.model.IncomingRequestCancellation import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction @@ -60,6 +61,9 @@ class KeyRequestHandler @Inject constructor( var session: Session? = null + // This functionality is disabled in element for now. As it could be prone to social attacks + var enablePromptingForRequest = false + fun start(session: Session) { this.session = session session.cryptoService().verificationService().addListener(this) @@ -72,10 +76,9 @@ class KeyRequestHandler @Inject constructor( session = null } - override fun onSecretShareRequest(request: IncomingSecretShareRequest): Boolean { + override fun onSecretShareRequest(request: SecretShareRequest): Boolean { // By default Element 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 } @@ -85,6 +88,8 @@ class KeyRequestHandler @Inject constructor( * @param request the key request. */ override fun onRoomKeyRequest(request: IncomingRoomKeyRequest) { + if (!enablePromptingForRequest) return + val userId = request.userId val deviceId = request.deviceId val requestId = request.requestId @@ -195,15 +200,14 @@ class KeyRequestHandler @Inject constructor( } private fun denyAllRequests(mappingKey: String) { - alertsToRequests[mappingKey]?.forEach { - it.ignore?.run() - } alertsToRequests.remove(mappingKey) } private fun shareAllSessions(mappingKey: String) { alertsToRequests[mappingKey]?.forEach { - it.share?.run() + session?.coroutineScope?.launch { + session?.cryptoService()?.manuallyAcceptRoomKeyRequest(it) + } } alertsToRequests.remove(mappingKey) } @@ -213,7 +217,7 @@ class KeyRequestHandler @Inject constructor( * * @param request the cancellation request. */ - override fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) { + override fun onRequestCancelled(request: IncomingRoomKeyRequest) { // 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/app/features/crypto/recover/BackupToQuadSMigrationTask.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt index bc756974ad..e620500d70 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BackupToQuadSMigrationTask.kt @@ -20,7 +20,6 @@ import im.vector.app.R import im.vector.app.core.platform.ViewModelTask import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.resources.StringProvider -import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME @@ -149,15 +148,8 @@ class BackupToQuadSMigrationTask @Inject constructor( // save for gossiping keysBackupService.saveBackupRecoveryKey(recoveryKey, version.version) - // while we are there let's restore, but do not block - session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( - version, - recoveryKey, - null, - null, - null, - NoOpMatrixCallback() - ) + // It's not a good idea to download the full backup, it might take very long + // and use a lot of resources return Result.Success } catch (failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt index 44694404bc..b65ce4b30b 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -42,7 +42,6 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult -import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest @@ -424,6 +423,11 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( } private fun tentativeRestoreBackup(res: Map?) { + // It's not a good idea to download the full backup, it might take very long + // and use a lot of resources + // Just check that the key is valid and store it, the backup will be used megolm session per + // megolm session when an UISI is encountered + viewModelScope.launch(Dispatchers.IO) { try { val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also { @@ -434,17 +438,13 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( session.cryptoService().keysBackupService().getCurrentVersion(it) }.toKeysVersionResult() ?: return@launch - awaitCallback { - session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( - version, - computeRecoveryKey(secret.fromBase64()), - null, - null, - null, - it - ) + val recoveryKey = computeRecoveryKey(secret.fromBase64()) + val isValid = awaitCallback { + session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(recoveryKey, it) + } + if (isValid) { + session.cryptoService().keysBackupService().saveBackupRecoveryKey(recoveryKey, version.version) } - awaitCallback { session.cryptoService().keysBackupService().trustKeysBackupVersion(version, true, it) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt index 83740c5018..ec4ef26001 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt @@ -27,10 +27,8 @@ import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.utils.createJSonViewerStyleProvider import im.vector.app.databinding.FragmentGenericRecyclerBinding -import org.billcarsonfr.jsonviewer.JSonViewerDialog -import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import javax.inject.Inject class GossipingEventsPaperTrailFragment @Inject constructor( @@ -64,17 +62,17 @@ class GossipingEventsPaperTrailFragment @Inject constructor( super.onDestroyView() } - override fun didTap(event: Event) { - if (event.isEncrypted()) { - event.toClearContentStringWithIndent() - } else { - event.toContentStringWithIndent() - }?.let { - JSonViewerDialog.newInstance( - it, - -1, - createJSonViewerStyleProvider(colorProvider) - ).show(childFragmentManager, "JSON_VIEWER") - } + override fun didTap(event: AuditTrail) { +// if (event.isEncrypted()) { +// event.toClearContentStringWithIndent() +// } else { +// event.toContentStringWithIndent() +// }?.let { +// JSonViewerDialog.newInstance( +// it, +// -1, +// createJSonViewerStyleProvider(colorProvider) +// ).show(childFragmentManager, "JSON_VIEWER") +// } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt index 30c2757eff..e7f09b09e0 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt @@ -32,10 +32,10 @@ import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail data class GossipingEventsPaperTrailState( - val events: Async> = Uninitialized + val events: Async> = Uninitialized ) : MavericksState class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted initialState: GossipingEventsPaperTrailState, diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt index 30c2efc3ce..b8d82699e6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt @@ -17,62 +17,43 @@ package im.vector.app.features.settings.devtools import im.vector.app.core.resources.DateProvider -import me.gujun.android.span.span -import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent -import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest -import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent -import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent -import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail +import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo +import org.matrix.android.sdk.api.session.crypto.model.TrailType +import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo import org.threeten.bp.format.DateTimeFormatter class GossipingEventsSerializer { private val full24DateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS") - fun serialize(eventList: List): String { + fun serialize(eventList: List): String { return buildString { - eventList.forEach { - val clearType = it.getClearType() - append("[${getFormattedDate(it.ageLocalTs)}] $clearType from:${it.senderId} - ") - when (clearType) { - EventType.ROOM_KEY_REQUEST -> { - val content = it.getClearContent().toModel() - append("reqId:${content?.requestId} action:${content?.action} ") - if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) { - append("sessionId: ${content.body?.sessionId} ") - } - append("requestedBy: ${content?.requestingDeviceId}") + eventList.forEach { trail -> + val type = trail.type + val info = trail.info + append("[${getFormattedDate(trail.ageLocalTs)}] ${type.name} ") + append("sessionId: ${info.sessionId} ") + when (type) { + TrailType.IncomingKeyRequest -> { + append("from:${info.userId}|${info.deviceId} - ") } - EventType.FORWARDED_ROOM_KEY -> { - val encryptedContent = it.content.toModel() - val content = it.getClearContent().toModel() - - append("sessionId:${content?.sessionId} From Device (sender key):${encryptedContent?.senderKey}") - span("\nFrom Device (sender key):") { - textStyle = "bold" + TrailType.OutgoingKeyForward -> { + append("to:${info.userId}|${info.deviceId} - ") + (trail.info as? ForwardInfo)?.let { + append("chainIndex: ${it.chainIndex} ") } } - EventType.ROOM_KEY -> { - val content = it.getClearContent() - append("sessionId:${content?.get("session_id")} roomId:${content?.get("room_id")} dest:${content?.get("_dest") ?: "me"}") - } - EventType.SEND_SECRET -> { - val content = it.getClearContent().toModel() - append("requestId:${content?.requestId} From Device:${it.mxDecryptionResult?.payload?.get("sender_device")}") - } - EventType.REQUEST_SECRET -> { - val content = it.getClearContent().toModel() - append("reqId:${content?.requestId} action:${content?.action} ") - if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) { - append("secretName:${content.secretName} ") + TrailType.OutgoingKeyWithheld -> { + append("to:${info.userId}|${info.deviceId} - ") + (trail.info as? WithheldInfo)?.let { + append("code: ${it.code} ") } - append("requestedBy:${content?.requestingDeviceId}") } - EventType.ENCRYPTED -> { - append("Failed to Decrypt") + TrailType.IncomingKeyForward -> { + append("from:${info.userId}|${info.deviceId} - ") + (trail.info as? ForwardInfo)?.let { + append("chainIndex: ${it.chainIndex} ") + } } else -> { append("??") diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt index dd016c2bf5..6a45968916 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt @@ -18,7 +18,6 @@ package im.vector.app.features.settings.devtools import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.paging.PagedListEpoxyController -import im.vector.app.R import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.resources.ColorProvider @@ -26,137 +25,75 @@ import im.vector.app.core.ui.list.GenericItem_ import im.vector.app.core.utils.createUIHandler import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span -import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent -import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject -import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest -import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent -import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent -import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.crypto.model.AuditTrail +import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo +import org.matrix.android.sdk.api.session.crypto.model.TrailType +import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo import javax.inject.Inject class GossipingTrailPagedEpoxyController @Inject constructor( private val vectorDateFormatter: VectorDateFormatter, private val colorProvider: ColorProvider -) : PagedListEpoxyController( +) : PagedListEpoxyController( // Important it must match the PageList builder notify Looper modelBuildingHandler = createUIHandler() ) { interface InteractionListener { - fun didTap(event: Event) + fun didTap(event: AuditTrail) } var interactionListener: InteractionListener? = null - override fun buildItemModel(currentPosition: Int, item: Event?): EpoxyModel<*> { + override fun buildItemModel(currentPosition: Int, item: AuditTrail?): EpoxyModel<*> { val host = this val event = item ?: return GenericItem_().apply { id(currentPosition) } return GenericItem_().apply { id(event.hashCode()) itemClickAction { host.interactionListener?.didTap(event) } - title( - if (event.isEncrypted()) { - "${event.getClearType()} [encrypted]" - } else { - event.type - }?.toEpoxyCharSequence() - ) + title(event.type.name.toEpoxyCharSequence()) description( span { +host.vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME) - span("\nfrom: ") { + span("\n${host.senderFromTo(event.type)}: ") { textStyle = "bold" } - +"${event.senderId}" + +"${event.info.userId}|${event.info.deviceId}" + span("\nroomId: ") { + textStyle = "bold" + } + +event.info.roomId + span("\nsessionId: ") { + textStyle = "bold" + } + +event.info.sessionId 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:") { + when (event.type) { + TrailType.OutgoingKeyForward -> { + val fInfo = event.info as ForwardInfo + span("\nchainIndex: ") { textStyle = "bold" } - +" ${content.body?.sessionId}" + +"${fInfo.chainIndex}" } - 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 = host.colorProvider.getColorFromAttribute(R.attr.colorError) - } - } - span("\nsessionId:") { - textStyle = "bold" - } - +" ${content?.sessionId}" - span("\nFrom Device (sender key):") { - textStyle = "bold" - } - +" ${encryptedContent?.senderKey}" - } else if (event.getClearType() == EventType.ROOM_KEY) { - // it's a bit of a fake event for trail reasons - val content = event.getClearContent() - span("\nsessionId:") { - textStyle = "bold" - } - +" ${content?.get("session_id")}" - span("\nroomId:") { - textStyle = "bold" - } - +" ${content?.get("room_id")}" - span("\nTo :") { - textStyle = "bold" - } - +" ${content?.get("_dest") ?: "me"}" - } 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:") { + TrailType.OutgoingKeyWithheld -> { + val fInfo = event.info as WithheldInfo + span("\ncode: ") { textStyle = "bold" } - +" ${content.secretName}" + +"${fInfo.code}" } - span("\nrequestedBy: ") { - textStyle = "bold" + TrailType.IncomingKeyRequest -> { + // no additional info } - +"${content?.requestingDeviceId}" - } else if (event.getClearType() == EventType.ENCRYPTED) { - span("**Failed to Decrypt** ${event.mCryptoError}") { - textColor = host.colorProvider.getColorFromAttribute(R.attr.colorError) + TrailType.IncomingKeyForward -> { + val fInfo = event.info as ForwardInfo + span("\nchainIndex: ") { + textStyle = "bold" + } + +"${fInfo.chainIndex}" + } + TrailType.Unknown -> { } } } @@ -164,4 +101,14 @@ class GossipingTrailPagedEpoxyController @Inject constructor( ) } } + + private fun senderFromTo(type: TrailType): String { + return when (type) { + TrailType.OutgoingKeyWithheld, + TrailType.OutgoingKeyForward -> "to" + TrailType.IncomingKeyRequest, + TrailType.IncomingKeyForward -> "from" + TrailType.Unknown -> "" + } + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt index 4653f04f2c..09dd198bee 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt @@ -53,6 +53,7 @@ class IncomingKeyRequestPagedController @Inject constructor( textStyle = "bold" } span("${roomKeyRequest.userId}") + +"\n" +host.vectorDateFormatter.format(roomKeyRequest.localCreationTimestamp, DateFormatKind.DEFAULT_DATE_AND_TIME) span("\nsessionId:") { textStyle = "bold" @@ -62,10 +63,6 @@ class IncomingKeyRequestPagedController @Inject constructor( textStyle = "bold" } +"${roomKeyRequest.deviceId}" - span("\nstate: ") { - textStyle = "bold" - } - +roomKeyRequest.state.name }.toEpoxyCharSequence() ) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt index a8045c07e3..e0a130a1e6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt @@ -32,12 +32,12 @@ import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest data class KeyRequestListViewState( val incomingRequests: Async> = Uninitialized, - val outgoingRoomKeyRequests: Async> = Uninitialized + val outgoingRoomKeyRequests: Async> = Uninitialized ) : MavericksState class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState: KeyRequestListViewState, diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt index b23bd77277..13ba4a69ac 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt @@ -22,10 +22,10 @@ import im.vector.app.core.ui.list.GenericItem_ import im.vector.app.core.utils.createUIHandler import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span -import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest +import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest import javax.inject.Inject -class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyController( +class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyController( // Important it must match the PageList builder notify Looper modelBuildingHandler = createUIHandler() ) { @@ -36,7 +36,7 @@ class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyCo var interactionListener: InteractionListener? = null - override fun buildItemModel(currentPosition: Int, item: OutgoingRoomKeyRequest?): EpoxyModel<*> { + override fun buildItemModel(currentPosition: Int, item: OutgoingKeyRequest?): EpoxyModel<*> { val roomKeyRequest = item ?: return GenericItem_().apply { id(currentPosition) } return GenericItem_().apply { diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt index 4f349f8506..d7d18b925d 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSharedSecretStorageService.kt @@ -66,7 +66,7 @@ class FakeSharedSecretStorageService : SharedSecretStorageService { override fun checkShouldBeAbleToAccessSecrets(secretNames: List, keyId: String?) = integrityResult - override fun requestSecret(name: String, myOtherDeviceId: String) { + override suspend fun requestSecret(name: String, myOtherDeviceId: String) { TODO("Not yet implemented") } }