Merge pull request #5559 from vector-im/feature/bca/crypto_better_key_share

Update/Revise SDK to implement reference flowchart for key sharing/forwarding + use backup
This commit is contained in:
Valere 2022-05-11 12:05:58 +02:00 committed by GitHub
commit 304cb07858
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
90 changed files with 3914 additions and 3943 deletions

1
changelog.d/5494.feature Normal file
View file

@ -0,0 +1 @@
Use key backup before requesting keys + refactor & improvement of key request/forward

4
changelog.d/5559.sdk Normal file
View file

@ -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()

View file

@ -19,6 +19,7 @@ package org.matrix.android.sdk.common
import android.os.SystemClock import android.os.SystemClock
import android.util.Log import android.util.Log
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import org.amshove.kluent.fail
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull 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.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session 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.MegolmBackupAuthData
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo 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.IncomingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod 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.Event
import org.matrix.android.sdk.api.session.events.model.EventType 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.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.getRoom
import org.matrix.android.sdk.api.session.room.Room 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.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary 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.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.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.Optional
import org.matrix.android.sdk.api.util.awaitCallback
import org.matrix.android.sdk.api.util.toBase64NoPadding
import java.util.UUID import java.util.UUID
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
import kotlin.coroutines.resume 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<MegolmBackupCreationInfo> {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
}
val version = awaitCallback<KeysVersion> {
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) { fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) {
assertTrue(alice.cryptoService().crossSigningService().canCrossSign()) assertTrue(alice.cryptoService().crossSigningService().canCrossSign())
assertTrue(bob.cryptoService().crossSigningService().canCrossSign()) assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
val requestID = UUID.randomUUID().toString()
val aliceVerificationService = alice.cryptoService().verificationService() val aliceVerificationService = alice.cryptoService().verificationService()
val bobVerificationService = bob.cryptoService().verificationService() val bobVerificationService = bob.cryptoService().verificationService()
aliceVerificationService.beginKeyVerificationInDMs( val localId = UUID.randomUUID().toString()
VerificationMethod.SAS, aliceVerificationService.requestKeyVerificationInDMs(
requestID, localId = localId,
roomId, methods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
bob.myUserId, otherUserId = bob.myUserId,
bob.sessionParams.credentials.deviceId!! 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.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) { testHelper.retryPeriodicallyWithLatch(it) {
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull {
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}") it.requestInfo?.fromDevice == alice.sessionParams.deviceId
if (bobPovTx?.state == VerificationTxState.OnStarted) { } != null
bobPovTx?.performAccept() }
}
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 true
} else { } else {
false 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.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) { 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}") Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}")
alicePovTx?.state == VerificationTxState.ShortCodeReady alicePovTx?.state == VerificationTxState.ShortCodeReady
} }
@ -340,7 +427,7 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
// wait for alice to get the ready // wait for alice to get the ready
testHelper.waitWithLatch { testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) { 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}") Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
if (bobPovTx?.state == VerificationTxState.OnStarted) { if (bobPovTx?.state == VerificationTxState.OnStarted) {
bobPovTx?.performAccept() bobPovTx?.performAccept()
@ -392,4 +479,50 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
return CryptoTestData(roomId, sessions) return CryptoTestData(roomId, sessions)
} }
fun ensureCanDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, messagesText: List<String>) {
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<MessageContent>()?.body
}
}
}
}
fun ensureCannotDecrypt(sentEventIds: List<String>, 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)
}
}
}
}
}
} }

View file

@ -30,13 +30,21 @@ import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.session.Session 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.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.KeysVersion
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult 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.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.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.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent 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.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.getRoomSummary 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.SessionTestParams
import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.common.TestMatrixCallback import org.matrix.android.sdk.common.TestMatrixCallback
import java.util.concurrent.CountDownLatch
@RunWith(JUnit4::class) @RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@LargeTest @LargeTest
class E2eeSanityTests : InstrumentedTest { class E2eeSanityTests : InstrumentedTest {
private val testHelper = CommonTestHelper(context())
private val cryptoTestHelper = CryptoTestHelper(testHelper)
/** /**
* Simple test that create an e2ee room. * Simple test that create an e2ee room.
* Some new members are added, and a message is sent. * Some new members are added, and a message is sent.
@ -72,16 +78,24 @@ class E2eeSanityTests : InstrumentedTest {
*/ */
@Test @Test
fun testSendingE2EEMessages() { fun testSendingE2EEMessages() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession val aliceSession = cryptoTestData.firstSession
val e2eRoomID = cryptoTestData.roomId val e2eRoomID = cryptoTestData.roomId
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! 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 // add some more users and invite them
val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu") val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu")
.map { .map {
testHelper.createAccount(it, SessionTestParams(true)) testHelper.createAccount(it, SessionTestParams(true)).also {
it.cryptoService().enableKeyGossiping(false)
}
} }
Log.v("#E2E TEST", "All accounts created") Log.v("#E2E TEST", "All accounts created")
@ -95,18 +109,18 @@ class E2eeSanityTests : InstrumentedTest {
// All user should accept invite // All user should accept invite
otherAccounts.forEach { otherSession -> otherAccounts.forEach { otherSession ->
waitForAndAcceptInviteInRoom(otherSession, e2eRoomID) waitForAndAcceptInviteInRoom(testHelper, otherSession, e2eRoomID)
Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID") Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
} }
// check that alice see them as joined (not really necessary?) // 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", "All users have joined the room")
Log.v("#E2E TEST", "Alice is sending the message") Log.v("#E2E TEST", "Alice is sending the message")
val text = "This is my 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() // val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first()
Assert.assertTrue("Message should be sent", sentEventId != null) Assert.assertTrue("Message should be sent", sentEventId != null)
@ -114,10 +128,10 @@ class E2eeSanityTests : InstrumentedTest {
otherAccounts.forEach { otherSession -> otherAccounts.forEach { otherSession ->
testHelper.waitWithLatch { latch -> testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) { testHelper.retryPeriodicallyWithLatch(latch) {
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!) val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
timelineEvent != null && timeLineEvent != null &&
timelineEvent.isEncrypted() && timeLineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE timeLineEvent.root.getClearType() == EventType.MESSAGE
} }
} }
} }
@ -136,10 +150,10 @@ class E2eeSanityTests : InstrumentedTest {
} }
newAccount.forEach { newAccount.forEach {
waitForAndAcceptInviteInRoom(it, e2eRoomID) waitForAndAcceptInviteInRoom(testHelper, it, e2eRoomID)
} }
ensureMembersHaveJoined(aliceSession, newAccount, e2eRoomID) ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID)
// wait a bit // wait a bit
testHelper.runBlockingTest { testHelper.runBlockingTest {
@ -164,7 +178,7 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Alice sends a new message") Log.v("#E2E TEST", "Alice sends a new message")
val secondMessage = "2 This is my 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 // new members should be able to decrypt it
newAccount.forEach { otherSession -> newAccount.forEach { otherSession ->
@ -188,6 +202,14 @@ class E2eeSanityTests : InstrumentedTest {
cryptoTestData.cleanUp(testHelper) 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 * Quick test for basic key backup
* 1. Create e2e between Alice and Bob * 1. Create e2e between Alice and Bob
@ -204,6 +226,9 @@ class E2eeSanityTests : InstrumentedTest {
*/ */
@Test @Test
fun testBasicBackupImport() { fun testBasicBackupImport() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession!! val bobSession = cryptoTestData.secondSession!!
@ -227,16 +252,16 @@ class E2eeSanityTests : InstrumentedTest {
val sentEventIds = mutableListOf<String>() val sentEventIds = mutableListOf<String>()
val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning") val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning")
messagesText.forEach { text -> messagesText.forEach { text ->
val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also { val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also {
sentEventIds.add(it) sentEventIds.add(it)
} }
testHelper.waitWithLatch { latch -> testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) { testHelper.retryPeriodicallyWithLatch(latch) {
val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
timelineEvent != null && timeLineEvent != null &&
timelineEvent.isEncrypted() && timeLineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE timeLineEvent.root.getClearType() == EventType.MESSAGE
} }
} }
// we want more so let's discard the session // 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 // 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 // Let's now import keys from backup
newBobSession.cryptoService().keysBackupService().let { keysBackupService -> newBobSession.cryptoService().keysBackupService().let { kbs ->
val keyVersionResult = testHelper.doSync<KeysVersionResult?> { val keyVersionResult = testHelper.doSync<KeysVersionResult?> {
keysBackupService.getVersion(version.version, it) kbs.getVersion(version.version, it)
} }
val importedResult = testHelper.doSync<ImportRoomKeysResult> { val importedResult = testHelper.doSync<ImportRoomKeysResult> {
keysBackupService.restoreKeyBackupWithPassword( kbs.restoreKeyBackupWithPassword(
keyVersionResult!!, keyVersionResult!!,
keyBackupPassword, keyBackupPassword,
null, null,
null, null,
null, it null,
it
) )
} }
@ -312,7 +338,7 @@ class E2eeSanityTests : InstrumentedTest {
} }
// ensure bob can now decrypt // ensure bob can now decrypt
ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
testHelper.signOutAndClose(newBobSession) testHelper.signOutAndClose(newBobSession)
} }
@ -323,6 +349,9 @@ class E2eeSanityTests : InstrumentedTest {
*/ */
@Test @Test
fun testSimpleGossip() { fun testSimpleGossip() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession!! val bobSession = cryptoTestData.secondSession!!
@ -330,30 +359,28 @@ class E2eeSanityTests : InstrumentedTest {
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
cryptoTestHelper.initializeCrossSigning(bobSession)
// let's send a few message to bob // let's send a few message to bob
val sentEventIds = mutableListOf<String>() val sentEventIds = mutableListOf<String>()
val messagesText = listOf("1. Hello", "2. Bob") val messagesText = listOf("1. Hello", "2. Bob")
Log.v("#E2E TEST", "Alice sends some messages") Log.v("#E2E TEST", "Alice sends some messages")
messagesText.forEach { text -> messagesText.forEach { text ->
val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also { val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also {
sentEventIds.add(it) sentEventIds.add(it)
} }
testHelper.waitWithLatch { latch -> testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) { testHelper.retryPeriodicallyWithLatch(latch) {
val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
timelineEvent != null && timeLineEvent != null &&
timelineEvent.isEncrypted() && timeLineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE timeLineEvent.root.getClearType() == EventType.MESSAGE
} }
} }
} }
// Ensure bob can decrypt // Ensure bob can decrypt
ensureIsDecrypted(sentEventIds, bobSession, e2eRoomID) ensureIsDecrypted(testHelper, sentEventIds, bobSession, e2eRoomID)
// Let's now add a new bob session // Let's now add a new bob session
// Create a new session for bob // Create a new session for bob
@ -363,7 +390,11 @@ class E2eeSanityTests : InstrumentedTest {
// check that new bob can't currently decrypt // check that new bob can't currently decrypt
Log.v("#E2E TEST", "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 // Try to request
sentEventIds.forEach { sentEventId -> sentEventIds.forEach { sentEventId ->
@ -372,12 +403,34 @@ class E2eeSanityTests : InstrumentedTest {
} }
// wait a bit // wait a bit
testHelper.runBlockingTest { // we need to wait a couple of syncs to let sharing occurs
delay(10_000) // testHelper.waitFewSyncs(newBobSession, 6)
}
// Ensure that new bob still can't decrypt (keys must have been withheld) // 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<EncryptedEventContent>()!!.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 // Now mark new bob session as verified
@ -390,12 +443,7 @@ class E2eeSanityTests : InstrumentedTest {
newBobSession.cryptoService().reRequestRoomKeyForEvent(event) newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
} }
// wait a bit cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
testHelper.runBlockingTest {
delay(10_000)
}
ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
cryptoTestData.cleanUp(testHelper) cryptoTestData.cleanUp(testHelper)
testHelper.signOutAndClose(newBobSession) testHelper.signOutAndClose(newBobSession)
@ -406,6 +454,9 @@ class E2eeSanityTests : InstrumentedTest {
*/ */
@Test @Test
fun testForwardBetterKey() { fun testForwardBetterKey() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession val aliceSession = cryptoTestData.firstSession
val bobSessionWithBetterKey = cryptoTestData.secondSession!! val bobSessionWithBetterKey = cryptoTestData.secondSession!!
@ -413,35 +464,33 @@ class E2eeSanityTests : InstrumentedTest {
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!! val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
cryptoTestHelper.initializeCrossSigning(bobSessionWithBetterKey)
// let's send a few message to bob // let's send a few message to bob
var firstEventId: String var firstEventId: String
val firstMessage = "1. Hello" val firstMessage = "1. Hello"
Log.v("#E2E TEST", "Alice sends some messages") Log.v("#E2E TEST", "Alice sends some messages")
firstMessage.let { text -> firstMessage.let { text ->
firstEventId = sendMessageInRoom(aliceRoomPOV, text)!! firstEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
testHelper.waitWithLatch { latch -> testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) { testHelper.retryPeriodicallyWithLatch(latch) {
val timelineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId) val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
timelineEvent != null && timeLineEvent != null &&
timelineEvent.isEncrypted() && timeLineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE timeLineEvent.root.getClearType() == EventType.MESSAGE
} }
} }
} }
// Ensure bob can decrypt // Ensure bob can decrypt
ensureIsDecrypted(listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID) ensureIsDecrypted(testHelper, listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID)
// Let's add a new unverified session from bob // Let's add a new unverified session from bob
val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true)) val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true))
// check that new bob can't currently decrypt // check that new bob can't currently decrypt
Log.v("#E2E TEST", "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 // Now let alice send a new message. this time the new bob session will be able to decrypt
var secondEventId: String var secondEventId: String
@ -449,14 +498,14 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Alice sends some messages") Log.v("#E2E TEST", "Alice sends some messages")
secondMessage.let { text -> secondMessage.let { text ->
secondEventId = sendMessageInRoom(aliceRoomPOV, text)!! secondEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
testHelper.waitWithLatch { latch -> testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) { testHelper.retryPeriodicallyWithLatch(latch) {
val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId) val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
timelineEvent != null && timeLineEvent != null &&
timelineEvent.isEncrypted() && timeLineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE timeLineEvent.root.getClearType() == EventType.MESSAGE
} }
} }
} }
@ -475,9 +524,7 @@ class E2eeSanityTests : InstrumentedTest {
try { try {
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
fail("Should not be able to decrypt event") fail("Should not be able to decrypt event")
} catch (error: MXCryptoError) { } catch (_: MXCryptoError) {
val errorType = (error as? MXCryptoError.Base)?.errorType
assertEquals(MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, errorType)
} }
} }
@ -499,41 +546,45 @@ class E2eeSanityTests : InstrumentedTest {
.markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!) .markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!)
// now let new session request // now let new session request
newBobSession.cryptoService().requestRoomKeyForEvent(firstEventNewBobPov.root) newBobSession.cryptoService().reRequestRoomKeyForEvent(firstEventNewBobPov.root)
// wait a bit // We need to wait for the key request to be sent out and then a reply to be received
testHelper.runBlockingTest {
delay(10_000)
}
// old session should have shared the key at earliest known index now // old session should have shared the key at earliest known index now
// we should be able to decrypt both // we should be able to decrypt both
testHelper.runBlockingTest { testHelper.waitWithLatch {
try { testHelper.retryPeriodicallyWithLatch(it) {
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "") val canDecryptFirst = try {
} catch (error: MXCryptoError) { testHelper.runBlockingTest {
fail("Should be able to decrypt first event now $error") newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
} }
} true
testHelper.runBlockingTest { } catch (error: MXCryptoError) {
try { false
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "") }
} catch (error: MXCryptoError) { val canDecryptSecond = try {
fail("Should be able to decrypt event $error") 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) 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) aliceRoomPOV.sendService().sendTextMessage(text)
var sentEventId: String? = null var sentEventId: String? = null
testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch -> testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60)) val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60))
timeline.start() timeline.start()
testHelper.retryPeriodicallyWithLatch(latch) { testHelper.retryPeriodicallyWithLatch(latch) {
val decryptedMsg = timeline.getSnapshot() val decryptedMsg = timeline.getSnapshot()
.filter { it.root.getClearType() == EventType.MESSAGE } .filter { it.root.getClearType() == EventType.MESSAGE }
@ -552,7 +603,157 @@ class E2eeSanityTests : InstrumentedTest {
return sentEventId return sentEventId
} }
private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, 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<Session>, e2eRoomID: String) {
testHelper.waitWithLatch { latch -> testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) { testHelper.retryPeriodicallyWithLatch(latch) {
otherAccounts.map { 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.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) { testHelper.retryPeriodicallyWithLatch(latch) {
val roomSummary = otherSession.getRoomSummary(e2eRoomID) 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") Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
try { try {
otherSession.roomService().joinRoom(e2eRoomID) otherSession.roomService().joinRoom(e2eRoomID)
@ -594,59 +796,14 @@ class E2eeSanityTests : InstrumentedTest {
} }
} }
private fun ensureCanDecrypt(sentEventIds: MutableList<String>, session: Session, e2eRoomID: String, messagesText: List<String>) { private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List<String>, session: Session, e2eRoomID: String) {
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<MessageContent>()?.body
}
}
}
}
private fun ensureIsDecrypted(sentEventIds: List<String>, session: Session, e2eRoomID: String) {
testHelper.waitWithLatch { latch -> testHelper.waitWithLatch { latch ->
sentEventIds.forEach { sentEventId -> sentEventIds.forEach { sentEventId ->
testHelper.retryPeriodicallyWithLatch(latch) { testHelper.retryPeriodicallyWithLatch(latch) {
val timelineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId) val timeLineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
timelineEvent != null && timeLineEvent != null &&
timelineEvent.isEncrypted() && timeLineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
}
private fun ensureCannotDecrypt(sentEventIds: List<String>, 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")
}
} }
} }
} }

View file

@ -27,7 +27,6 @@ import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest 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.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent 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.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom 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.getTimelineEvent
@ -51,10 +50,7 @@ class PreShareKeysTest : InstrumentedTest {
// clear any outbound session // clear any outbound session
aliceSession.cryptoService().discardOutboundSession(e2eRoomID) aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
val preShareCount = bobSession.cryptoService().getGossipingEvents().count { val preShareCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
it.senderId == aliceSession.myUserId &&
it.getClearType() == EventType.ROOM_KEY
}
assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount) assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount)
Log.d("#Test", "Room Key Received from alice $preShareCount") Log.d("#Test", "Room Key Received from alice $preShareCount")
@ -66,23 +62,23 @@ class PreShareKeysTest : InstrumentedTest {
testHelper.waitWithLatch { latch -> testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) { testHelper.retryPeriodicallyWithLatch(latch) {
val newGossipCount = bobSession.cryptoService().getGossipingEvents().count { val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
it.senderId == aliceSession.myUserId && newKeysCount > preShareCount
it.getClearType() == EventType.ROOM_KEY
}
newGossipCount > preShareCount
} }
} }
val latest = bobSession.cryptoService().getGossipingEvents().lastOrNull { val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
it.senderId == aliceSession.myUserId && val aliceOutboundSessionInRoom = aliceCryptoStore.getCurrentOutboundGroupSessionForRoom(e2eRoomID)!!.outboundGroupSession.sessionIdentifier()
it.getClearType() == EventType.ROOM_KEY
}
val content = latest?.getClearContent().toModel<RoomKeyContent>() val bobCryptoStore = (bobSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
assertNotNull("Bob should have received and decrypted a room key event from alice", content) val aliceDeviceBobPov = bobCryptoStore.getUserDevice(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!)!!
assertEquals("Wrong room", e2eRoomID, content!!.roomId) val bobInboundForAlice = bobCryptoStore.getInboundGroupSession(aliceOutboundSessionInRoom, aliceDeviceBobPov.identityKey()!!)
val megolmSessionId = content.sessionId!! 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) val sharedIndex = aliceSession.cryptoService().getSharedWithInfo(e2eRoomID, megolmSessionId)
.getObject(bobSession.myUserId, bobSession.sessionParams.deviceId) .getObject(bobSession.myUserId, bobSession.sessionParams.deviceId)

View file

@ -19,59 +19,45 @@ package org.matrix.android.sdk.internal.crypto.gossiping
import android.util.Log import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue import junit.framework.TestCase.assertTrue
import junit.framework.TestCase.fail import junit.framework.TestCase.fail
import org.amshove.kluent.internal.assertEquals
import org.junit.Assert import org.junit.Assert
import org.junit.Assert.assertNull
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.session.crypto.RequestResult
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.crosssigning.DeviceTrustLevel 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.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.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom 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.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility 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.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.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestConstants
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@LargeTest @LargeTest
class KeyShareTests : InstrumentedTest { class KeyShareTests : InstrumentedTest {
private val commonTestHelper = CommonTestHelper(context())
@Test @Test
@Ignore("This test will be ignored until it is fixed")
fun test_DoNotSelfShareIfNotTrusted() { fun test_DoNotSelfShareIfNotTrusted() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) 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 // Create an encrypted room and add a message
val roomId = commonTestHelper.runBlockingTest { val roomId = commonTestHelper.runBlockingTest {
@ -86,11 +72,18 @@ class KeyShareTests : InstrumentedTest {
assertNotNull(room) assertNotNull(room)
Thread.sleep(4_000) Thread.sleep(4_000)
assertTrue(room?.roomCryptoService()?.isEncrypted() == true) 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) val roomSecondSessionPOV = aliceSession2.getRoom(roomId)
@ -107,7 +100,10 @@ class KeyShareTests : InstrumentedTest {
} }
val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests() val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
assertEquals("There should be no request as it's disabled", 0, outgoingRequestsBefore.size)
// Try to request // Try to request
aliceSession2.cryptoService().enableKeyGossiping(true)
aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root) aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root)
val eventMegolmSessionId = receivedEvent.root.content.toModel<EncryptedEventContent>()?.sessionId val eventMegolmSessionId = receivedEvent.root.content.toModel<EncryptedEventContent>()?.sessionId
@ -117,10 +113,6 @@ class KeyShareTests : InstrumentedTest {
commonTestHelper.waitWithLatch { latch -> commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) { commonTestHelper.retryPeriodicallyWithLatch(latch) {
aliceSession2.cryptoService().getOutgoingRoomKeyRequests() aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
.filter { req ->
// filter out request that was known before
!outgoingRequestsBefore.any { req.requestId == it.requestId }
}
.let { .let {
val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId } val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
outGoingRequestId = outgoing?.requestId outGoingRequestId = outgoing?.requestId
@ -141,20 +133,34 @@ class KeyShareTests : InstrumentedTest {
commonTestHelper.waitWithLatch { latch -> commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) { commonTestHelper.retryPeriodicallyWithLatch(latch) {
// DEBUG LOGS // DEBUG LOGS
aliceSession.cryptoService().getIncomingRoomKeyRequests().let { // aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)") // 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", "=========================") Log.v("TEST", "=========================")
it.forEach { keyRequest -> Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
Log.v( Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
"TEST",
"[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId} is ${keyRequest.state}"
)
}
Log.v("TEST", "=========================") Log.v("TEST", "=========================")
} }
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
incoming?.state == GossipingRequestState.REJECTED 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 // Re request
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root) aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
commonTestHelper.waitWithLatch { latch -> cryptoTestHelper.ensureCanDecrypt(listOf(receivedEvent.eventId), aliceSession2, roomId, listOf(sentEventText ?: ""))
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")
}
commonTestHelper.signOutAndClose(aliceSession) commonTestHelper.signOutAndClose(aliceSession)
commonTestHelper.signOutAndClose(aliceSession2) 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 @Test
@Ignore("This test will be ignored until it is fixed") fun test_reShareIfWasIntendedToBeShared() {
fun test_ShareSSSSSecret() { val commonTestHelper = CommonTestHelper(context())
val aliceSession1 = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
commonTestHelper.doSync<Unit> { val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
aliceSession1.cryptoService().crossSigningService() val aliceSession = testData.firstSession
.initializeCrossSigning( val roomFromAlice = aliceSession.getRoom(testData.roomId)!!
object : UserInteractiveAuthInterceptor { val bobSession = testData.secondSession!!
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = aliceSession1.myUserId,
password = TestConstants.PASSWORD
)
)
}
}, it
)
}
// Also bootstrap keybackup on first session val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first()
val creationInfo = commonTestHelper.doSync<MegolmBackupCreationInfo> { val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
aliceSession1.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
}
val version = commonTestHelper.doSync<KeysVersion> {
aliceSession1.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
}
// Save it for gossiping
aliceSession1.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
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() // Let's try to request any how.
val aliceVerificationService2 = aliceSession2.cryptoService().verificationService() // As it was share previously alice should accept to reshare
bobSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
// force keys download
commonTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
aliceSession1.cryptoService().downloadKeys(listOf(aliceSession1.myUserId), true, it)
}
commonTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
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
)
commonTestHelper.waitWithLatch { latch -> commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(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 @Test
@Ignore("This test will be ignored until it is fixed") fun test_reShareToUnverifiedIfWasIntendedToBeShared() {
fun test_ImproperKeyShareBug() { val commonTestHelper = CommonTestHelper(context())
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
commonTestHelper.doSync<Unit> { val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true)
aliceSession.cryptoService().crossSigningService() val aliceSession = testData.firstSession
.initializeCrossSigning( val roomFromAlice = aliceSession.getRoom(testData.roomId)!!
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
promise.resume(
UserPasswordAuth( // we wait for alice first session to be aware of that session?
user = aliceSession.myUserId, commonTestHelper.waitWithLatch { latch ->
password = TestConstants.PASSWORD, commonTestHelper.retryPeriodicallyWithLatch(latch) {
session = flowResponse.session val newSession = aliceSession.cryptoService().getUserDevices(aliceSession.myUserId)
) .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
) newSession != null
} }
}, it
)
} }
val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first()
val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
// Create an encrypted room and send a couple of messages // Let's try to request any how.
val roomId = commonTestHelper.runBlockingTest { // As it was share previously alice should accept to reshare
aliceSession.roomService().createRoom( aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
CreateRoomParams().apply {
visibility = RoomDirectoryVisibility.PRIVATE commonTestHelper.waitWithLatch { latch ->
enableEncryption() 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)) val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
commonTestHelper.doSync<Unit> { val aliceSession = testData.firstSession
bobSession.cryptoService().crossSigningService() val bobSession = testData.secondSession!!
.initializeCrossSigning( val roomFromBob = bobSession.getRoom(testData.roomId)!!
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, it
)
}
// Let alice invite bob val sentEvents = commonTestHelper.sendTextMessage(roomFromBob, "Hello", 3)
commonTestHelper.runBlockingTest { val sentEventMegolmSession = sentEvents.first().root.content.toModel<EncryptedEventContent>()!!.sessionId!!
roomAlicePov.membershipService().invite(bobSession.myUserId, null)
}
commonTestHelper.runBlockingTest { // Let alice now add a new session
bobSession.roomService().joinRoom(roomAlicePov.roomId, null, emptyList()) val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(false))
} aliceNewSession.cryptoService().enableKeyGossiping(false)
commonTestHelper.syncSession(aliceNewSession)
// we want to discard alice outbound session // we wait bob first session to be aware of that session?
aliceSession.cryptoService().discardOutboundSession(roomAlicePov.roomId) commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
// and now resend a new message to reset index to 0 val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId)
commonTestHelper.sendTextMessage(roomAlicePov, "After", 1) .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
newSession != null
val roomRoomBobPov = aliceSession.getRoom(roomId)
val beforeJoin = roomRoomBobPov!!.getTimelineEvent(secondEventId)
var dRes = tryOrNull {
commonTestHelper.runBlockingTest {
bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "")
} }
} }
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<EncryptedEventContent>()!!.sessionId)
}
assertEquals(sentEventMegolmSession, newEvent.root.content.toModel<EncryptedEventContent>()!!.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 :/ commonTestHelper.waitWithLatch { latch ->
dRes = tryOrNull { commonTestHelper.retryPeriodicallyWithLatch(latch) {
commonTestHelper.runBlockingTest { val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
bobSession.cryptoService().decryptEvent(beforeJoin.root, "") 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<MessageContent>()?.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<EncryptedEventContent>()!!.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<EncryptedEventContent>()!!.sessionId)
}
assertEquals(sentEventMegolmSession, newEvent.root.content.toModel<EncryptedEventContent>()!!.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)
} }
} }

View file

@ -21,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import org.junit.Assert import org.junit.Assert
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters 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.NoOpMatrixCallback
import org.matrix.android.sdk.api.extensions.tryOrNull 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.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.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent 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.content.WithHeldCode
@ -46,12 +46,11 @@ import org.matrix.android.sdk.common.TestConstants
@LargeTest @LargeTest
class WithHeldTests : InstrumentedTest { class WithHeldTests : InstrumentedTest {
private val testHelper = CommonTestHelper(context())
private val cryptoTestHelper = CryptoTestHelper(testHelper)
@Test @Test
@Ignore("This test will be ignored until it is fixed")
fun test_WithHeldUnverifiedReason() { fun test_WithHeldUnverifiedReason() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
// ============================= // =============================
// ARRANGE // ARRANGE
// ============================= // =============================
@ -69,7 +68,6 @@ class WithHeldTests : InstrumentedTest {
val roomAlicePOV = aliceSession.getRoom(roomId)!! val roomAlicePOV = aliceSession.getRoom(roomId)!!
val bobUnverifiedSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true)) val bobUnverifiedSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
// ============================= // =============================
// ACT // ACT
// ============================= // =============================
@ -88,6 +86,7 @@ class WithHeldTests : InstrumentedTest {
val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!! val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!!
val megolmSessionId = eventBobPOV.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
// ============================= // =============================
// ASSERT // ASSERT
// ============================= // =============================
@ -103,9 +102,23 @@ class WithHeldTests : InstrumentedTest {
val type = (failure as MXCryptoError.Base).errorType val type = (failure as MXCryptoError.Base).errorType
val technicalMessage = failure.technicalMessage val technicalMessage = failure.technicalMessage
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) 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 // enable back sending to unverified
aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false) aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false)
@ -130,7 +143,7 @@ class WithHeldTests : InstrumentedTest {
val type = (failure as MXCryptoError.Base).errorType val type = (failure as MXCryptoError.Base).errorType
val technicalMessage = failure.technicalMessage val technicalMessage = failure.technicalMessage
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type) 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) testHelper.signOutAndClose(aliceSession)
@ -139,8 +152,10 @@ class WithHeldTests : InstrumentedTest {
} }
@Test @Test
@Ignore("This test will be ignored until it is fixed")
fun test_WithHeldNoOlm() { fun test_WithHeldNoOlm() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = testData.firstSession val aliceSession = testData.firstSession
val bobSession = testData.secondSession!! val bobSession = testData.secondSession!!
@ -220,8 +235,10 @@ class WithHeldTests : InstrumentedTest {
} }
@Test @Test
@Ignore("This test will be ignored until it is fixed")
fun test_WithHeldKeyRequest() { fun test_WithHeldKeyRequest() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = testData.firstSession val aliceSession = testData.firstSession
val bobSession = testData.secondSession!! val bobSession = testData.secondSession!!
@ -267,5 +284,8 @@ class WithHeldTests : InstrumentedTest {
wc?.code == WithHeldCode.UNAUTHORISED wc?.code == WithHeldCode.UNAUTHORISED
} }
} }
testHelper.signOutAndClose(aliceSession)
testHelper.signOutAndClose(bobSecondSession)
} }
} }

View file

@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.crypto.verification.qrcode
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBe
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
@ -40,7 +39,6 @@ import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@Ignore("This test is flaky ; see issue #5449")
class VerificationTest : InstrumentedTest { class VerificationTest : InstrumentedTest {
data class ExpectedResult( data class ExpectedResult(

View file

@ -31,5 +31,11 @@ data class MXCryptoConfig constructor(
* If set to false, the request will be forwarded to the application layer; in this * If set to false, the request will be forwarded to the application layer; in this
* case the application can decide to prompt the user. * 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,
) )

View file

@ -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.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService 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.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.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse 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.MXEncryptEventContentResult
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult 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.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.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.events.model.Content 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.Event
@ -76,6 +75,15 @@ interface CryptoService {
fun setGlobalBlacklistUnverifiedDevices(block: Boolean) 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 setRoomUnBlacklistUnverifiedDevices(roomId: String)
fun getDeviceTrackingStatus(userId: String): Int fun getDeviceTrackingStatus(userId: String): Int
@ -94,8 +102,6 @@ interface CryptoService {
fun reRequestRoomKeyForEvent(event: Event) fun reRequestRoomKeyForEvent(event: Event)
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody)
fun addRoomKeysRequestListener(listener: GossipingRequestListener) fun addRoomKeysRequestListener(listener: GossipingRequestListener)
fun removeRoomKeysRequestListener(listener: GossipingRequestListener) fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
@ -142,14 +148,20 @@ interface CryptoService {
fun addNewSessionListener(newSessionListener: NewSessionListener) fun addNewSessionListener(newSessionListener: NewSessionListener)
fun removeSessionListener(listener: NewSessionListener) fun removeSessionListener(listener: NewSessionListener)
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest>
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>>
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
fun getGossipingEventsTrail(): LiveData<PagedList<Event>> /**
fun getGossipingEvents(): List<Event> * 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<PagedList<AuditTrail>>
fun getGossipingEvents(): List<AuditTrail>
// For testing shared session // For testing shared session
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int> fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>

View file

@ -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<String, List<String>>,
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<RequestReply>
) {
/**
* Used only for log.
*
* @return the room id.
*/
val roomId = requestBody?.roomId
/**
* Used only for log.
*
* @return the session id
*/
val sessionId = requestBody?.sessionId
}

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,14 +14,20 @@
* limitations under the License. * 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, UNSENT,
SENDING,
SENT, SENT,
CANCELLING, SENT_THEN_CANCELED,
CANCELLED, CANCELLATION_PENDING,
FAILED_TO_SEND, CANCELLATION_PENDING_AND_WILL_RESEND;
FAILED_TO_CANCEL
companion object {
fun pendingStates() = setOf(
UNSENT,
CANCELLATION_PENDING_AND_WILL_RESEND,
CANCELLATION_PENDING
)
}
} }

View file

@ -16,9 +16,8 @@
package org.matrix.android.sdk.api.session.crypto.keyshare 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.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 * Room keys events listener
@ -35,12 +34,12 @@ interface GossipingRequestListener {
* Returns the secret value to be shared * Returns the secret value to be shared
* @return true if is handled * @return true if is handled
*/ */
fun onSecretShareRequest(request: IncomingSecretShareRequest): Boolean fun onSecretShareRequest(request: SecretShareRequest): Boolean
/** /**
* A room key request cancellation has been received. * A room key request cancellation has been received.
* *
* @param request the cancellation request * @param request the cancellation request
*/ */
fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) fun onRequestCancelled(request: IncomingRoomKeyRequest)
} }

View file

@ -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
)

View file

@ -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<ShareRequestCancellation>()
?.let {
IncomingRequestCancellation(
userId = event.senderId,
deviceId = it.requestingDeviceId,
requestId = it.requestId,
localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis
)
}
}
}
}

View file

@ -16,9 +16,7 @@
package org.matrix.android.sdk.api.session.crypto.model package org.matrix.android.sdk.api.session.crypto.model
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.internal.util.time.Clock
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
/** /**
* IncomingRoomKeyRequest class defines the incoming room keys request. * IncomingRoomKeyRequest class defines the incoming room keys request.
@ -27,38 +25,25 @@ data class IncomingRoomKeyRequest(
/** /**
* The user id * The user id
*/ */
override val userId: String? = null, val userId: String? = null,
/** /**
* The device id * The device id
*/ */
override val deviceId: String? = null, val deviceId: String? = null,
/** /**
* The request id * The request id
*/ */
override val requestId: String? = null, val requestId: String? = null,
/** /**
* The request body * The request body
*/ */
val requestBody: RoomKeyRequestBody? = null, val requestBody: RoomKeyRequestBody? = null,
val state: GossipingRequestState = GossipingRequestState.NONE, val localCreationTimestamp: Long?
) {
/**
* 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 {
companion object { companion object {
/** /**
* Factory * Factory
@ -66,18 +51,36 @@ data class IncomingRoomKeyRequest(
* @param event the event * @param event the event
* @param currentTimeMillis the current time in milliseconds * @param currentTimeMillis the current time in milliseconds
*/ */
fun fromEvent(event: Event, currentTimeMillis: Long): IncomingRoomKeyRequest? { fun fromEvent(trail: AuditTrail): IncomingRoomKeyRequest? {
return event.getClearContent() return trail
.toModel<RoomKeyShareRequest>() .takeIf { it.type == TrailType.IncomingKeyRequest }
?.let {
it.info as? IncomingKeyRequestInfo
}
?.let { ?.let {
IncomingRoomKeyRequest( IncomingRoomKeyRequest(
userId = event.senderId, userId = it.userId,
deviceId = it.requestingDeviceId, deviceId = it.deviceId,
requestId = it.requestId, requestId = it.requestId,
requestBody = it.body ?: RoomKeyRequestBody(), requestBody = RoomKeyRequestBody(
localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis 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()
)
}
} }
} }

View file

@ -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<SecretShareRequest>()
?.let {
IncomingSecretShareRequest(
userId = event.senderId,
deviceId = it.requestingDeviceId,
requestId = it.requestId,
secretName = it.secretName,
localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis
)
}
}
}
}

View file

@ -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<String, List<String>>,
// 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
}

View file

@ -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. * 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? val code: WithHeldCode?

View file

@ -131,7 +131,7 @@ interface SharedSecretStorageService {
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?): IntegrityResult fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?): IntegrityResult
fun requestSecret(name: String, myOtherDeviceId: String) suspend fun requestSecret(name: String, myOtherDeviceId: String)
data class KeyRef( data class KeyRef(
val keyId: String?, val keyId: String?,

View file

@ -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<CancelGossipRequestWorker.Params>(context, params, sessionManager, Params::class.java) {
@JsonClass(generateAdapter = true)
internal data class Params(
override val sessionId: String,
val requestId: String,
val recipients: Map<String, List<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 {
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<Any>()
val toDeviceContent = ShareRequestCancellation(
requestingDeviceId = credentials.deviceId,
requestId = params.requestId
)
cryptoStore.saveGossipingEvent(Event(
type = EventType.ROOM_KEY_REQUEST,
content = toDeviceContent.toContent(),
senderId = credentials.userId
).also {
it.ageLocalTs = 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)
}
}

View file

@ -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.CryptoService
import org.matrix.android.sdk.api.session.crypto.MXCryptoError 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.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.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME 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.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.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.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener 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.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse 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.MXEncryptEventContentResult
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult 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.MXUsersDevicesMap
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.RoomKeyRequestBody 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.Content
import org.matrix.android.sdk.api.session.events.model.Event 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.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.RoomKeyContent
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent 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.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility 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.actions.SetDeviceVerificationAction
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting 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.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.megolm.MXMegolmEncryptionFactory
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService 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.SetDeviceNameTask
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask 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.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.DeviceId
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
@ -156,9 +156,10 @@ internal class DefaultCryptoService @Inject constructor(
private val crossSigningService: DefaultCrossSigningService, 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 // Actions
private val setDeviceVerificationAction: SetDeviceVerificationAction, private val setDeviceVerificationAction: SetDeviceVerificationAction,
private val megolmSessionDataImporter: MegolmSessionDataImporter, private val megolmSessionDataImporter: MegolmSessionDataImporter,
@ -178,6 +179,7 @@ internal class DefaultCryptoService @Inject constructor(
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val cryptoCoroutineScope: CoroutineScope, private val cryptoCoroutineScope: CoroutineScope,
private val eventDecryptor: EventDecryptor, private val eventDecryptor: EventDecryptor,
private val verificationMessageProcessor: VerificationMessageProcessor,
private val liveEventManager: Lazy<StreamEventsManager> private val liveEventManager: Lazy<StreamEventsManager>
) : CryptoService { ) : 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 // handle state events
if (event.isStateEvent()) { if (event.isStateEvent()) {
when (event.type) { when (event.type) {
@ -201,9 +203,18 @@ internal class DefaultCryptoService @Inject constructor(
EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) 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<Event>() // val gossipingBuffer = mutableListOf<Event>()
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) { override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
setDeviceNameTask setDeviceNameTask
@ -379,27 +390,8 @@ internal class DefaultCryptoService @Inject constructor(
// Open the store // Open the store
cryptoStore.open() cryptoStore.open()
runCatching { isStarting.set(false)
// if (isInitialSync) { isStarted.set(true)
// // 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")
}
)
} }
/** /**
@ -407,7 +399,8 @@ internal class DefaultCryptoService @Inject constructor(
*/ */
fun close() = runBlocking(coroutineDispatchers.crypto) { fun close() = runBlocking(coroutineDispatchers.crypto) {
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
incomingGossipingRequestManager.close() incomingKeyRequestManager.close()
outgoingKeyRequestManager.close()
olmDevice.release() olmDevice.release()
cryptoStore.close() cryptoStore.close()
} }
@ -472,15 +465,28 @@ internal class DefaultCryptoService @Inject constructor(
} }
oneTimeKeysUploader.maybeUploadOneTimeKeys() oneTimeKeysUploader.maybeUploadOneTimeKeys()
incomingGossipingRequestManager.processReceivedGossipingRequests()
} }
}
tryOrNull { // Process pending key requests
gossipingBuffer.toList().let { try {
cryptoStore.saveGossipingEvents(it) 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?) // (for now at least. Maybe we should alert the user somehow?)
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId) val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
if (existingAlgorithm == algorithm && roomEncryptorsStore.get(roomId) != null) { if (existingAlgorithm == algorithm) {
// ignore // ignore
Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in $roomId") Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in $roomId")
return false return false
@ -787,19 +793,25 @@ internal class DefaultCryptoService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
when (event.getClearType()) { when (event.getClearType()) {
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> { EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
gossipingBuffer.add(event)
// Keys are imported directly, not waiting for end of sync // Keys are imported directly, not waiting for end of sync
onRoomKeyEvent(event) onRoomKeyEvent(event)
} }
EventType.REQUEST_SECRET, EventType.REQUEST_SECRET -> {
secretShareManager.handleSecretRequest(event)
}
EventType.ROOM_KEY_REQUEST -> { EventType.ROOM_KEY_REQUEST -> {
// save audit trail event.getClearContent().toModel<RoomKeyShareRequest>()?.let { req ->
gossipingBuffer.add(event) // We'll always get these because we send room key requests to
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete) // '*' (ie. 'all devices') which includes the sending device,
incomingGossipingRequestManager.onGossipingRequestEvent(event) // 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 -> { EventType.SEND_SECRET -> {
gossipingBuffer.add(event)
onSecretSendReceived(event) onSecretSendReceived(event)
} }
EventType.ROOM_KEY_WITHHELD -> { EventType.ROOM_KEY_WITHHELD -> {
@ -837,50 +849,38 @@ internal class DefaultCryptoService @Inject constructor(
val withHeldContent = event.getClearContent().toModel<RoomKeyWithHeldContent>() ?: return Unit.also { val withHeldContent = event.getClearContent().toModel<RoomKeyWithHeldContent>() ?: return Unit.also {
Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields") Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields")
} }
Timber.tag(loggerTag.value).i("onKeyWithHeldReceived() received from:${event.senderId}, content <$withHeldContent>") val senderId = event.senderId ?: return Unit.also {
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm) Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields")
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
} }
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) { private suspend fun onSecretSendReceived(event: Event) {
Timber.tag(loggerTag.value).i("GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}") secretShareManager.onSecretSendReceived(event) { secretName, secretValue ->
if (!event.isEncrypted()) { handleSDKLevelGossip(secretName, secretValue)
// 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<SecretSendEventContent>() ?: 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")
} }
} }
/** /**
* Returns true if handled by SDK, otherwise should be sent to application layer * 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) { return when (secretName) {
MASTER_KEY_SSSS_NAME -> { MASTER_KEY_SSSS_NAME -> {
crossSigningService.onSecretMSKGossip(secretValue) crossSigningService.onSecretMSKGossip(secretValue)
@ -1095,6 +1095,12 @@ internal class DefaultCryptoService @Inject constructor(
cryptoStore.setGlobalBlacklistUnverifiedDevices(block) 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. * Tells whether the client should ever send encrypted messages to unverified devices.
* The default value is false. * The default value is false.
@ -1158,52 +1164,17 @@ internal class DefaultCryptoService @Inject constructor(
setRoomBlacklistUnverifiedDevices(roomId, false) 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. * Re request the encryption keys required to decrypt an event.
* *
* @param event the event to decrypt again. * @param event the event to decrypt again.
*/ */
override fun reRequestRoomKeyForEvent(event: Event) { override fun reRequestRoomKeyForEvent(event: Event) {
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also { outgoingKeyRequestManager.requestKeyForEvent(event, true)
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)
} }
override fun requestRoomKeyForEvent(event: Event) { override fun requestRoomKeyForEvent(event: Event) {
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also { outgoingKeyRequestManager.requestKeyForEvent(event, false)
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}")
}
}
} }
/** /**
@ -1212,7 +1183,8 @@ internal class DefaultCryptoService @Inject constructor(
* @param listener listener * @param listener listener
*/ */
override fun addRoomKeysRequestListener(listener: GossipingRequestListener) { 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 * @param listener listener
*/ */
override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) { 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<String, Any>("type" to EventType.DUMMY)
//
// val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
// val sendToDeviceMap = MXUsersDevicesMap<Any>()
// 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 * Provides the list of unknown devices
* *
@ -1302,27 +1242,41 @@ internal class DefaultCryptoService @Inject constructor(
return "DefaultCryptoService of $userId ($deviceId)" return "DefaultCryptoService of $userId ($deviceId)"
} }
override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> { override fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest> {
return cryptoStore.getOutgoingRoomKeyRequests() return cryptoStore.getOutgoingRoomKeyRequests()
} }
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> { override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>> {
return cryptoStore.getOutgoingRoomKeyRequestsPaged() return cryptoStore.getOutgoingRoomKeyRequestsPaged()
} }
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
return cryptoStore.getIncomingRoomKeyRequestsPaged()
}
override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> { override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
return cryptoStore.getIncomingRoomKeyRequests() return cryptoStore.getGossipingEvents()
.mapNotNull {
IncomingRoomKeyRequest.fromEvent(it)
}
} }
override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> { override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
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<PagedList<AuditTrail>> {
return cryptoStore.getGossipingEventsTrail() return cryptoStore.getGossipingEventsTrail()
} }
override fun getGossipingEvents(): List<Event> { override fun getGossipingEvents(): List<AuditTrail> {
return cryptoStore.getGossipingEvents() return cryptoStore.getGossipingEvents()
} }
@ -1346,8 +1300,8 @@ internal class DefaultCryptoService @Inject constructor(
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members") Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members")
callback.onFailure(failure) // we probably shouldn't block sending on that (but questionable)
return@launch // but some members won't be able to decrypt
} }
val userIds = getRoomUserIds(roomId) val userIds = getRoomUserIds(roomId)

View file

@ -315,10 +315,19 @@ internal class DeviceListManager @Inject constructor(
} else { } else {
Timber.v("## CRYPTO | downloadKeys() : starts") Timber.v("## CRYPTO | downloadKeys() : starts")
val t0 = clock.epochMillis() val t0 = clock.epochMillis()
val result = doKeyDownloadForUsers(downloadUsers) try {
Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms") val result = doKeyDownloadForUsers(downloadUsers)
result.also { Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms")
it.addEntriesFromMap(stored) 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
}
} }
} }
} }

View file

@ -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 <reified W : ListenableWorker> createWork(data: Data, startChain: Boolean): OneTimeWorkRequest {
return workManagerProvider.matrixOneTimeWorkRequestBuilder<W>()
.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)
}
}

View file

@ -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<IncomingShareRequestCommon>()
private val receivedRequestCancellations = ArrayList<IncomingRequestCancellation>()
// the listeners
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet()
init {
receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
}
fun close() {
executor.shutdownNow()
}
// Recently verified devices (map of deviceId and timestamp)
private val recentlyVerifiedDevices = HashMap<String, Long>()
/**
* 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<GossipingDefaultContent>()
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<IncomingRequestCancellation>? = 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<SendGossipWorker>(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<SendGossipWorker>(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
}
}

View file

@ -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<ValidMegolmRequestBody>()
// the listeners
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = 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<Any>().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<Any>()
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")
}
}
}

View file

@ -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?
}

View file

@ -43,7 +43,6 @@ import org.matrix.olm.OlmOutboundGroupSession
import org.matrix.olm.OlmSession import org.matrix.olm.OlmSession
import org.matrix.olm.OlmUtility import org.matrix.olm.OlmUtility
import timber.log.Timber import timber.log.Timber
import java.net.URLEncoder
import javax.inject.Inject import javax.inject.Inject
private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO) 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).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() val olmMessage = OlmMessage()
olmMessage.mCipherText = ciphertext olmMessage.mCipherText = ciphertext
olmMessage.mType = messageType.toLong() olmMessage.mType = messageType.toLong()
@ -589,6 +580,13 @@ internal class MXOlmDevice @Inject constructor(
// Inbound group session // 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. * Add an inbound group session to the session store.
* *
@ -607,7 +605,7 @@ internal class MXOlmDevice @Inject constructor(
senderKey: String, senderKey: String,
forwardingCurve25519KeyChain: List<String>, forwardingCurve25519KeyChain: List<String>,
keysClaimed: Map<String, String>, keysClaimed: Map<String, String>,
exportFormat: Boolean): Boolean { exportFormat: Boolean): AddSessionResult {
val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat) val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
val existingSession = existingSessionHolder?.wrapper val existingSession = existingSessionHolder?.wrapper
@ -615,7 +613,7 @@ internal class MXOlmDevice @Inject constructor(
if (existingSession != null) { if (existingSession != null) {
Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session") Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session")
try { 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? // This is quite unexpected, could throw if native was released?
Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session") Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
candidateSession.olmInboundGroupSession?.releaseSession() candidateSession.olmInboundGroupSession?.releaseSession()
@ -626,12 +624,12 @@ internal class MXOlmDevice @Inject constructor(
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId") Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
candidateSession.olmInboundGroupSession?.releaseSession() candidateSession.olmInboundGroupSession?.releaseSession()
return false return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt())
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}") Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
candidateSession.olmInboundGroupSession?.releaseSession() candidateSession.olmInboundGroupSession?.releaseSession()
return false return AddSessionResult.NotImported
} }
} }
@ -641,19 +639,19 @@ internal class MXOlmDevice @Inject constructor(
val candidateOlmInboundSession = candidateSession.olmInboundGroupSession val candidateOlmInboundSession = candidateSession.olmInboundGroupSession
if (null == candidateOlmInboundSession) { if (null == candidateOlmInboundSession) {
Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>") Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>")
return false return AddSessionResult.NotImported
} }
try { try {
if (candidateOlmInboundSession.sessionIdentifier() != sessionId) { if (candidateOlmInboundSession.sessionIdentifier() != sessionId) {
Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
candidateOlmInboundSession.releaseSession() candidateOlmInboundSession.releaseSession()
return false return AddSessionResult.NotImported
} }
} catch (e: Throwable) { } catch (e: Throwable) {
candidateOlmInboundSession.releaseSession() candidateOlmInboundSession.releaseSession()
Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed") Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed")
return false return AddSessionResult.NotImported
} }
candidateSession.senderKey = senderKey candidateSession.senderKey = senderKey
@ -667,7 +665,7 @@ internal class MXOlmDevice @Inject constructor(
inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey) 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)) { if (timelineSet.contains(messageIndexKey)) {
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex) 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) throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
} }

View file

@ -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<String, List<String>>
val requestId: String
val state: OutgoingGossipingRequestState
// transaction id for the cancellation, if any
// var cancellationTxnId: String?
}

View file

@ -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<String, List<String>>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients)?.let {
// Don't resend if it's already done, you need to cancel first (reRequest)
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
Timber.v("## CRYPTO - GOSSIP sendOutgoingRoomKeyRequest() : we already request for that session: $it")
return@launch
}
sendOutgoingGossipingRequest(it)
}
}
}
fun sendSecretShareRequest(secretName: String, recipients: Map<String, List<String>>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
// 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<SendGossipRequestWorker>(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<CancelGossipRequestWorker>(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<SendGossipRequestWorker>(WorkerParamsFactory.toData(reSendParams), true)
gossipingWorkManager.postWork(reSendWorkRequest)
}
}
}

View file

@ -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<Pair<String, String>>()
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<Map<String, List<String>>, RoomKeyRequestBody>? {
val sender = event.senderId ?: return null
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: 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<EncryptedEventContent>() ?: 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<String, List<String>>, 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<RoomKeyWithHeldContent>()?.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<String, List<String>>, 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<Any>()
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<Any>()
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) }
}
}
}

View file

@ -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<String, List<String>>,
// 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
}

View file

@ -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<DefaultKeysBackupService>,
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<Info, LastTry>()
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<ImportRoomKeysResult> {
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
}
}
}

View file

@ -16,12 +16,21 @@
package org.matrix.android.sdk.internal.crypto 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.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 org.matrix.android.sdk.internal.session.SessionScope
import javax.inject.Inject import javax.inject.Inject
@SessionScope @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. // MXEncrypting instance for each room.
private val roomEncryptors = mutableMapOf<String, IMXEncrypting>() private val roomEncryptors = mutableMapOf<String, IMXEncrypting>()
@ -34,7 +43,18 @@ internal class RoomEncryptorsStore @Inject constructor() {
fun get(roomId: String): IMXEncrypting? { fun get(roomId: String): IMXEncrypting? {
return synchronized(roomEncryptors) { 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
}
} }
} }
} }

View file

@ -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<String, Long>()
private val verifMutex = Mutex()
/**
* Secrets are exchanged as part of interactive verification,
* so we can just store in memory.
*/
private val outgoingSecretRequests = mutableListOf<SecretShareRequest>()
// the listeners
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = 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<SecretShareRequest>()
?: 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<Any>()
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<Any>()
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<SecretSendEventContent>() ?: 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")
}
}
}

View file

@ -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<SendGossipRequestWorker.Params>(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<Any>()
val eventType: String
val requestId: String
when {
params.keyShareRequest != null -> {
eventType = EventType.ROOM_KEY_REQUEST
requestId = params.keyShareRequest.requestId
val toDeviceContent = RoomKeyShareRequest(
requestingDeviceId = credentials.deviceId,
requestId = params.keyShareRequest.requestId,
action = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
body = params.keyShareRequest.requestBody
)
cryptoStore.saveGossipingEvent(Event(
type = eventType,
content = toDeviceContent.toContent(),
senderId = credentials.userId
).also {
it.ageLocalTs = 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)
}
}

View file

@ -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<SendGossipWorker.Params>(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<Any>()
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)
}
}

View file

@ -17,12 +17,13 @@
package org.matrix.android.sdk.internal.crypto.actions package org.matrix.android.sdk.internal.crypto.actions
import androidx.annotation.WorkerThread 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.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.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.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.MegolmSessionData 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.RoomDecryptorProvider
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore 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 timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class MegolmSessionDataImporter @Inject constructor( private val loggerTag = LoggerTag("MegolmSessionDataImporter", LoggerTag.CRYPTO)
private val olmDevice: MXOlmDevice,
private val roomDecryptorProvider: RoomDecryptorProvider, internal class MegolmSessionDataImporter @Inject constructor(private val olmDevice: MXOlmDevice,
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val roomDecryptorProvider: RoomDecryptorProvider,
private val cryptoStore: IMXCryptoStore, private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val clock: Clock, private val cryptoStore: IMXCryptoStore,
private val clock: Clock,
) { ) {
/** /**
@ -66,19 +68,23 @@ internal class MegolmSessionDataImporter @Inject constructor(
if (null != decrypting) { if (null != decrypting) {
try { try {
val sessionId = megolmSessionData.sessionId 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++ totalNumbersOfImportedKeys++
// cancel any outstanding room key requests for this session // 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 // Have another go at decrypting events sent with this session
when (decrypting) { when (decrypting) {
@ -87,7 +93,7 @@ internal class MegolmSessionDataImporter @Inject constructor(
} }
} }
} catch (e: Exception) { } 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() 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) return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys)
} }

View file

@ -17,8 +17,6 @@
package org.matrix.android.sdk.internal.crypto.algorithms 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.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.crypto.model.MXEventDecryptionResult
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
@ -44,23 +42,4 @@ internal interface IMXDecrypting {
* @param event the key event. * @param event the key event.
*/ */
fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {} 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)
} }

View file

@ -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)
}

View file

@ -17,51 +17,32 @@
package org.matrix.android.sdk.internal.crypto.algorithms.megolm package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import dagger.Lazy 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.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError 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.NewSessionListener
import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent 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.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.Event
import org.matrix.android.sdk.api.session.events.model.EventType 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.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent 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.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.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
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.algorithms.IMXDecrypting 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.keysbackup.DefaultKeysBackupService
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore 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 org.matrix.android.sdk.internal.session.StreamEventsManager
import timber.log.Timber import timber.log.Timber
private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO) private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO)
internal class MXMegolmDecryption(private val userId: String, internal class MXMegolmDecryption(
private val olmDevice: MXOlmDevice, private val olmDevice: MXOlmDevice,
private val deviceListManager: DeviceListManager, private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val cryptoStore: IMXCryptoStore,
private val messageEncrypter: MessageEncrypter, private val liveEventManager: Lazy<StreamEventsManager>
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, ) : IMXDecrypting {
private val cryptoStore: IMXCryptoStore,
private val sendToDeviceTask: SendToDeviceTask,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope,
private val liveEventManager: Lazy<StreamEventsManager>
) : IMXDecrypting, IMXWithHeldExtension {
var newSessionListener: NewSessionListener? = null var newSessionListener: NewSessionListener? = null
@ -73,10 +54,7 @@ internal class MXMegolmDecryption(private val userId: String,
@Throws(MXCryptoError::class) @Throws(MXCryptoError::class)
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
// If cross signing is enabled, we don't send request until the keys are trusted return decryptEvent(event, timeline, true)
// 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)
} }
@Throws(MXCryptoError::class) @Throws(MXCryptoError::class)
@ -126,13 +104,14 @@ internal class MXMegolmDecryption(private val userId: String,
if (throwable is MXCryptoError.OlmError) { if (throwable is MXCryptoError.OlmError) {
// TODO Check the value of .message // TODO Check the value of .message
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") { if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
// addEventToPendingList(event, timeline) // So we know that session, but it's ratcheted and we can't decrypt at that index
// The session might has been partially withheld (and only pass ratcheted)
if (requestKeysOnFail) {
requestKeysForEvent(event)
}
// Check if partially withheld
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
if (withHeldInfo != null) { if (withHeldInfo != null) {
if (requestKeysOnFail) {
requestKeysForEvent(event, true)
}
// Encapsulate as withHeld exception // Encapsulate as withHeld exception
throw MXCryptoError.Base( throw MXCryptoError.Base(
MXCryptoError.ErrorType.KEYS_WITHHELD, MXCryptoError.ErrorType.KEYS_WITHHELD,
@ -141,10 +120,6 @@ internal class MXMegolmDecryption(private val userId: String,
) )
} }
if (requestKeysOnFail) {
requestKeysForEvent(event, false)
}
throw MXCryptoError.Base( throw MXCryptoError.Base(
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
"UNKNOWN_MESSAGE_INDEX", "UNKNOWN_MESSAGE_INDEX",
@ -162,27 +137,22 @@ internal class MXMegolmDecryption(private val userId: String,
) )
} }
if (throwable is MXCryptoError.Base) { if (throwable is MXCryptoError.Base) {
if ( if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
/** if the session is unknown*/ // Check if it was withheld by sender to enrich error code
throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
) {
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId) val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
if (withHeldInfo != null) { if (withHeldInfo != null) {
if (requestKeysOnFail) { if (requestKeysOnFail) {
requestKeysForEvent(event, true) requestKeysForEvent(event)
} }
// Encapsulate as withHeld exception // Encapsulate as withHeld exception
throw MXCryptoError.Base( throw MXCryptoError.Base(
MXCryptoError.ErrorType.KEYS_WITHHELD, MXCryptoError.ErrorType.KEYS_WITHHELD,
withHeldInfo.code?.value ?: "", withHeldInfo.code?.value ?: "",
withHeldInfo.reason withHeldInfo.reason)
) }
} else {
// This is un-used in Matrix Android SDK2, not sure if needed if (requestKeysOnFail) {
// addEventToPendingList(event, timeline) requestKeysForEvent(event)
if (requestKeysOnFail) {
requestKeysForEvent(event, false)
}
} }
} }
} }
@ -198,55 +168,10 @@ internal class MXMegolmDecryption(private val userId: String,
* *
* @param event the event * @param event the event
*/ */
override fun requestKeysForEvent(event: Event, withHeld: Boolean) { private fun requestKeysForEvent(event: Event) {
val sender = event.senderId ?: return outgoingKeyRequestManager.requestKeyForEvent(event, false)
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
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)
} }
// /**
// * Add an event to the list of those we couldn't decrypt the first time we
// * saw them.
// *
// * @param event the event to try to decrypt later
// * @param timelineId the timeline identifier
// */
// private fun addEventToPendingList(event: Event, timelineId: String) {
// val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return
// val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}"
//
// val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() }
// val events = timeline.getOrPut(timelineId) { ArrayList() }
//
// if (event !in events) {
// Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}")
// events.add(event)
// }
// }
/** /**
* Handle a key event. * Handle a key event.
* *
@ -266,6 +191,11 @@ internal class MXMegolmDecryption(private val userId: String,
return return
} }
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { 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}") Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>() val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>()
?: return ?: return
@ -306,7 +236,7 @@ internal class MXMegolmDecryption(private val userId: String,
} }
Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}") Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
val added = olmDevice.addInboundGroupSession( val addSessionResult = olmDevice.addInboundGroupSession(
roomKeyContent.sessionId, roomKeyContent.sessionId,
roomKeyContent.sessionKey, roomKeyContent.sessionKey,
roomKeyContent.roomId, roomKeyContent.roomId,
@ -316,18 +246,47 @@ internal class MXMegolmDecryption(private val userId: String,
exportFormat 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() 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) 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") Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey")
newSessionListener?.onNewSession(roomId, senderKey, sessionId) 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<Any>()
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)
}
}
} }

View file

@ -17,46 +17,24 @@
package org.matrix.android.sdk.internal.crypto.algorithms.megolm package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import dagger.Lazy 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.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
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.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 org.matrix.android.sdk.internal.session.StreamEventsManager
import javax.inject.Inject import javax.inject.Inject
internal class MXMegolmDecryptionFactory @Inject constructor( internal class MXMegolmDecryptionFactory @Inject constructor(
@UserId private val userId: String,
private val olmDevice: MXOlmDevice, private val olmDevice: MXOlmDevice,
private val deviceListManager: DeviceListManager, private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
private val messageEncrypter: MessageEncrypter,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
private val sendToDeviceTask: SendToDeviceTask,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope,
private val eventsManager: Lazy<StreamEventsManager> private val eventsManager: Lazy<StreamEventsManager>
) { ) {
fun create(): MXMegolmDecryption { fun create(): MXMegolmDecryption {
return MXMegolmDecryption( return MXMegolmDecryption(
userId,
olmDevice, olmDevice,
deviceListManager, outgoingKeyRequestManager,
outgoingGossipingRequestManager,
messageEncrypter,
ensureOlmSessionsForDevicesAction,
cryptoStore, cryptoStore,
sendToDeviceTask, eventsManager)
coroutineDispatchers,
cryptoCoroutineScope,
eventsManager
)
} }
} }

View file

@ -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.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.forEach 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.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.EventType
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent 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.session.events.model.content.WithHeldCode
@ -285,25 +284,14 @@ internal class MXMegolmEncryption(
// attempted to share with) rather than the contentMap (those we did // 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 // share with), because we don't want to try to claim a one-time-key
// for dead devices on every message. // for dead devices on every message.
val gossipingEventBuffer = arrayListOf<Event>() for ((_, devicesToShareWith) in devicesByUser) {
for ((userId, devicesToShareWith) in devicesByUser) {
for (deviceInfo in devicesToShareWith) { for (deviceInfo in devicesToShareWith) {
session.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex) session.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex)
gossipingEventBuffer.add( // XXX is it needed to add it to the audit trail?
Event( // For now decided that no, we are more interested by forward trail
type = EventType.ROOM_KEY,
senderId = myUserId,
content = submap.apply {
this["session_key"] = ""
// we add a fake key for trail
this["_dest"] = "$userId|${deviceInfo.deviceId}"
}
))
} }
} }
cryptoStore.saveGossipingEvents(gossipingEventBuffer)
if (haveTargets) { if (haveTargets) {
t0 = clock.epochMillis() t0 = clock.epochMillis()
Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target") Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target")
@ -346,7 +334,8 @@ internal class MXMegolmEncryption(
senderKey = senderKey, senderKey = senderKey,
algorithm = MXCRYPTO_ALGORITHM_MEGOLM, algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
sessionId = sessionId, sessionId = sessionId,
codeString = code.value codeString = code.value,
fromDevice = myDeviceId
) )
val params = SendToDeviceTask.Params( val params = SendToDeviceTask.Params(
EventType.ROOM_KEY_WITHHELD, EventType.ROOM_KEY_WITHHELD,

View file

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

View file

@ -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.Optional
import org.matrix.android.sdk.api.util.fromBase64 import org.matrix.android.sdk.api.util.fromBase64
import org.matrix.android.sdk.internal.crypto.DeviceListManager 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.model.rest.UploadSignatureQueryBuilder
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
@ -70,6 +71,7 @@ internal class DefaultCrossSigningService @Inject constructor(
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope, private val cryptoCoroutineScope: CoroutineScope,
private val workManagerProvider: WorkManagerProvider, private val workManagerProvider: WorkManagerProvider,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
) : CrossSigningService, ) : CrossSigningService,
DeviceListManager.UserDevicesUpdateListener { DeviceListManager.UserDevicesUpdateListener {
@ -785,7 +787,8 @@ internal class DefaultCrossSigningService @Inject constructor(
// If it's me, recheck trust of all users and devices? // If it's me, recheck trust of all users and devices?
val users = ArrayList<String>() val users = ArrayList<String>()
if (otherUserId == userId && currentTrust != trusted) { if (otherUserId == userId && currentTrust != trusted) {
// reRequestAllPendingRoomKeyRequest() // notify key requester
outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted)
cryptoStore.updateUsersTrust { cryptoStore.updateUsersTrust {
users.add(it) users.add(it)
checkUserTrust(it).isVerified() 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)
// }
// }
// }
// }
// }
} }

View file

@ -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.model.rest.UpdateKeysBackupVersionBody
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask 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.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.GetKeysBackupLastVersionTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask 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.GetRoomSessionDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask 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.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.StoreSessionsDataTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
@ -112,16 +107,11 @@ internal class DefaultKeysBackupService @Inject constructor(
// Tasks // Tasks
private val createKeysBackupVersionTask: CreateKeysBackupVersionTask, private val createKeysBackupVersionTask: CreateKeysBackupVersionTask,
private val deleteBackupTask: DeleteBackupTask, private val deleteBackupTask: DeleteBackupTask,
private val deleteRoomSessionDataTask: DeleteRoomSessionDataTask,
private val deleteRoomSessionsDataTask: DeleteRoomSessionsDataTask,
private val deleteSessionDataTask: DeleteSessionsDataTask,
private val getKeysBackupLastVersionTask: GetKeysBackupLastVersionTask, private val getKeysBackupLastVersionTask: GetKeysBackupLastVersionTask,
private val getKeysBackupVersionTask: GetKeysBackupVersionTask, private val getKeysBackupVersionTask: GetKeysBackupVersionTask,
private val getRoomSessionDataTask: GetRoomSessionDataTask, private val getRoomSessionDataTask: GetRoomSessionDataTask,
private val getRoomSessionsDataTask: GetRoomSessionsDataTask, private val getRoomSessionsDataTask: GetRoomSessionsDataTask,
private val getSessionsDataTask: GetSessionsDataTask, private val getSessionsDataTask: GetSessionsDataTask,
private val storeRoomSessionDataTask: StoreRoomSessionDataTask,
private val storeSessionsDataTask: StoreRoomSessionsDataTask,
private val storeSessionDataTask: StoreSessionsDataTask, private val storeSessionDataTask: StoreSessionsDataTask,
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask, private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
// Task executor // Task executor
@ -168,58 +158,63 @@ internal class DefaultKeysBackupService @Inject constructor(
override fun prepareKeysBackupVersion(password: String?, override fun prepareKeysBackupVersion(password: String?,
progressListener: ProgressListener?, progressListener: ProgressListener?,
callback: MatrixCallback<MegolmBackupCreationInfo>) { callback: MatrixCallback<MegolmBackupCreationInfo>) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) { cryptoCoroutineScope.launch(coroutineDispatchers.io) {
runCatching { try {
withContext(coroutineDispatchers.crypto) { val olmPkDecryption = OlmPkDecryption()
val olmPkDecryption = OlmPkDecryption() val signalableMegolmBackupAuthData = if (password != null) {
val signalableMegolmBackupAuthData = if (password != null) { // Generate a private key from the password
// Generate a private key from the password val backgroundProgressListener = if (progressListener == null) {
val backgroundProgressListener = if (progressListener == null) { null
null } else {
} else { object : ProgressListener {
object : ProgressListener { override fun onProgress(progress: Int, total: Int) {
override fun onProgress(progress: Int, total: Int) { uiHandler.post {
uiHandler.post { try {
try { progressListener.onProgress(progress, total)
progressListener.onProgress(progress, total) } catch (e: Exception) {
} catch (e: Exception) { Timber.e(e, "prepareKeysBackupVersion: onProgress failure")
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 generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener)
SignalableMegolmBackupAuthData(
val signedMegolmBackupAuthData = MegolmBackupAuthData( publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey),
publicKey = signalableMegolmBackupAuthData.publicKey, privateKeySalt = generatePrivateKeyResult.salt,
privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt, privateKeyIterations = generatePrivateKeyResult.iterations
privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations,
signatures = objectSigner.signObject(canonicalJson)
) )
} else {
val publicKey = olmPkDecryption.generateKey()
MegolmBackupCreationInfo( SignalableMegolmBackupAuthData(
algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, publicKey = publicKey
authData = signedMegolmBackupAuthData,
recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey())
) )
} }
}.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<Unit>?) { override fun deleteBackup(version: String, callback: MatrixCallback<Unit>?) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) { cryptoCoroutineScope.launch(coroutineDispatchers.io) {
withContext(coroutineDispatchers.crypto) { // If we're currently backing up to this backup... stop.
// If we're currently backing up to this backup... stop. // (We start using it automatically in createKeysBackupVersion so this is symmetrical).
// (We start using it automatically in createKeysBackupVersion so this is symmetrical). if (keysBackupVersion != null && version == keysBackupVersion?.version) {
if (keysBackupVersion != null && version == keysBackupVersion?.version) { resetKeysBackupData()
resetKeysBackupData() keysBackupVersion = null
keysBackupVersion = null keysBackupStateManager.state = KeysBackupState.Unknown
keysBackupStateManager.state = KeysBackupState.Unknown }
}
deleteBackupTask deleteBackupTask
.configureWith(DeleteBackupTask.Params(version)) { .configureWith(DeleteBackupTask.Params(version)) {
this.callback = object : MatrixCallback<Unit> { this.callback = object : MatrixCallback<Unit> {
private fun eventuallyRestartBackup() { private fun eventuallyRestartBackup() {
// Do not stay in KeysBackupState.Unknown but check what is available on the homeserver // Do not stay in KeysBackupState.Unknown but check what is available on the homeserver
if (state == KeysBackupState.Unknown) { if (state == KeysBackupState.Unknown) {
checkAndStartKeysBackup() checkAndStartKeysBackup()
}
}
override fun onSuccess(data: Unit) {
eventuallyRestartBackup()
uiHandler.post { callback?.onSuccess(Unit) }
}
override fun onFailure(failure: Throwable) {
eventuallyRestartBackup()
uiHandler.post { callback?.onFailure(failure) }
} }
} }
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) { if (authData == null) {
Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data") Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data")
uiHandler.post {
callback.onFailure(IllegalArgumentException("Missing element")) callback.onFailure(IllegalArgumentException("Missing element"))
}
} else { } else {
cryptoCoroutineScope.launch(coroutineDispatchers.main) { cryptoCoroutineScope.launch(coroutineDispatchers.io) {
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) { val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
// Get current signatures, or create an empty set // Get current signatures, or create an empty set
val myUserSignatures = authData.signatures?.get(userId).orEmpty().toMutableMap() val myUserSignatures = authData.signatures?.get(userId).orEmpty().toMutableMap()
@ -536,11 +530,15 @@ internal class DefaultKeysBackupService @Inject constructor(
checkAndStartWithKeysBackupVersion(newKeysBackupVersion) checkAndStartWithKeysBackupVersion(newKeysBackupVersion)
callback.onSuccess(data) uiHandler.post {
callback.onSuccess(data)
}
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
callback.onFailure(failure) uiHandler.post {
callback.onFailure(failure)
}
} }
} }
} }
@ -554,15 +552,14 @@ internal class DefaultKeysBackupService @Inject constructor(
callback: MatrixCallback<Unit>) { callback: MatrixCallback<Unit>) {
Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}") Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}")
cryptoCoroutineScope.launch(coroutineDispatchers.main) { cryptoCoroutineScope.launch(coroutineDispatchers.io) {
val isValid = withContext(coroutineDispatchers.crypto) { val isValid = isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)
isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)
}
if (!isValid) { if (!isValid) {
Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.") Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.")
uiHandler.post {
callback.onFailure(IllegalArgumentException("Invalid recovery key or password")) callback.onFailure(IllegalArgumentException("Invalid recovery key or password"))
}
} else { } else {
trustKeysBackupVersion(keysBackupVersion, true, callback) trustKeysBackupVersion(keysBackupVersion, true, callback)
} }
@ -574,15 +571,14 @@ internal class DefaultKeysBackupService @Inject constructor(
callback: MatrixCallback<Unit>) { callback: MatrixCallback<Unit>) {
Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}") Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}")
cryptoCoroutineScope.launch(coroutineDispatchers.main) { cryptoCoroutineScope.launch(coroutineDispatchers.io) {
val recoveryKey = withContext(coroutineDispatchers.crypto) { val recoveryKey = recoveryKeyFromPassword(password, keysBackupVersion, null)
recoveryKeyFromPassword(password, keysBackupVersion, null)
}
if (recoveryKey == null) { if (recoveryKey == null) {
Timber.w("trustKeysBackupVersionWithPassphrase: Key backup is missing required data") Timber.w("trustKeysBackupVersionWithPassphrase: Key backup is missing required data")
uiHandler.post {
callback.onFailure(IllegalArgumentException("Missing element")) callback.onFailure(IllegalArgumentException("Missing element"))
}
} else { } else {
// Check trust using the recovery key // Check trust using the recovery key
trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback) trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback)
@ -593,30 +589,28 @@ internal class DefaultKeysBackupService @Inject constructor(
override fun onSecretKeyGossip(secret: String) { override fun onSecretKeyGossip(secret: String) {
Timber.i("## CrossSigning - onSecretKeyGossip") Timber.i("## CrossSigning - onSecretKeyGossip")
cryptoCoroutineScope.launch(coroutineDispatchers.main) { cryptoCoroutineScope.launch(coroutineDispatchers.io) {
try { try {
when (val keysBackupLastVersionResult = getKeysBackupLastVersionTask.execute(Unit)) { val keysBackupVersion = getKeysBackupLastVersionTask.execute(Unit).toKeysVersionResult()
KeysBackupLastVersionResult.NoKeysBackup -> { ?: return@launch Unit.also {
Timber.d("No keys backup found") Timber.d("Failed to get backup last version")
}
is KeysBackupLastVersionResult.KeysBackup -> {
val keysBackupVersion = keysBackupLastVersionResult.keysVersionResult
val recoveryKey = computeRecoveryKey(secret.fromBase64())
if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) {
awaitCallback<Unit> {
trustKeysBackupVersion(keysBackupVersion, true, it)
}
val importResult = awaitCallback<ImportRoomKeysResult> {
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 recoveryKey = computeRecoveryKey(secret.fromBase64())
if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) {
awaitCallback<Unit> {
trustKeysBackupVersion(keysBackupVersion, true, it)
} }
// we don't want to start immediately downloading all as it can take very long
// val importResult = awaitCallback<ImportRoomKeysResult> {
// 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) { } catch (failure: Throwable) {
Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}") Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}")
@ -679,9 +673,9 @@ internal class DefaultKeysBackupService @Inject constructor(
callback: MatrixCallback<ImportRoomKeysResult>) { callback: MatrixCallback<ImportRoomKeysResult>) {
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}") Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
cryptoCoroutineScope.launch(coroutineDispatchers.main) { cryptoCoroutineScope.launch(coroutineDispatchers.io) {
runCatching { runCatching {
val decryption = withContext(coroutineDispatchers.crypto) { val decryption = withContext(coroutineDispatchers.computation) {
// Check if the recovery is valid before going any further // Check if the recovery is valid before going any further
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) { if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version") Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
@ -754,7 +748,19 @@ internal class DefaultKeysBackupService @Inject constructor(
} }
result result
} }
}.foldToCallback(callback) }.foldToCallback(object : MatrixCallback<ImportRoomKeysResult> {
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<ImportRoomKeysResult>) { callback: MatrixCallback<ImportRoomKeysResult>) {
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}") Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
cryptoCoroutineScope.launch(coroutineDispatchers.main) { cryptoCoroutineScope.launch(coroutineDispatchers.io) {
runCatching { runCatching {
val progressListener = if (stepProgressListener != null) { val progressListener = if (stepProgressListener != null) {
object : ProgressListener { object : ProgressListener {
@ -791,7 +797,19 @@ internal class DefaultKeysBackupService @Inject constructor(
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, it) restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, it)
} }
} }
}.foldToCallback(callback) }.foldToCallback(object : MatrixCallback<ImportRoomKeysResult> {
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) { } else if (roomId != null) {
// Get all keys for the room // 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 // Convert to KeysBackupData
KeysBackupData(mutableMapOf(roomId to data)) KeysBackupData(mutableMapOf(roomId to data))
} else { } else {
// Get all keys // Get all keys
getSessionsDataTask.execute(GetSessionsDataTask.Params(version)) withContext(coroutineDispatchers.io) {
getSessionsDataTask.execute(GetSessionsDataTask.Params(version))
}
} }
} }

View file

@ -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.session.securestorage.SsssPassphrase
import org.matrix.android.sdk.api.util.fromBase64 import org.matrix.android.sdk.api.util.fromBase64
import org.matrix.android.sdk.api.util.toBase64NoPadding 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.keysbackup.generatePrivateKeyWithPassword
import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256 import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256
import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption
@ -57,7 +57,7 @@ import kotlin.experimental.and
internal class DefaultSharedSecretStorageService @Inject constructor( internal class DefaultSharedSecretStorageService @Inject constructor(
@UserId private val userId: String, @UserId private val userId: String,
private val accountDataService: SessionAccountDataService, private val accountDataService: SessionAccountDataService,
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val secretShareManager: SecretShareManager,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope private val cryptoCoroutineScope: CoroutineScope
) : SharedSecretStorageService { ) : SharedSecretStorageService {
@ -380,10 +380,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
return IntegrityResult.Success(keyInfo.content.passphrase != null) return IntegrityResult.Success(keyInfo.content.passphrase != null)
} }
override fun requestSecret(name: String, myOtherDeviceId: String) { override suspend fun requestSecret(name: String, myOtherDeviceId: String) {
outgoingGossipingRequestManager.sendSecretShareRequest( secretShareManager.requestSecretTo(myOtherDeviceId, name)
name,
mapOf(userId to listOf(myOtherDeviceId))
)
} }
} }

View file

@ -19,23 +19,22 @@ package org.matrix.android.sdk.internal.crypto.store
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.paging.PagedList import androidx.paging.PagedList
import org.matrix.android.sdk.api.session.crypto.NewSessionListener 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.CryptoCrossSigningKey
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo 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.crosssigning.PrivateKeysInfo
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo 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.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo 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.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.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.Event
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent 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.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.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper 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.model.OutboundGroupSessionWrapper
@ -82,6 +81,15 @@ internal interface IMXCryptoStore {
*/ */
fun setGlobalBlacklistUnverifiedDevices(block: Boolean) 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. * 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<String, Int> fun getDeviceTrackingStatuses(): Map<String, Int>
/**
* @return the pending IncomingRoomKeyRequest requests
*/
fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon>
fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?)
fun storeIncomingGossipingRequests(requests: List<IncomingShareRequestCommon>)
// fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest>
/** /**
* Indicate if the store contains data for the passed account. * Indicate if the store contains data for the passed account.
* *
@ -377,7 +373,9 @@ internal interface IMXCryptoStore {
* @param requestBody the request body * @param requestBody the request body
* @return an OutgoingRoomKeyRequest instance or null * @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<OutgoingKeyRequest>
/** /**
* Look for an existing outgoing room key request, and if none is found, add a new one. * 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 * @param request the request
* @return either the same instance as passed in, or the existing one. * @return either the same instance as passed in, or the existing one.
*/ */
fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>): OutgoingRoomKeyRequest? fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>, 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<String, List<String>>): 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<Event>) fun saveWithheldAuditTrail(
roomId: String,
sessionId: String,
senderKey: String,
algorithm: String,
code: WithHeldCode,
userId: String,
deviceId: String
)
fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) { fun saveForwardKeyAuditTrail(
updateGossipingRequestState( roomId: String,
requestUserId = request.userId, sessionId: String,
requestDeviceId = request.deviceId, senderKey: String,
requestId = request.requestId, algorithm: String,
state = state userId: String,
) deviceId: String,
} chainIndex: Long?
)
fun updateGossipingRequestState(requestUserId: String?, fun saveIncomingForwardKeyAuditTrail(
requestDeviceId: String?, roomId: String,
requestId: String?, sessionId: String,
state: GossipingRequestState) senderKey: String,
algorithm: String,
/** userId: String,
* Search an IncomingRoomKeyRequest deviceId: String,
* chainIndex: Long?
* @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 addNewSessionListener(listener: NewSessionListener) fun addNewSessionListener(listener: NewSessionListener)
@ -477,17 +495,15 @@ internal interface IMXCryptoStore {
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int> fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
// Dev tools // Dev tools
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest>
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>>
fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest> fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>>
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? fun <T> getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData<PagedList<T>>
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> fun getGossipingEvents(): List<AuditTrail>
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
fun getGossipingEvents(): List<Event>
fun setDeviceKeysUploaded(uploaded: Boolean) fun setDeviceKeysUploaded(uploaded: Boolean)
fun areDeviceKeysUploaded(): Boolean fun areDeviceKeysUploaded(): Boolean
fun tidyUpDataBase() fun tidyUpDataBase()
fun logDbUsageInfo() fun logDbUsageInfo()
fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest>
} }

View file

@ -24,37 +24,40 @@ import com.zhuinden.monarchy.Monarchy
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.Sort import io.realm.Sort
import io.realm.kotlin.createObject
import io.realm.kotlin.where import io.realm.kotlin.where
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.extensions.tryOrNull 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.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.CryptoCrossSigningKey
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo 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.crosssigning.PrivateKeysInfo
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo 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.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo 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.ForwardInfo
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest import org.matrix.android.sdk.api.session.crypto.model.IncomingKeyRequestInfo
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.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.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.Event
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent 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.Optional
import org.matrix.android.sdk.api.util.toOptional 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.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper 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.model.OutboundGroupSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore 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.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.CrossSigningInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper 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.CryptoRoomEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity 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.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.KeysBackupDataEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity 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.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.OlmSessionEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields 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.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.OutgoingGossipingRequestEntityFields 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.SharedSessionEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity 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.getById
import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate 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.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.database.tools.RealmDebugTools
import org.matrix.android.sdk.internal.di.CryptoDatabase import org.matrix.android.sdk.internal.di.CryptoDatabase
import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.DeviceId
@ -106,6 +104,8 @@ import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
private val loggerTag = LoggerTag("RealmCryptoStore", LoggerTag.CRYPTO)
@SessionScope @SessionScope
internal class RealmCryptoStore @Inject constructor( internal class RealmCryptoStore @Inject constructor(
@CryptoDatabase private val realmConfiguration: RealmConfiguration, @CryptoDatabase private val realmConfiguration: RealmConfiguration,
@ -273,12 +273,13 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? { override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? {
return doWithRealm(realmConfiguration) { return doWithRealm(realmConfiguration) { realm ->
it.where<DeviceInfoEntity>() realm.where<DeviceInfoEntity>()
.equalTo(DeviceInfoEntityFields.IDENTITY_KEY, identityKey) .contains(DeviceInfoEntityFields.KEYS_MAP_JSON, identityKey)
.findFirst() .findAll()
?.let { deviceInfo -> .mapNotNull { CryptoMapper.mapToModel(it) }
CryptoMapper.mapToModel(deviceInfo) .firstOrNull {
it.identityKey() == identityKey
} }
} }
} }
@ -743,14 +744,23 @@ internal class RealmCryptoStore @Inject constructor(
if (sessionIdentifier != null) { if (sessionIdentifier != null) {
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey) val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey)
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { val existing = realm.where<OlmInboundGroupSessionEntity>()
primaryKey = key .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
sessionId = sessionIdentifier .findFirst()
senderKey = session.senderKey
putInboundGroupSession(session)
}
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 return
} }
doRealmTransaction(realmConfiguration) { doRealmTransaction(realmConfiguration) { realm ->
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
try { try {
val sessionIdentifier = olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier()
val key = OlmInboundGroupSessionEntity.createPrimaryKey( val key = OlmInboundGroupSessionEntity.createPrimaryKey(
olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier(), sessionIdentifier,
olmInboundGroupSessionWrapper.senderKey olmInboundGroupSessionWrapper.senderKey
) )
it.where<OlmInboundGroupSessionEntity>() val existing = realm.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
.findFirst() .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) { } catch (e: OlmException) {
Timber.e(e, "OlmException") Timber.e(e, "OlmException")
} }
@ -929,6 +954,18 @@ internal class RealmCryptoStore @Inject constructor(
} }
} }
override fun enableKeyGossiping(enable: Boolean) {
doRealmTransaction(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.globalEnableKeyGossiping = enable
}
}
override fun isKeyGossipingEnabled(): Boolean {
return doWithRealm(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.globalEnableKeyGossiping
} ?: true
}
override fun getGlobalBlacklistUnverifiedDevices(): Boolean { override fun getGlobalBlacklistUnverifiedDevices(): Boolean {
return doWithRealm(realmConfiguration) { return doWithRealm(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.globalBlacklistUnverifiedDevices it.where<CryptoMetadataEntity>().findFirst()?.globalBlacklistUnverifiedDevices
@ -1010,12 +1047,13 @@ internal class RealmCryptoStore @Inject constructor(
?: defaultValue ?: defaultValue
} }
override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? { override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest? {
return monarchy.fetchAllCopiedSync { realm -> return monarchy.fetchAllCopiedSync { realm ->
realm.where<OutgoingGossipingRequestEntity>() realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId)
}.mapNotNull { .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId)
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest }.map {
it.toOutgoingKeyRequest()
}.firstOrNull { }.firstOrNull {
it.requestBody?.algorithm == requestBody.algorithm && it.requestBody?.algorithm == requestBody.algorithm &&
it.requestBody?.roomId == requestBody.roomId && 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 -> return monarchy.fetchAllCopiedSync { realm ->
realm.where<OutgoingGossipingRequestEntity>() realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
.equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName) }.map {
}.mapNotNull { it.toOutgoingKeyRequest()
it.toOutgoingGossipingRequest() as? OutgoingSecretRequest
}.firstOrNull() }.firstOrNull()
} }
override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> { override fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List<OutgoingKeyRequest> {
// TODO this annoying we have to load all
return monarchy.fetchAllCopiedSync { realm -> return monarchy.fetchAllCopiedSync { realm ->
realm.where<IncomingGossipingRequestEntity>() realm.where<OutgoingKeyRequestEntity>()
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId)
}.mapNotNull { .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId)
it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest }.map {
it.toOutgoingKeyRequest()
}.filter {
it.requestBody?.algorithm == algorithm &&
it.requestBody?.senderKey == senderKey
} }
} }
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> { override fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>> {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
realm.where<IncomingGossipingRequestEntity>() realm.where<AuditTrailEntity>().sort(AuditTrailEntityFields.AGE_LOCAL_TS, Sort.DESCENDING)
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
.sort(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Sort.DESCENDING)
} }
val dataSourceFactory = realmDataSourceFactory.map { val dataSourceFactory = realmDataSourceFactory.map {
it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest AuditTrailMapper.map(it)
?: IncomingRoomKeyRequest( // mm we can't map not null...
requestBody = null, ?: createUnknownTrail()
deviceId = "",
userId = "",
requestId = "",
state = GossipingRequestState.NONE,
localCreationTimestamp = 0
)
} }
return monarchy.findAllPagedWithChanges( return monarchy.findAllPagedWithChanges(
realmDataSourceFactory, realmDataSourceFactory,
@ -1073,12 +1107,33 @@ internal class RealmCryptoStore @Inject constructor(
) )
} }
override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> { private fun createUnknownTrail() = AuditTrail(
clock.epochMillis(),
TrailType.Unknown,
IncomingKeyRequestInfo(
"",
"",
"",
"",
"",
"",
"",
)
)
override fun <T> getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData<PagedList<T>> {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
realm.where<GossipingEventEntity>().sort(GossipingEventEntityFields.AGE_LOCAL_TS, Sort.DESCENDING) realm.where<AuditTrailEntity>()
.equalTo(AuditTrailEntityFields.TYPE, type.name)
.sort(AuditTrailEntityFields.AGE_LOCAL_TS, Sort.DESCENDING)
} }
val dataSourceFactory = realmDataSourceFactory.map { it.toModel() } val dataSourceFactory = realmDataSourceFactory.map { entity ->
val trail = monarchy.findAllPagedWithChanges( (AuditTrailMapper.map(entity)
// mm we can't map not null...
?: createUnknownTrail()
).let { mapper.invoke(it) }
}
return monarchy.findAllPagedWithChanges(
realmDataSourceFactory, realmDataSourceFactory,
LivePagedListBuilder( LivePagedListBuilder(
dataSourceFactory, dataSourceFactory,
@ -1089,28 +1144,36 @@ internal class RealmCryptoStore @Inject constructor(
.build() .build()
) )
) )
return trail
} }
override fun getGossipingEvents(): List<Event> { override fun getGossipingEvents(): List<AuditTrail> {
return monarchy.fetchAllCopiedSync { realm -> return monarchy.fetchAllCopiedSync { realm ->
realm.where<GossipingEventEntity>() realm.where<AuditTrailEntity>()
}.map { }.mapNotNull {
it.toModel() AuditTrailMapper.map(it)
} }
} }
override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>): OutgoingRoomKeyRequest? { override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody,
recipients: Map<String, List<String>>,
fromIndex: Int): OutgoingKeyRequest {
// Insert the request and return the one passed in parameter // Insert the request and return the one passed in parameter
var request: OutgoingRoomKeyRequest? = null lateinit var request: OutgoingKeyRequest
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction(realmConfiguration) { realm ->
val existing = realm.where<OutgoingGossipingRequestEntity>() val existing = realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId)
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId)
.findAll() .findAll()
.mapNotNull { .map {
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest it.toOutgoingKeyRequest()
}.firstOrNull { }.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?.algorithm == requestBody.algorithm &&
it.requestBody?.sessionId == requestBody.sessionId && it.requestBody?.sessionId == requestBody.sessionId &&
it.requestBody?.senderKey == requestBody.senderKey && it.requestBody?.senderKey == requestBody.senderKey &&
@ -1118,13 +1181,14 @@ internal class RealmCryptoStore @Inject constructor(
} }
if (existing == null) { if (existing == null) {
request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply { request = realm.createObject(OutgoingKeyRequestEntity::class.java).apply {
this.requestId = RequestIdHelper.createUniqueRequestId() this.requestId = RequestIdHelper.createUniqueRequestId()
this.setRecipients(recipients) this.setRecipients(recipients)
this.requestState = OutgoingGossipingRequestState.UNSENT this.requestedIndex = fromIndex
this.type = GossipRequestType.KEY this.requestState = OutgoingRoomKeyRequestState.UNSENT
this.requestedInfoStr = requestBody.toJson() this.setRequestBody(requestBody)
}.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest this.creationTimeStamp = clock.epochMillis()
}.toOutgoingKeyRequest()
} else { } else {
request = existing request = existing
} }
@ -1132,284 +1196,175 @@ internal class RealmCryptoStore @Inject constructor(
return request return request
} }
override fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest? { override fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) {
var request: OutgoingSecretRequest? = null
// Insert the request and return the one passed in parameter
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction(realmConfiguration) { realm ->
val existing = realm.where<OutgoingGossipingRequestEntity>() realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
.equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName) .findFirst()?.apply {
.findAll() this.requestState = newState
.mapNotNull { if (newState == OutgoingRoomKeyRequestState.UNSENT) {
it.toOutgoingGossipingRequest() as? OutgoingSecretRequest // clear the old replies
}.firstOrNull() this.replies.deleteAllFromRealm()
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<Event>) {
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<ShareRequestState>): OutgoingRoomKeyRequest? {
// val statesIndex = states.map { it.ordinal }.toTypedArray()
// return doRealmQueryAndCopy(realmConfiguration) { realm ->
// realm.where<GossipingEventEntity>()
// .equalTo(GossipingEventEntityFields.SENDER, credentials.userId)
// .findAll()
// .filter {entity ->
// states.any { it == entity.requestState}
// }
// }.mapNotNull {
// ContentMapper.map(it.content)?.toModel<OutgoingSecretRequest>()
// }
// ?.toOutgoingRoomKeyRequest()
// }
//
// override fun getOutgoingSecretShareRequestByState(states: Set<ShareRequestState>): OutgoingSecretRequest? {
// val statesIndex = states.map { it.ordinal }.toTypedArray()
// return doRealmQueryAndCopy(realmConfiguration) {
// it.where<OutgoingSecretRequestEntity>()
// .`in`(OutgoingSecretRequestEntityFields.STATE, statesIndex)
// .findFirst()
// }
// ?.toOutgoingSecretRequest()
// }
// override fun 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<OutgoingRoomKeyRequestEntity>()
// .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<IncomingRoomKeyRequestEntity>()
// .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId)
// .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId)
// .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId)
// .findAll()
// .deleteAllFromRealm()
//
// // Then store it
// it.createObject(IncomingRoomKeyRequestEntity::class.java).apply {
// userId = incomingRoomKeyRequest.userId
// deviceId = incomingRoomKeyRequest.deviceId
// requestId = incomingRoomKeyRequest.requestId
// putRequestBody(incomingRoomKeyRequest.requestBody)
// }
// }
// }
// override fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingShareRequestCommon) {
// doRealmTransaction(realmConfiguration) {
// it.where<GossipingEventEntity>()
// .equalTo(GossipingEventEntityFields.TYPE, EventType.ROOM_KEY_REQUEST)
// .notEqualTo(GossipingEventEntityFields.SENDER, credentials.userId)
// .findAll()
// .filter {
// ContentMapper.map(it.content).toModel<IncomingRoomKeyRequest>()?.let {
//
// }
// }
// // .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId)
// // .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId)
// // .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId)
// // .findAll()
// // .deleteAllFromRealm()
// }
// }
override fun updateGossipingRequestState(requestUserId: String?,
requestDeviceId: String?,
requestId: String?,
state: GossipingRequestState) {
doRealmTransaction(realmConfiguration) { realm ->
realm.where<IncomingGossipingRequestEntity>()
.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<OutgoingGossipingRequestEntity>()
.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<IncomingGossipingRequestEntity>()
.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<IncomingRoomKeyRequest> {
return doWithRealm(realmConfiguration) {
it.where<IncomingGossipingRequestEntity>()
.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<IncomingShareRequestCommon> {
return doWithRealm(realmConfiguration) {
it.where<IncomingGossipingRequestEntity>()
.equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name)
.findAll()
.mapNotNull { entity ->
when (entity.type) {
GossipRequestType.KEY -> {
IncomingRoomKeyRequest(
userId = entity.otherUserId,
deviceId = entity.otherDeviceId,
requestId = entity.requestId,
requestBody = entity.getRequestedKeyInfo(),
localCreationTimestamp = entity.localCreationTimestamp
)
}
GossipRequestType.SECRET -> {
IncomingSecretShareRequest(
userId = entity.otherUserId,
deviceId = entity.otherDeviceId,
requestId = entity.requestId,
secretName = entity.getRequestedSecretName(),
localCreationTimestamp = entity.localCreationTimestamp
)
}
} }
} }
} }
} }
override fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) { override fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) {
doRealmTransactionAsync(realmConfiguration) { realm -> doRealmTransaction(realmConfiguration) { realm ->
realm.where<OutgoingKeyRequestEntity>()
// After a clear cache, we might have a .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
.findFirst()?.apply {
realm.createObject(IncomingGossipingRequestEntity::class.java).let { this.requestedIndex = newIndex
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<IncomingShareRequestCommon>) {
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 updateOutgoingRoomKeyReply(roomId: String,
sessionId: String,
algorithm: String,
senderKey: String,
fromDevice: String?,
event: Event) {
doRealmTransaction(realmConfiguration) { realm ->
realm.where<OutgoingKeyRequestEntity>()
.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<OutgoingKeyRequestEntity>()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
.findFirst()?.deleteOnCascade()
}
}
override fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) {
doRealmTransaction(realmConfiguration) { realm ->
realm.where<OutgoingKeyRequestEntity>()
.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<AuditTrailEntity>().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<IncomingSecretShareRequest> { override fun saveWithheldAuditTrail(roomId: String,
// return doRealmQueryAndCopyList(realmConfiguration) { sessionId: String,
// it.where<GossipingEventEntity>() senderKey: String,
// .findAll() algorithm: String,
// }.map { code: WithHeldCode,
// it.toIncomingSecretShareRequest() userId: String,
// } deviceId: String) {
// } monarchy.writeAsync { realm ->
val now = clock.epochMillis()
realm.createObject<AuditTrailEntity>().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<AuditTrailEntity>().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 * Cross Signing
@ -1513,37 +1468,34 @@ internal class RealmCryptoStore @Inject constructor(
} }
} }
override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> { override fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest> {
return monarchy.fetchAllMappedSync({ realm -> return monarchy.fetchAllMappedSync({ realm ->
realm realm
.where(OutgoingGossipingRequestEntity::class.java) .where(OutgoingKeyRequestEntity::class.java)
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
}, { entity -> }, { entity ->
entity.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest entity.toOutgoingKeyRequest()
}) })
.filterNotNull() .filterNotNull()
} }
override fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest> { override fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest> {
return monarchy.fetchAllMappedSync({ realm -> return monarchy.fetchAllMappedSync({ realm ->
realm realm
.where(OutgoingGossipingRequestEntity::class.java) .where(OutgoingKeyRequestEntity::class.java)
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name) .`in`(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, inStates.map { it.name }.toTypedArray())
}, { entity -> }, { entity ->
entity.toOutgoingGossipingRequest() as? OutgoingSecretRequest entity.toOutgoingKeyRequest()
}) })
.filterNotNull() .filterNotNull()
} }
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> { override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>> {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
realm realm
.where(OutgoingGossipingRequestEntity::class.java) .where(OutgoingKeyRequestEntity::class.java)
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
} }
val dataSourceFactory = realmDataSourceFactory.map { val dataSourceFactory = realmDataSourceFactory.map {
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest it.toOutgoingKeyRequest()
?: OutgoingRoomKeyRequest(requestBody = null, requestId = "?", recipients = emptyMap(), state = OutgoingGossipingRequestState.CANCELLED)
} }
val trail = monarchy.findAllPagedWithChanges( val trail = monarchy.findAllPagedWithChanges(
realmDataSourceFactory, realmDataSourceFactory,
@ -1722,26 +1674,20 @@ internal class RealmCryptoStore @Inject constructor(
val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000 val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction(realmConfiguration) { realm ->
// Only keep one week history // Clean the old ones?
realm.where<IncomingGossipingRequestEntity>() realm.where<OutgoingKeyRequestEntity>()
.lessThan(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, prevWeekTs) .lessThan(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, prevWeekTs)
.findAll() .findAll()
.also { Timber.i("## Crypto Clean up ${it.size} IncomingGossipingRequestEntity") } .also { Timber.i("## Crypto Clean up ${it.size} OutgoingKeyRequestEntity") }
.deleteAllFromRealm() .deleteAllFromRealm()
// Clean the cancelled ones? // Only keep one month history
realm.where<OutgoingGossipingRequestEntity>()
.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 week history val prevMonthTs = clock.epochMillis() - 4 * 7 * 24 * 60 * 60 * 1_000L
realm.where<GossipingEventEntity>() realm.where<AuditTrailEntity>()
.lessThan(GossipingEventEntityFields.AGE_LOCAL_TS, prevWeekTs) .lessThan(AuditTrailEntityFields.AGE_LOCAL_TS, prevMonthTs)
.findAll() .findAll()
.also { Timber.i("## Crypto Clean up ${it.size} GossipingEventEntityFields") } .also { Timber.i("## Crypto Clean up ${it.size} AuditTrailEntity") }
.deleteAllFromRealm() .deleteAllFromRealm()
// Can we do something for WithHeldSessionEntity? // Can we do something for WithHeldSessionEntity?

View file

@ -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.MigrateCryptoTo013
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014 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.MigrateCryptoTo015
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016
import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -50,7 +51,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(
// 0, 1, 2: legacy Riot-Android // 0, 1, 2: legacy Riot-Android
// 3: migrate to RiotX schema // 3: migrate to RiotX schema
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6) // 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) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion") 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 < 13) MigrateCryptoTo013(realm).perform()
if (oldVersion < 14) MigrateCryptoTo014(realm).perform() if (oldVersion < 14) MigrateCryptoTo014(realm).perform()
if (oldVersion < 15) MigrateCryptoTo015(realm).perform() if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
if (oldVersion < 16) MigrateCryptoTo016(realm).perform()
} }
} }

View file

@ -17,19 +17,19 @@
package org.matrix.android.sdk.internal.crypto.store.db package org.matrix.android.sdk.internal.crypto.store.db
import io.realm.annotations.RealmModule 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.CrossSigningInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity 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.CryptoRoomEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity 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.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.KeysBackupDataEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity 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.OlmInboundGroupSessionEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity 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.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.SharedSessionEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity 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, KeyInfoEntity::class,
CrossSigningInfoEntity::class, CrossSigningInfoEntity::class,
TrustLevelEntity::class, TrustLevelEntity::class,
GossipingEventEntity::class, AuditTrailEntity::class,
IncomingGossipingRequestEntity::class, OutgoingKeyRequestEntity::class,
OutgoingGossipingRequestEntity::class, KeyRequestReplyEntity::class,
MyDeviceLastSeenInfoEntity::class, MyDeviceLastSeenInfoEntity::class,
WithHeldSessionEntity::class, WithHeldSessionEntity::class,
SharedSessionEntity::class, SharedSessionEntity::class,

View file

@ -17,9 +17,6 @@
package org.matrix.android.sdk.internal.crypto.store.db.migration package org.matrix.android.sdk.internal.crypto.store.db.migration
import io.realm.DynamicRealm 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 import org.matrix.android.sdk.internal.util.database.RealmMigrator
internal class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) { internal class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) {
@ -29,38 +26,37 @@ internal class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5)
realm.schema.remove("IncomingRoomKeyRequestEntity") realm.schema.remove("IncomingRoomKeyRequestEntity")
// Not need to migrate existing request, just start fresh? // Not need to migrate existing request, just start fresh?
realm.schema.create("GossipingEventEntity") realm.schema.create("GossipingEventEntity")
.addField(GossipingEventEntityFields.TYPE, String::class.java) .addField("type", String::class.java)
.addIndex(GossipingEventEntityFields.TYPE) .addIndex("type")
.addField(GossipingEventEntityFields.CONTENT, String::class.java) .addField("content", String::class.java)
.addField(GossipingEventEntityFields.SENDER, String::class.java) .addField("sender", String::class.java)
.addIndex(GossipingEventEntityFields.SENDER) .addIndex("sender")
.addField(GossipingEventEntityFields.DECRYPTION_RESULT_JSON, String::class.java) .addField("decryptionResultJson", String::class.java)
.addField(GossipingEventEntityFields.DECRYPTION_ERROR_CODE, String::class.java) .addField("decryptionErrorCode", String::class.java)
.addField(GossipingEventEntityFields.AGE_LOCAL_TS, Long::class.java) .addField("ageLocalTs", Long::class.java)
.setNullable(GossipingEventEntityFields.AGE_LOCAL_TS, true) .setNullable("ageLocalTs", true)
.addField(GossipingEventEntityFields.SEND_STATE_STR, String::class.java) .addField("sendStateStr", String::class.java)
realm.schema.create("IncomingGossipingRequestEntity") realm.schema.create("IncomingGossipingRequestEntity")
.addField(IncomingGossipingRequestEntityFields.REQUEST_ID, String::class.java) .addField("requestId", String::class.java)
.addIndex(IncomingGossipingRequestEntityFields.REQUEST_ID) .addIndex("requestId")
.addField(IncomingGossipingRequestEntityFields.TYPE_STR, String::class.java) .addField("typeStr", String::class.java)
.addIndex(IncomingGossipingRequestEntityFields.TYPE_STR) .addIndex("typeStr")
.addField(IncomingGossipingRequestEntityFields.OTHER_USER_ID, String::class.java) .addField("otherUserId", String::class.java)
.addField(IncomingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java) .addField("requestedInfoStr", String::class.java)
.addField(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, String::class.java) .addField("otherDeviceId", String::class.java)
.addField(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java) .addField("requestStateStr", String::class.java)
.addField(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Long::class.java) .addField("localCreationTimestamp", Long::class.java)
.setNullable(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, true) .setNullable("localCreationTimestamp", true)
realm.schema.create("OutgoingGossipingRequestEntity") realm.schema.create("OutgoingGossipingRequestEntity")
.addField(OutgoingGossipingRequestEntityFields.REQUEST_ID, String::class.java) .addField("requestId", String::class.java)
.addIndex(OutgoingGossipingRequestEntityFields.REQUEST_ID) .addIndex("requestId")
.addField(OutgoingGossipingRequestEntityFields.RECIPIENTS_DATA, String::class.java) .addField("recipientsData", String::class.java)
.addField(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java) .addField("requestedInfoStr", String::class.java)
.addField(OutgoingGossipingRequestEntityFields.TYPE_STR, String::class.java) .addField("typeStr", String::class.java)
.addIndex(OutgoingGossipingRequestEntityFields.TYPE_STR) .addIndex("typeStr")
.addField(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java) .addField("requestStateStr", String::class.java)
} }
} }

View file

@ -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)
}
}
}

View file

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,18 +14,15 @@
* limitations under the License. * 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 { import io.realm.RealmObject
NONE, import io.realm.annotations.Index
PENDING,
REJECTED,
ACCEPTING,
ACCEPTED,
FAILED_TO_ACCEPTED,
// USER_REJECTED, internal open class AuditTrailEntity(
UNABLE_TO_PROCESS, var ageLocalTs: Long? = null,
CANCELLED_BY_REQUESTER, @Index var type: String? = null,
RE_REQUESTED var contentJson: String? = null
) : RealmObject() {
companion object
} }

View file

@ -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
)
}
}
}
}

View file

@ -33,6 +33,8 @@ internal open class CryptoMetadataEntity(
var deviceSyncToken: String? = null, var deviceSyncToken: String? = null,
// Settings for blacklisting unverified devices. // Settings for blacklisting unverified devices.
var globalBlacklistUnverifiedDevices: Boolean = false, 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. // The keys backup version currently used. Null means no backup.
var backupVersion: String? = null, var backupVersion: String? = null,

View file

@ -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>(OlmDecryptionResult::class.java)
decryptionResultJson = adapter.toJson(decryptionResult)
decryptionErrorCode = null
}
fun toModel(): Event {
return Event(
type = this.type ?: "",
content = ContentMapper.map(this.content),
senderId = this.sender
).also {
it.ageLocalTs = this.ageLocalTs
it.sendState = this.sendState
this.decryptionResultJson?.let { json ->
try {
it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json)
} catch (t: JsonDataException) {
Timber.e(t, "Failed to parse decryption result")
}
}
// TODO get the full crypto error object
it.mCryptoError = this.decryptionErrorCode?.let { errorCode ->
MXCryptoError.ErrorType.valueOf(errorCode)
}
}
}
}

View file

@ -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
)
}
}
}
}

View file

@ -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)
}
}
}

View file

@ -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<Map<String, List<String>>> =
MoshiProvider
.providesMoshi()
.adapter<Map<String, List<String>>>(
Types.newParameterizedType(Map::class.java, String::class.java, List::class.java)
)
}
fun toOutgoingGossipingRequest(): OutgoingGossipingRequest {
return when (type) {
GossipRequestType.KEY -> {
OutgoingRoomKeyRequest(
requestBody = getRequestedKeyInfo(),
recipients = getRecipients().orEmpty(),
requestId = requestId ?: "",
state = requestState
)
}
GossipRequestType.SECRET -> {
OutgoingSecretRequest(
secretName = getRequestedSecretName(),
recipients = getRecipients().orEmpty(),
requestId = requestId ?: "",
state = requestState
)
}
}
}
private fun getRecipients(): Map<String, List<String>>? {
return this.recipientsData?.let { recipientsDataMapper.fromJson(it) }
}
fun setRecipients(recipients: Map<String, List<String>>) {
this.recipientsData = recipientsDataMapper.toJson(recipients)
}
}

View file

@ -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<KeyRequestReplyEntity> = RealmList()
) : RealmObject() {
@Index private var requestStateStr: String = OutgoingRoomKeyRequestState.UNSENT.name
companion object {
private val recipientsDataMapper: JsonAdapter<Map<String, List<String>>> =
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<String, List<String>>? {
return this.recipientsData?.let { recipientsDataMapper.fromJson(it) }
}
fun setRecipients(recipients: Map<String, List<String>>) {
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<RoomKeyWithHeldContent>()?.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()
}

View file

@ -15,6 +15,7 @@
*/ */
package org.matrix.android.sdk.internal.crypto.tasks 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.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
@ -39,7 +40,7 @@ internal interface EncryptEventTask : Task<EncryptEventTask.Params, Event> {
internal class DefaultEncryptEventTask @Inject constructor( internal class DefaultEncryptEventTask @Inject constructor(
private val localEchoRepository: LocalEchoRepository, private val localEchoRepository: LocalEchoRepository,
private val cryptoService: CryptoService private val cryptoService: Lazy<CryptoService>
) : EncryptEventTask { ) : EncryptEventTask {
override suspend fun execute(params: EncryptEventTask.Params): Event { override suspend fun execute(params: EncryptEventTask.Params): Event {
// don't want to wait for any query // don't want to wait for any query
@ -59,7 +60,7 @@ internal class DefaultEncryptEventTask @Inject constructor(
// try { // try {
// let it throws // let it throws
awaitCallback<MXEncryptEventContentResult> { awaitCallback<MXEncryptEventContentResult> {
cryptoService.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) cryptoService.get().encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
}.let { result -> }.let { result ->
val modifiedContent = HashMap(result.eventContent) val modifiedContent = HashMap(result.eventContent)
params.keepKeys?.forEach { toKeep -> params.keepKeys?.forEach { toKeep ->
@ -80,7 +81,7 @@ internal class DefaultEncryptEventTask @Inject constructor(
).toContent(), ).toContent(),
forwardingCurve25519KeyChain = emptyList(), forwardingCurve25519KeyChain = emptyList(),
senderCurve25519Key = result.eventContent["sender_key"] as? String, senderCurve25519Key = result.eventContent["sender_key"] as? String,
claimedEd25519Key = cryptoService.getMyDevice().fingerprint() claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint()
) )
} else { } else {
null null

View file

@ -15,6 +15,7 @@
*/ */
package org.matrix.android.sdk.internal.crypto.tasks 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.events.model.Event
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
@ -46,7 +47,9 @@ internal class DefaultSendEventTask @Inject constructor(
params.event.roomId params.event.roomId
?.takeIf { params.encrypt } ?.takeIf { params.encrypt }
?.let { roomId -> ?.let { roomId ->
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) tryOrNull {
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
}
} }
val event = handleEncryption(params) val event = handleEncryption(params)

View file

@ -47,7 +47,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
localEchoRepository.updateSendState(localId, event.roomId, SendState.SENDING) localEchoRepository.updateSendState(localId, event.roomId, SendState.SENDING)
val response = executeRequest(globalErrorReceiver) { val response = executeRequest(globalErrorReceiver) {
roomAPI.send( roomAPI.send(
localId, txId = localId,
roomId = event.roomId ?: "", roomId = event.roomId ?: "",
content = event.content, content = event.content,
eventType = event.type ?: "" eventType = event.type ?: ""

View file

@ -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.SasMode
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState 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.session.events.model.EventType
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager import org.matrix.android.sdk.internal.crypto.SecretShareManager
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import timber.log.Timber import timber.log.Timber
@ -35,8 +35,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
override val deviceId: String?, override val deviceId: String?,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
crossSigningService: CrossSigningService, crossSigningService: CrossSigningService,
outgoingGossipingRequestManager: OutgoingGossipingRequestManager, outgoingKeyRequestManager: OutgoingKeyRequestManager,
incomingGossipingRequestManager: IncomingGossipingRequestManager, secretShareManager: SecretShareManager,
deviceFingerprint: String, deviceFingerprint: String,
transactionId: String, transactionId: String,
otherUserID: String, otherUserID: String,
@ -47,8 +47,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
deviceId, deviceId,
cryptoStore, cryptoStore,
crossSigningService, crossSigningService,
outgoingGossipingRequestManager, outgoingKeyRequestManager,
incomingGossipingRequestManager, secretShareManager,
deviceFingerprint, deviceFingerprint,
transactionId, transactionId,
otherUserID, otherUserID,

View file

@ -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.OutgoingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState 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.session.events.model.EventType
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager import org.matrix.android.sdk.internal.crypto.SecretShareManager
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import timber.log.Timber import timber.log.Timber
@ -32,8 +32,8 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
deviceId: String?, deviceId: String?,
cryptoStore: IMXCryptoStore, cryptoStore: IMXCryptoStore,
crossSigningService: CrossSigningService, crossSigningService: CrossSigningService,
outgoingGossipingRequestManager: OutgoingGossipingRequestManager, outgoingKeyRequestManager: OutgoingKeyRequestManager,
incomingGossipingRequestManager: IncomingGossipingRequestManager, secretShareManager: SecretShareManager,
deviceFingerprint: String, deviceFingerprint: String,
transactionId: String, transactionId: String,
otherUserId: String, otherUserId: String,
@ -44,8 +44,8 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
deviceId, deviceId,
cryptoStore, cryptoStore,
crossSigningService, crossSigningService,
outgoingGossipingRequestManager, outgoingKeyRequestManager,
incomingGossipingRequestManager, secretShareManager,
deviceFingerprint, deviceFingerprint,
transactionId, transactionId,
otherUserId, otherUserId,

View file

@ -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.MessageVerificationStartContent
import org.matrix.android.sdk.api.session.room.model.message.ValidVerificationDone 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.DeviceListManager
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder 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.actions.SetDeviceVerificationAction
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel
@ -95,8 +95,8 @@ internal class DefaultVerificationService @Inject constructor(
@UserId private val userId: String, @UserId private val userId: String,
@DeviceId private val deviceId: String?, @DeviceId private val deviceId: String?,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val incomingGossipingRequestManager: IncomingGossipingRequestManager, private val secretShareManager: SecretShareManager,
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>, private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
private val deviceListManager: DeviceListManager, private val deviceListManager: DeviceListManager,
private val setDeviceVerificationAction: SetDeviceVerificationAction, private val setDeviceVerificationAction: SetDeviceVerificationAction,
@ -551,8 +551,8 @@ internal class DefaultVerificationService @Inject constructor(
deviceId, deviceId,
cryptoStore, cryptoStore,
crossSigningService, crossSigningService,
outgoingGossipingRequestManager, outgoingKeyRequestManager,
incomingGossipingRequestManager, secretShareManager,
myDeviceInfoHolder.get().myDevice.fingerprint()!!, myDeviceInfoHolder.get().myDevice.fingerprint()!!,
startReq.transactionId, startReq.transactionId,
otherUserId, otherUserId,
@ -771,8 +771,15 @@ internal class DefaultVerificationService @Inject constructor(
return return
} }
val roomId = event.roomId
if (roomId == null) {
Timber.e("## SAS Verification missing roomId for event")
// TODO cancel?
return
}
handleReadyReceived(event.senderId, readyReq) { 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) getExistingTransaction(userId, doneReq.transactionId)
?: getOldTransaction(userId, doneReq.transactionId) ?: getOldTransaction(userId, doneReq.transactionId)
?.let { vt -> ?.let { vt ->
val otherDeviceId = vt.otherDeviceId val otherDeviceId = vt.otherDeviceId ?: return@let
if (!crossSigningService.canCrossSign()) { if (!crossSigningService.canCrossSign()) {
outgoingGossipingRequestManager.sendSecretShareRequest( cryptoCoroutineScope.launch {
MASTER_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")) secretShareManager.requestSecretTo(otherDeviceId, MASTER_KEY_SSSS_NAME)
) secretShareManager.requestSecretTo(otherDeviceId, SELF_SIGNING_KEY_SSSS_NAME)
outgoingGossipingRequestManager.sendSecretShareRequest( secretShareManager.requestSecretTo(otherDeviceId, USER_SIGNING_KEY_SSSS_NAME)
SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")) secretShareManager.requestSecretTo(otherDeviceId, KEYBACKUP_SECRET_SSSS_NAME)
) }
outgoingGossipingRequestManager.sendSecretShareRequest(
USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*"))
)
} }
outgoingGossipingRequestManager.sendSecretShareRequest(
KEYBACKUP_SECRET_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*"))
)
} }
} }
} }
@ -922,8 +923,8 @@ internal class DefaultVerificationService @Inject constructor(
otherUserId = senderId, otherUserId = senderId,
otherDeviceId = readyReq.fromDevice, otherDeviceId = readyReq.fromDevice,
crossSigningService = crossSigningService, crossSigningService = crossSigningService,
outgoingGossipingRequestManager = outgoingGossipingRequestManager, outgoingKeyRequestManager = outgoingKeyRequestManager,
incomingGossipingRequestManager = incomingGossipingRequestManager, secretShareManager = secretShareManager,
cryptoStore = cryptoStore, cryptoStore = cryptoStore,
qrCodeData = qrCodeData, qrCodeData = qrCodeData,
userId = userId, userId = userId,
@ -1124,8 +1125,8 @@ internal class DefaultVerificationService @Inject constructor(
deviceId, deviceId,
cryptoStore, cryptoStore,
crossSigningService, crossSigningService,
outgoingGossipingRequestManager, outgoingKeyRequestManager,
incomingGossipingRequestManager, secretShareManager,
myDeviceInfoHolder.get().myDevice.fingerprint()!!, myDeviceInfoHolder.get().myDevice.fingerprint()!!,
txID, txID,
otherUserId, otherUserId,
@ -1188,6 +1189,7 @@ internal class DefaultVerificationService @Inject constructor(
} }
.distinct() .distinct()
requestsForUser.add(verificationRequest)
transport.sendVerificationRequest(methodValues, validLocalId, otherUserId, roomId, null) { syncedId, info -> transport.sendVerificationRequest(methodValues, validLocalId, otherUserId, roomId, null) { syncedId, info ->
// We need to update with the syncedID // We need to update with the syncedID
updatePendingRequest( updatePendingRequest(
@ -1199,7 +1201,6 @@ internal class DefaultVerificationService @Inject constructor(
) )
} }
requestsForUser.add(verificationRequest)
dispatchRequestAdded(verificationRequest) dispatchRequestAdded(verificationRequest)
return verificationRequest return verificationRequest
@ -1323,8 +1324,8 @@ internal class DefaultVerificationService @Inject constructor(
deviceId, deviceId,
cryptoStore, cryptoStore,
crossSigningService, crossSigningService,
outgoingGossipingRequestManager, outgoingKeyRequestManager,
incomingGossipingRequestManager, secretShareManager,
myDeviceInfoHolder.get().myDevice.fingerprint()!!, myDeviceInfoHolder.get().myDevice.fingerprint()!!,
transactionId, transactionId,
otherUserId, otherUserId,
@ -1465,8 +1466,8 @@ internal class DefaultVerificationService @Inject constructor(
otherUserId = otherUserId, otherUserId = otherUserId,
otherDeviceId = otherDeviceId, otherDeviceId = otherDeviceId,
crossSigningService = crossSigningService, crossSigningService = crossSigningService,
outgoingGossipingRequestManager = outgoingGossipingRequestManager, outgoingKeyRequestManager = outgoingKeyRequestManager,
incomingGossipingRequestManager = incomingGossipingRequestManager, secretShareManager = secretShareManager,
cryptoStore = cryptoStore, cryptoStore = cryptoStore,
qrCodeData = qrCodeData, qrCodeData = qrCodeData,
userId = userId, userId = userId,

View file

@ -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.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction 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.crypto.verification.VerificationTxState
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager import org.matrix.android.sdk.internal.crypto.SecretShareManager
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
import timber.log.Timber import timber.log.Timber
@ -31,8 +31,8 @@ import timber.log.Timber
internal abstract class DefaultVerificationTransaction( internal abstract class DefaultVerificationTransaction(
private val setDeviceVerificationAction: SetDeviceVerificationAction, private val setDeviceVerificationAction: SetDeviceVerificationAction,
private val crossSigningService: CrossSigningService, private val crossSigningService: CrossSigningService,
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager, private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val incomingGossipingRequestManager: IncomingGossipingRequestManager, private val secretShareManager: SecretShareManager,
private val userId: String, private val userId: String,
override val transactionId: String, override val transactionId: String,
override val otherUserId: String, override val otherUserId: String,
@ -86,7 +86,7 @@ internal abstract class DefaultVerificationTransaction(
} }
if (otherUserId == userId) { if (otherUserId == userId) {
incomingGossipingRequestManager.onVerificationCompleteForDevice(otherDeviceId!!) secretShareManager.onVerificationCompleteForDevice(otherDeviceId!!)
// If me it's reasonable to sign and upload the device signature // 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 // Notice that i might not have the private keys, so may not be able to do it

View file

@ -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.SasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState 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.session.events.model.EventType
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager import org.matrix.android.sdk.internal.crypto.SecretShareManager
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.extensions.toUnsignedInt import org.matrix.android.sdk.internal.extensions.toUnsignedInt
@ -42,8 +42,8 @@ internal abstract class SASDefaultVerificationTransaction(
open val deviceId: String?, open val deviceId: String?,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
crossSigningService: CrossSigningService, crossSigningService: CrossSigningService,
outgoingGossipingRequestManager: OutgoingGossipingRequestManager, outgoingKeyRequestManager: OutgoingKeyRequestManager,
incomingGossipingRequestManager: IncomingGossipingRequestManager, secretShareManager: SecretShareManager,
private val deviceFingerprint: String, private val deviceFingerprint: String,
transactionId: String, transactionId: String,
otherUserId: String, otherUserId: String,
@ -52,8 +52,8 @@ internal abstract class SASDefaultVerificationTransaction(
) : DefaultVerificationTransaction( ) : DefaultVerificationTransaction(
setDeviceVerificationAction, setDeviceVerificationAction,
crossSigningService, crossSigningService,
outgoingGossipingRequestManager, outgoingKeyRequestManager,
incomingGossipingRequestManager, secretShareManager,
userId, userId,
transactionId, transactionId,
otherUserId, otherUserId,

View file

@ -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<SendVerificationMessageWorker.Params>(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)
}
}

View file

@ -15,13 +15,9 @@
*/ */
package org.matrix.android.sdk.internal.crypto.verification 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.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.events.model.Event 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.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.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent 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.MessageVerificationReadyContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent 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.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.DeviceId
import org.matrix.android.sdk.internal.di.UserId 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 org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class VerificationMessageProcessor @Inject constructor( internal class VerificationMessageProcessor @Inject constructor(
private val eventDecryptor: EventDecryptor,
private val clock: Clock,
private val verificationService: DefaultVerificationService, private val verificationService: DefaultVerificationService,
@UserId private val userId: String, @UserId private val userId: String,
@DeviceId private val deviceId: String? @DeviceId private val deviceId: String?,
) : EventInsertLiveProcessor { private val clock: Clock,
) {
private val transactionsHandledByOtherDevice = ArrayList<String>() private val transactionsHandledByOtherDevice = ArrayList<String>()
@ -60,40 +52,20 @@ internal class VerificationMessageProcessor @Inject constructor(
EventType.ENCRYPTED EventType.ENCRYPTED
) )
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { fun shouldProcess(eventType: String): Boolean {
if (insertType != EventInsertType.INCREMENTAL_SYNC) { return allowedTypes.contains(eventType)
return false
}
return allowedTypes.contains(eventType) && !LocalEcho.isLocalEchoId(eventId)
} }
override suspend fun process(realm: Realm, event: Event) { suspend fun process(event: Event) {
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") 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, // If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
// the message should be ignored by the receiver. // the message should be ignored by the receiver.
if (!VerificationService.isValidRequest(event.ageLocalTs ?: event.originServerTs, clock.epochMillis())) return Unit.also { if (event.ageLocalTs != null && !VerificationService.isValidRequest(event.ageLocalTs, clock.epochMillis())) return Unit.also {
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated") 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()}") Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
// Relates to is not encrypted // Relates to is not encrypted
@ -102,7 +74,6 @@ internal class VerificationMessageProcessor @Inject constructor(
if (event.senderId == userId) { if (event.senderId == userId) {
// If it's send from me, we need to keep track of Requests or Start // If it's send from me, we need to keep track of Requests or Start
// done from another device of mine // done from another device of mine
if (EventType.MESSAGE == event.getClearType()) { if (EventType.MESSAGE == event.getClearType()) {
val msgType = event.getClearContent().toModel<MessageContent>()?.msgType val msgType = event.getClearContent().toModel<MessageContent>()?.msgType
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) { if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
@ -137,6 +108,8 @@ internal class VerificationMessageProcessor @Inject constructor(
transactionsHandledByOtherDevice.remove(it) transactionsHandledByOtherDevice.remove(it)
verificationService.onRoomRequestHandledByOtherDevice(event) 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()}") Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}")

View file

@ -15,14 +15,8 @@
*/ */
package org.matrix.android.sdk.internal.crypto.verification 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest 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.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_RECIPROCATE
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS 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.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.util.time.Clock
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
internal class VerificationTransportRoomMessage( internal class VerificationTransportRoomMessage(
private val workManagerProvider: WorkManagerProvider, private val sendVerificationMessageTask: SendVerificationMessageTask,
private val sessionId: String,
private val userId: String, private val userId: String,
private val userDeviceId: String?, private val userDeviceId: String?,
private val roomId: String, private val roomId: String,
private val localEchoEventFactory: LocalEchoEventFactory, private val localEchoEventFactory: LocalEchoEventFactory,
private val tx: DefaultVerificationTransaction?, private val tx: DefaultVerificationTransaction?,
private val coroutineScope: CoroutineScope, cryptoCoroutineScope: CoroutineScope,
private val clock: Clock, private val clock: Clock,
) : VerificationTransport { ) : VerificationTransport {
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val verificationSenderScope = CoroutineScope(cryptoCoroutineScope.coroutineContext + dispatcher)
private val sequencer = SemaphoreCoroutineSequencer()
override fun <T> sendToOther(type: String, override fun <T> sendToOther(type: String,
verificationInfo: VerificationInfo<T>, verificationInfo: VerificationInfo<T>,
nextState: VerificationTxState, nextState: VerificationTxState,
@ -79,70 +74,22 @@ internal class VerificationTransportRoomMessage(
content = verificationInfo.toEventContent()!! content = verificationInfo.toEventContent()!!
) )
val workerParams = WorkerParamsFactory.toData( verificationSenderScope.launch {
SendVerificationMessageWorker.Params( sequencer.post {
sessionId = sessionId, try {
eventId = event.eventId ?: "" val params = SendVerificationMessageTask.Params(event)
) sendVerificationMessageTask.executeRetry(params, 5)
) // Do I need to update local echo state to sent?
val enqueueInfo = enqueueSendWork(workerParams) if (onDone != null) {
onDone()
// I cannot just listen to the given work request, because when used in a uniqueWork, } else {
// The callback is called while it is still Running ... tx?.state = nextState
}
// Futures.addCallback(enqueueInfo.first.result, object : FutureCallback<Operation.State.SUCCESS> { } catch (failure: Throwable) {
// override fun onSuccess(result: Operation.State.SUCCESS?) { tx?.cancel(onErrorReason)
// 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<List<WorkInfo>> {
override fun onChanged(workInfoList: List<WorkInfo>?) {
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
}
}
}
} }
} }
// TODO listen to DB to get synced info
coroutineScope.launch(Dispatchers.Main) {
workLiveData.observeForever(observer)
}
} }
override fun sendVerificationRequest(supportedMethods: List<String>, override fun sendVerificationRequest(supportedMethods: List<String>,
@ -173,60 +120,24 @@ internal class VerificationTransportRoomMessage(
val content = info.toContent() val content = info.toContent()
val event = createEventAndLocalEcho( val event = createEventAndLocalEcho(
localId, localId = localId,
EventType.MESSAGE, type = EventType.MESSAGE,
roomId, roomId = roomId,
content content = content
) )
val workerParams = WorkerParamsFactory.toData( verificationSenderScope.launch {
SendVerificationMessageWorker.Params( val params = SendVerificationMessageTask.Params(event)
sessionId = sessionId, sequencer.post {
eventId = event.eventId ?: "" try {
) val eventId = sendVerificationMessageTask.executeRetry(params, 5)
) // Do I need to update local echo state to sent?
callback(eventId, validInfo)
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SendVerificationMessageWorker>() } catch (failure: Throwable) {
.setConstraints(WorkManagerProvider.workConstraints) callback(null, null)
.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<List<WorkInfo>> {
override fun onChanged(workInfoList: List<WorkInfo>?) {
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)
}
} }
} }
// 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) { override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String?, code: CancelCode) {
@ -236,13 +147,17 @@ internal class VerificationTransportRoomMessage(
roomId = roomId, roomId = roomId,
content = MessageVerificationCancelContent.create(transactionId, code).toContent() content = MessageVerificationCancelContent.create(transactionId, code).toContent()
) )
val workerParams = WorkerParamsFactory.toData(
SendVerificationMessageWorker.Params( verificationSenderScope.launch {
sessionId = sessionId, sequencer.post {
eventId = event.eventId ?: "" try {
) val params = SendVerificationMessageTask.Params(event)
) sendVerificationMessageTask.executeRetry(params, 5)
enqueueSendWork(workerParams) } catch (failure: Throwable) {
Timber.w(failure, "Failed to cancel verification transaction")
}
}
}
} }
override fun done(transactionId: String, override fun done(transactionId: String,
@ -258,47 +173,21 @@ internal class VerificationTransportRoomMessage(
) )
).toContent() ).toContent()
) )
val workerParams = WorkerParamsFactory.toData( verificationSenderScope.launch {
SendVerificationMessageWorker.Params( sequencer.post {
sessionId = sessionId, try {
eventId = event.eventId ?: "" val params = SendVerificationMessageTask.Params(event)
) sendVerificationMessageTask.executeRetry(params, 5)
) } catch (failure: Throwable) {
val enqueueInfo = enqueueSendWork(workerParams) Timber.w(failure, "Failed to complete (done) verification")
// should we call onDone?
val workLiveData = workManagerProvider.workManager } finally {
.getWorkInfosForUniqueWorkLiveData(uniqueQueueName()) onDone?.invoke()
val observer = object : Observer<List<WorkInfo>> { }
override fun onChanged(workInfoList: List<WorkInfo>?) {
workInfoList
?.filter { it.state == WorkInfo.State.SUCCEEDED }
?.firstOrNull { it.id == enqueueInfo.second }
?.let { _ ->
onDone?.invoke()
workLiveData.removeObserver(this)
}
} }
} }
// TODO listen to DB to get synced info
coroutineScope.launch(Dispatchers.Main) {
workLiveData.observeForever(observer)
}
} }
private fun enqueueSendWork(workerParams: Data): Pair<Operation, UUID> {
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SendVerificationMessageWorker>()
.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, override fun createAccept(tid: String,
keyAgreementProtocol: String, keyAgreementProtocol: String,
hash: String, hash: String,

View file

@ -16,39 +16,35 @@
package org.matrix.android.sdk.internal.crypto.verification 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.DeviceId
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.UserId 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.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.time.Clock
import javax.inject.Inject import javax.inject.Inject
internal class VerificationTransportRoomMessageFactory @Inject constructor( internal class VerificationTransportRoomMessageFactory @Inject constructor(
private val workManagerProvider: WorkManagerProvider, private val sendVerificationMessageTask: SendVerificationMessageTask,
@SessionId
private val sessionId: String,
@UserId @UserId
private val userId: String, private val userId: String,
@DeviceId @DeviceId
private val deviceId: String?, private val deviceId: String?,
private val localEchoEventFactory: LocalEchoEventFactory, private val localEchoEventFactory: LocalEchoEventFactory,
private val taskExecutor: TaskExecutor, private val cryptoCoroutineScope: CoroutineScope,
private val clock: Clock, private val clock: Clock,
) { ) {
fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage { fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage {
return VerificationTransportRoomMessage( return VerificationTransportRoomMessage(
workManagerProvider, sendVerificationMessageTask = sendVerificationMessageTask,
sessionId, userId = userId,
userId, userDeviceId = deviceId,
deviceId, roomId = roomId,
roomId, localEchoEventFactory = localEchoEventFactory,
localEchoEventFactory, tx = tx,
tx, cryptoCoroutineScope = cryptoCoroutineScope,
taskExecutor.executorScope, clock = clock,
clock
) )
} }
} }

View file

@ -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.crypto.verification.VerificationTxState
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.util.fromBase64 import org.matrix.android.sdk.api.util.fromBase64
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager import org.matrix.android.sdk.internal.crypto.SecretShareManager
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction 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.crosssigning.fromBase64Safe
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
@ -37,8 +37,8 @@ internal class DefaultQrCodeVerificationTransaction(
override val otherUserId: String, override val otherUserId: String,
override var otherDeviceId: String?, override var otherDeviceId: String?,
private val crossSigningService: CrossSigningService, private val crossSigningService: CrossSigningService,
outgoingGossipingRequestManager: OutgoingGossipingRequestManager, outgoingKeyRequestManager: OutgoingKeyRequestManager,
incomingGossipingRequestManager: IncomingGossipingRequestManager, secretShareManager: SecretShareManager,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
// Not null only if other user is able to scan QR code // Not null only if other user is able to scan QR code
private val qrCodeData: QrCodeData?, private val qrCodeData: QrCodeData?,
@ -48,8 +48,8 @@ internal class DefaultQrCodeVerificationTransaction(
) : DefaultVerificationTransaction( ) : DefaultVerificationTransaction(
setDeviceVerificationAction, setDeviceVerificationAction,
crossSigningService, crossSigningService,
outgoingGossipingRequestManager, outgoingKeyRequestManager,
incomingGossipingRequestManager, secretShareManager,
userId, userId,
transactionId, transactionId,
otherUserId, otherUserId,

View file

@ -21,12 +21,8 @@ import dagger.Component
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.auth.data.SessionParams
import org.matrix.android.sdk.api.session.Session 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.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.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.di.MatrixComponent
import org.matrix.android.sdk.internal.federation.FederationModule import org.matrix.android.sdk.internal.federation.FederationModule
import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
@ -133,14 +129,6 @@ internal interface SessionComponent {
fun inject(worker: AddPusherWorker) fun inject(worker: AddPusherWorker)
fun inject(worker: SendVerificationMessageWorker)
fun inject(worker: SendGossipRequestWorker)
fun inject(worker: CancelGossipRequestWorker)
fun inject(worker: SendGossipWorker)
fun inject(worker: UpdateTrustWorker) fun inject(worker: UpdateTrustWorker)
@Component.Factory @Component.Factory

View file

@ -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.secrets.DefaultSharedSecretStorageService
import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask 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.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.EventInsertLiveObserver
import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory
@ -318,10 +317,6 @@ internal abstract class SessionModule {
@IntoSet @IntoSet
abstract fun bindRoomCreateEventProcessor(processor: RoomCreateEventProcessor): EventInsertLiveProcessor abstract fun bindRoomCreateEventProcessor(processor: RoomCreateEventProcessor): EventInsertLiveProcessor
@Binds
@IntoSet
abstract fun bindVerificationMessageProcessor(processor: VerificationMessageProcessor): EventInsertLiveProcessor
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindCallEventProcessor(processor: CallEventProcessor): EventInsertLiveProcessor abstract fun bindCallEventProcessor(processor: CallEventProcessor): EventInsertLiveProcessor

View file

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.membership.joining
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import kotlinx.coroutines.TimeoutCancellationException 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.events.model.toContent
import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
@ -53,6 +54,7 @@ internal class DefaultJoinRoomTask @Inject constructor(
private val readMarkersTask: SetReadMarkersTask, private val readMarkersTask: SetReadMarkersTask,
@SessionDatabase @SessionDatabase
private val realmConfiguration: RealmConfiguration, private val realmConfiguration: RealmConfiguration,
private val coroutineDispatcher: MatrixCoroutineDispatchers,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
private val globalErrorReceiver: GlobalErrorReceiver, private val globalErrorReceiver: GlobalErrorReceiver,
private val clock: Clock, private val clock: Clock,

View file

@ -382,7 +382,10 @@ internal class RoomSyncHandler @Inject constructor(
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>() val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>() val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
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) { if (event.eventId == null || event.senderId == null || event.type == null) {
continue continue
} }
@ -454,7 +457,7 @@ internal class RoomSyncHandler @Inject constructor(
} }
} }
// Give info to crypto module // Give info to crypto module
cryptoService.onLiveEvent(roomEntity.roomId, event) cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync)
// Try to remove local echo // Try to remove local echo
event.unsignedData?.transactionId?.also { event.unsignedData?.transactionId?.also {

View file

@ -22,11 +22,7 @@ import androidx.work.ListenableWorker
import androidx.work.WorkerFactory import androidx.work.WorkerFactory
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import org.matrix.android.sdk.internal.SessionManager 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.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.di.MatrixScope
import org.matrix.android.sdk.internal.session.content.UploadContentWorker import org.matrix.android.sdk.internal.session.content.UploadContentWorker
import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker 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) CheckFactoryWorker(appContext, workerParameters, true)
AddPusherWorker::class.java.name -> AddPusherWorker::class.java.name ->
AddPusherWorker(appContext, workerParameters, sessionManager) AddPusherWorker(appContext, workerParameters, sessionManager)
CancelGossipRequestWorker::class.java.name ->
CancelGossipRequestWorker(appContext, workerParameters, sessionManager)
GetGroupDataWorker::class.java.name -> GetGroupDataWorker::class.java.name ->
GetGroupDataWorker(appContext, workerParameters, sessionManager) GetGroupDataWorker(appContext, workerParameters, sessionManager)
MultipleEventSendingDispatcherWorker::class.java.name -> MultipleEventSendingDispatcherWorker::class.java.name ->
@ -66,12 +60,6 @@ internal class MatrixWorkerFactory @Inject constructor(private val sessionManage
RedactEventWorker(appContext, workerParameters, sessionManager) RedactEventWorker(appContext, workerParameters, sessionManager)
SendEventWorker::class.java.name -> SendEventWorker::class.java.name ->
SendEventWorker(appContext, workerParameters, sessionManager) 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::class.java.name ->
SyncWorker(appContext, workerParameters, sessionManager) SyncWorker(appContext, workerParameters, sessionManager)
UpdateTrustWorker::class.java.name -> UpdateTrustWorker::class.java.name ->

View file

@ -22,16 +22,17 @@ import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.features.popup.DefaultVectorAlert import im.vector.app.features.popup.DefaultVectorAlert
import im.vector.app.features.popup.PopupAlertManager 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.MatrixCallback
import org.matrix.android.sdk.api.session.Session 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.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener 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.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo 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.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.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.SasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService 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.VerificationTransaction
@ -60,6 +61,9 @@ class KeyRequestHandler @Inject constructor(
var session: Session? = null 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) { fun start(session: Session) {
this.session = session this.session = session
session.cryptoService().verificationService().addListener(this) session.cryptoService().verificationService().addListener(this)
@ -72,10 +76,9 @@ class KeyRequestHandler @Inject constructor(
session = null 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 // By default Element will not prompt if the SDK has decided that the request should not be fulfilled
Timber.v("## onSecretShareRequest() : Ignoring $request") Timber.v("## onSecretShareRequest() : Ignoring $request")
request.ignore?.run()
return true return true
} }
@ -85,6 +88,8 @@ class KeyRequestHandler @Inject constructor(
* @param request the key request. * @param request the key request.
*/ */
override fun onRoomKeyRequest(request: IncomingRoomKeyRequest) { override fun onRoomKeyRequest(request: IncomingRoomKeyRequest) {
if (!enablePromptingForRequest) return
val userId = request.userId val userId = request.userId
val deviceId = request.deviceId val deviceId = request.deviceId
val requestId = request.requestId val requestId = request.requestId
@ -195,15 +200,14 @@ class KeyRequestHandler @Inject constructor(
} }
private fun denyAllRequests(mappingKey: String) { private fun denyAllRequests(mappingKey: String) {
alertsToRequests[mappingKey]?.forEach {
it.ignore?.run()
}
alertsToRequests.remove(mappingKey) alertsToRequests.remove(mappingKey)
} }
private fun shareAllSessions(mappingKey: String) { private fun shareAllSessions(mappingKey: String) {
alertsToRequests[mappingKey]?.forEach { alertsToRequests[mappingKey]?.forEach {
it.share?.run() session?.coroutineScope?.launch {
session?.cryptoService()?.manuallyAcceptRoomKeyRequest(it)
}
} }
alertsToRequests.remove(mappingKey) alertsToRequests.remove(mappingKey)
} }
@ -213,7 +217,7 @@ class KeyRequestHandler @Inject constructor(
* *
* @param request the cancellation request. * @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 // see if we can find the request in the queue
val userId = request.userId val userId = request.userId
val deviceId = request.deviceId val deviceId = request.deviceId

View file

@ -20,7 +20,6 @@ import im.vector.app.R
import im.vector.app.core.platform.ViewModelTask import im.vector.app.core.platform.ViewModelTask
import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.platform.WaitingViewData
import im.vector.app.core.resources.StringProvider 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.listeners.ProgressListener
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
@ -149,15 +148,8 @@ class BackupToQuadSMigrationTask @Inject constructor(
// save for gossiping // save for gossiping
keysBackupService.saveBackupRecoveryKey(recoveryKey, version.version) keysBackupService.saveBackupRecoveryKey(recoveryKey, version.version)
// while we are there let's restore, but do not block // It's not a good idea to download the full backup, it might take very long
session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( // and use a lot of resources
version,
recoveryKey,
null,
null,
null,
NoOpMatrixCallback()
)
return Result.Success return Result.Success
} catch (failure: Throwable) { } catch (failure: Throwable) {

View file

@ -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.KeysBackupLastVersionResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey 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.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.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
@ -424,6 +423,11 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
} }
private fun tentativeRestoreBackup(res: Map<String, String>?) { private fun tentativeRestoreBackup(res: Map<String, String>?) {
// 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) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also { 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) session.cryptoService().keysBackupService().getCurrentVersion(it)
}.toKeysVersionResult() ?: return@launch }.toKeysVersionResult() ?: return@launch
awaitCallback<ImportRoomKeysResult> { val recoveryKey = computeRecoveryKey(secret.fromBase64())
session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( val isValid = awaitCallback<Boolean> {
version, session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(recoveryKey, it)
computeRecoveryKey(secret.fromBase64()), }
null, if (isValid) {
null, session.cryptoService().keysBackupService().saveBackupRecoveryKey(recoveryKey, version.version)
null,
it
)
} }
awaitCallback<Unit> { awaitCallback<Unit> {
session.cryptoService().keysBackupService().trustKeysBackupVersion(version, true, it) session.cryptoService().keysBackupService().trustKeysBackupVersion(version, true, it)
} }

View file

@ -27,10 +27,8 @@ import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.createJSonViewerStyleProvider
import im.vector.app.databinding.FragmentGenericRecyclerBinding import im.vector.app.databinding.FragmentGenericRecyclerBinding
import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
import org.matrix.android.sdk.api.session.events.model.Event
import javax.inject.Inject import javax.inject.Inject
class GossipingEventsPaperTrailFragment @Inject constructor( class GossipingEventsPaperTrailFragment @Inject constructor(
@ -64,17 +62,17 @@ class GossipingEventsPaperTrailFragment @Inject constructor(
super.onDestroyView() super.onDestroyView()
} }
override fun didTap(event: Event) { override fun didTap(event: AuditTrail) {
if (event.isEncrypted()) { // if (event.isEncrypted()) {
event.toClearContentStringWithIndent() // event.toClearContentStringWithIndent()
} else { // } else {
event.toContentStringWithIndent() // event.toContentStringWithIndent()
}?.let { // }?.let {
JSonViewerDialog.newInstance( // JSonViewerDialog.newInstance(
it, // it,
-1, // -1,
createJSonViewerStyleProvider(colorProvider) // createJSonViewerStyleProvider(colorProvider)
).show(childFragmentManager, "JSON_VIEWER") // ).show(childFragmentManager, "JSON_VIEWER")
} // }
} }
} }

View file

@ -32,10 +32,10 @@ import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import org.matrix.android.sdk.api.session.Session 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( data class GossipingEventsPaperTrailState(
val events: Async<PagedList<Event>> = Uninitialized val events: Async<PagedList<AuditTrail>> = Uninitialized
) : MavericksState ) : MavericksState
class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted initialState: GossipingEventsPaperTrailState, class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted initialState: GossipingEventsPaperTrailState,

View file

@ -17,62 +17,43 @@
package im.vector.app.features.settings.devtools package im.vector.app.features.settings.devtools
import im.vector.app.core.resources.DateProvider import im.vector.app.core.resources.DateProvider
import me.gujun.android.span.span import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo
import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject import org.matrix.android.sdk.api.session.crypto.model.TrailType
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo
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.threeten.bp.format.DateTimeFormatter import org.threeten.bp.format.DateTimeFormatter
class GossipingEventsSerializer { class GossipingEventsSerializer {
private val full24DateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS") private val full24DateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
fun serialize(eventList: List<Event>): String { fun serialize(eventList: List<AuditTrail>): String {
return buildString { return buildString {
eventList.forEach { eventList.forEach { trail ->
val clearType = it.getClearType() val type = trail.type
append("[${getFormattedDate(it.ageLocalTs)}] $clearType from:${it.senderId} - ") val info = trail.info
when (clearType) { append("[${getFormattedDate(trail.ageLocalTs)}] ${type.name} ")
EventType.ROOM_KEY_REQUEST -> { append("sessionId: ${info.sessionId} ")
val content = it.getClearContent().toModel<RoomKeyShareRequest>() when (type) {
append("reqId:${content?.requestId} action:${content?.action} ") TrailType.IncomingKeyRequest -> {
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) { append("from:${info.userId}|${info.deviceId} - ")
append("sessionId: ${content.body?.sessionId} ")
}
append("requestedBy: ${content?.requestingDeviceId}")
} }
EventType.FORWARDED_ROOM_KEY -> { TrailType.OutgoingKeyForward -> {
val encryptedContent = it.content.toModel<OlmEventContent>() append("to:${info.userId}|${info.deviceId} - ")
val content = it.getClearContent().toModel<ForwardedRoomKeyContent>() (trail.info as? ForwardInfo)?.let {
append("chainIndex: ${it.chainIndex} ")
append("sessionId:${content?.sessionId} From Device (sender key):${encryptedContent?.senderKey}")
span("\nFrom Device (sender key):") {
textStyle = "bold"
} }
} }
EventType.ROOM_KEY -> { TrailType.OutgoingKeyWithheld -> {
val content = it.getClearContent() append("to:${info.userId}|${info.deviceId} - ")
append("sessionId:${content?.get("session_id")} roomId:${content?.get("room_id")} dest:${content?.get("_dest") ?: "me"}") (trail.info as? WithheldInfo)?.let {
} append("code: ${it.code} ")
EventType.SEND_SECRET -> {
val content = it.getClearContent().toModel<SecretSendEventContent>()
append("requestId:${content?.requestId} From Device:${it.mxDecryptionResult?.payload?.get("sender_device")}")
}
EventType.REQUEST_SECRET -> {
val content = it.getClearContent().toModel<SecretShareRequest>()
append("reqId:${content?.requestId} action:${content?.action} ")
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
append("secretName:${content.secretName} ")
} }
append("requestedBy:${content?.requestingDeviceId}")
} }
EventType.ENCRYPTED -> { TrailType.IncomingKeyForward -> {
append("Failed to Decrypt") append("from:${info.userId}|${info.deviceId} - ")
(trail.info as? ForwardInfo)?.let {
append("chainIndex: ${it.chainIndex} ")
}
} }
else -> { else -> {
append("??") append("??")

View file

@ -18,7 +18,6 @@ package im.vector.app.features.settings.devtools
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController import com.airbnb.epoxy.paging.PagedListEpoxyController
import im.vector.app.R
import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.resources.ColorProvider 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.app.core.utils.createUIHandler
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import me.gujun.android.span.span 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.AuditTrail
import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo
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.crypto.model.SecretShareRequest 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.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 javax.inject.Inject import javax.inject.Inject
class GossipingTrailPagedEpoxyController @Inject constructor( class GossipingTrailPagedEpoxyController @Inject constructor(
private val vectorDateFormatter: VectorDateFormatter, private val vectorDateFormatter: VectorDateFormatter,
private val colorProvider: ColorProvider private val colorProvider: ColorProvider
) : PagedListEpoxyController<Event>( ) : PagedListEpoxyController<AuditTrail>(
// Important it must match the PageList builder notify Looper // Important it must match the PageList builder notify Looper
modelBuildingHandler = createUIHandler() modelBuildingHandler = createUIHandler()
) { ) {
interface InteractionListener { interface InteractionListener {
fun didTap(event: Event) fun didTap(event: AuditTrail)
} }
var interactionListener: InteractionListener? = null var interactionListener: InteractionListener? = null
override fun buildItemModel(currentPosition: Int, item: Event?): EpoxyModel<*> { override fun buildItemModel(currentPosition: Int, item: AuditTrail?): EpoxyModel<*> {
val host = this val host = this
val event = item ?: return GenericItem_().apply { id(currentPosition) } val event = item ?: return GenericItem_().apply { id(currentPosition) }
return GenericItem_().apply { return GenericItem_().apply {
id(event.hashCode()) id(event.hashCode())
itemClickAction { host.interactionListener?.didTap(event) } itemClickAction { host.interactionListener?.didTap(event) }
title( title(event.type.name.toEpoxyCharSequence())
if (event.isEncrypted()) {
"${event.getClearType()} [encrypted]"
} else {
event.type
}?.toEpoxyCharSequence()
)
description( description(
span { span {
+host.vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME) +host.vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
span("\nfrom: ") { span("\n${host.senderFromTo(event.type)}: ") {
textStyle = "bold" 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 { apply {
if (event.getClearType() == EventType.ROOM_KEY_REQUEST) { when (event.type) {
val content = event.getClearContent().toModel<RoomKeyShareRequest>() TrailType.OutgoingKeyForward -> {
span("\nreqId:") { val fInfo = event.info as ForwardInfo
textStyle = "bold" span("\nchainIndex: ") {
}
+" ${content?.requestId}"
span("\naction:") {
textStyle = "bold"
}
+" ${content?.action}"
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
span("\nsessionId:") {
textStyle = "bold" textStyle = "bold"
} }
+" ${content.body?.sessionId}" +"${fInfo.chainIndex}"
} }
span("\nrequestedBy: ") { TrailType.OutgoingKeyWithheld -> {
textStyle = "bold" val fInfo = event.info as WithheldInfo
} span("\ncode: ") {
+"${content?.requestingDeviceId}"
} else if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
val encryptedContent = event.content.toModel<OlmEventContent>()
val content = event.getClearContent().toModel<ForwardedRoomKeyContent>()
if (event.mxDecryptionResult == null) {
span("**Failed to Decrypt** ${event.mCryptoError}") {
textColor = 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<SecretSendEventContent>()
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<SecretShareRequest>()
span("\nreqId:") {
textStyle = "bold"
}
+" ${content?.requestId}"
span("\naction:") {
textStyle = "bold"
}
+" ${content?.action}"
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
span("\nsecretName:") {
textStyle = "bold" textStyle = "bold"
} }
+" ${content.secretName}" +"${fInfo.code}"
} }
span("\nrequestedBy: ") { TrailType.IncomingKeyRequest -> {
textStyle = "bold" // no additional info
} }
+"${content?.requestingDeviceId}" TrailType.IncomingKeyForward -> {
} else if (event.getClearType() == EventType.ENCRYPTED) { val fInfo = event.info as ForwardInfo
span("**Failed to Decrypt** ${event.mCryptoError}") { span("\nchainIndex: ") {
textColor = host.colorProvider.getColorFromAttribute(R.attr.colorError) 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 -> ""
}
}
} }

View file

@ -53,6 +53,7 @@ class IncomingKeyRequestPagedController @Inject constructor(
textStyle = "bold" textStyle = "bold"
} }
span("${roomKeyRequest.userId}") span("${roomKeyRequest.userId}")
+"\n"
+host.vectorDateFormatter.format(roomKeyRequest.localCreationTimestamp, DateFormatKind.DEFAULT_DATE_AND_TIME) +host.vectorDateFormatter.format(roomKeyRequest.localCreationTimestamp, DateFormatKind.DEFAULT_DATE_AND_TIME)
span("\nsessionId:") { span("\nsessionId:") {
textStyle = "bold" textStyle = "bold"
@ -62,10 +63,6 @@ class IncomingKeyRequestPagedController @Inject constructor(
textStyle = "bold" textStyle = "bold"
} }
+"${roomKeyRequest.deviceId}" +"${roomKeyRequest.deviceId}"
span("\nstate: ") {
textStyle = "bold"
}
+roomKeyRequest.state.name
}.toEpoxyCharSequence() }.toEpoxyCharSequence()
) )
} }

View file

@ -32,12 +32,12 @@ import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session 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.IncomingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
data class KeyRequestListViewState( data class KeyRequestListViewState(
val incomingRequests: Async<PagedList<IncomingRoomKeyRequest>> = Uninitialized, val incomingRequests: Async<PagedList<IncomingRoomKeyRequest>> = Uninitialized,
val outgoingRoomKeyRequests: Async<PagedList<OutgoingRoomKeyRequest>> = Uninitialized val outgoingRoomKeyRequests: Async<PagedList<OutgoingKeyRequest>> = Uninitialized
) : MavericksState ) : MavericksState
class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState: KeyRequestListViewState, class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState: KeyRequestListViewState,

View file

@ -22,10 +22,10 @@ import im.vector.app.core.ui.list.GenericItem_
import im.vector.app.core.utils.createUIHandler import im.vector.app.core.utils.createUIHandler
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import me.gujun.android.span.span 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 import javax.inject.Inject
class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyController<OutgoingRoomKeyRequest>( class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyController<OutgoingKeyRequest>(
// Important it must match the PageList builder notify Looper // Important it must match the PageList builder notify Looper
modelBuildingHandler = createUIHandler() modelBuildingHandler = createUIHandler()
) { ) {
@ -36,7 +36,7 @@ class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyCo
var interactionListener: InteractionListener? = null 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) } val roomKeyRequest = item ?: return GenericItem_().apply { id(currentPosition) }
return GenericItem_().apply { return GenericItem_().apply {

View file

@ -66,7 +66,7 @@ class FakeSharedSecretStorageService : SharedSecretStorageService {
override fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?) = integrityResult override fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?) = integrityResult
override fun requestSecret(name: String, myOtherDeviceId: String) { override suspend fun requestSecret(name: String, myOtherDeviceId: String) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }