mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-22 01:15:54 +03:00
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:
commit
304cb07858
90 changed files with 3914 additions and 3943 deletions
1
changelog.d/5494.feature
Normal file
1
changelog.d/5494.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Use key backup before requesting keys + refactor & improvement of key request/forward
|
4
changelog.d/5559.sdk
Normal file
4
changelog.d/5559.sdk
Normal 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()
|
|
@ -19,6 +19,7 @@ package org.matrix.android.sdk.common
|
|||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.Observer
|
||||
import org.amshove.kluent.fail
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
|
@ -31,8 +32,16 @@ import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
|||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
||||
|
@ -40,13 +49,19 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxStat
|
|||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner
|
||||
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.awaitCallback
|
||||
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
||||
import java.util.UUID
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
|
@ -296,33 +311,94 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize cross-signing, set up megolm backup and save all in 4S
|
||||
*/
|
||||
fun bootstrapSecurity(session: Session) {
|
||||
initializeCrossSigning(session)
|
||||
val ssssService = session.sharedSecretStorageService()
|
||||
testHelper.runBlockingTest {
|
||||
val keyInfo = ssssService.generateKey(
|
||||
UUID.randomUUID().toString(),
|
||||
null,
|
||||
"ssss_key",
|
||||
EmptyKeySigner()
|
||||
)
|
||||
ssssService.setDefaultKey(keyInfo.keyId)
|
||||
|
||||
ssssService.storeSecret(
|
||||
MASTER_KEY_SSSS_NAME,
|
||||
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master!!,
|
||||
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
|
||||
)
|
||||
|
||||
ssssService.storeSecret(
|
||||
SELF_SIGNING_KEY_SSSS_NAME,
|
||||
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned!!,
|
||||
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
|
||||
)
|
||||
|
||||
ssssService.storeSecret(
|
||||
USER_SIGNING_KEY_SSSS_NAME,
|
||||
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user!!,
|
||||
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
|
||||
)
|
||||
|
||||
// set up megolm backup
|
||||
val creationInfo = awaitCallback<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) {
|
||||
assertTrue(alice.cryptoService().crossSigningService().canCrossSign())
|
||||
assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
|
||||
|
||||
val requestID = UUID.randomUUID().toString()
|
||||
val aliceVerificationService = alice.cryptoService().verificationService()
|
||||
val bobVerificationService = bob.cryptoService().verificationService()
|
||||
|
||||
aliceVerificationService.beginKeyVerificationInDMs(
|
||||
VerificationMethod.SAS,
|
||||
requestID,
|
||||
roomId,
|
||||
bob.myUserId,
|
||||
bob.sessionParams.credentials.deviceId!!
|
||||
)
|
||||
val localId = UUID.randomUUID().toString()
|
||||
aliceVerificationService.requestKeyVerificationInDMs(
|
||||
localId = localId,
|
||||
methods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
|
||||
otherUserId = bob.myUserId,
|
||||
roomId = roomId
|
||||
).transactionId
|
||||
|
||||
// we should reach SHOW SAS on both
|
||||
var alicePovTx: OutgoingSasVerificationTransaction? = null
|
||||
var bobPovTx: IncomingSasVerificationTransaction? = null
|
||||
|
||||
// wait for alice to get the ready
|
||||
testHelper.waitWithLatch {
|
||||
testHelper.retryPeriodicallyWithLatch(it) {
|
||||
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
|
||||
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
|
||||
if (bobPovTx?.state == VerificationTxState.OnStarted) {
|
||||
bobPovTx?.performAccept()
|
||||
bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull {
|
||||
it.requestInfo?.fromDevice == alice.sessionParams.deviceId
|
||||
} != null
|
||||
}
|
||||
}
|
||||
val incomingRequest = bobVerificationService.getExistingVerificationRequests(alice.myUserId).first {
|
||||
it.requestInfo?.fromDevice == alice.sessionParams.deviceId
|
||||
}
|
||||
bobVerificationService.readyPendingVerification(listOf(VerificationMethod.SAS), alice.myUserId, incomingRequest.transactionId!!)
|
||||
|
||||
var requestID: String? = null
|
||||
// wait for it to be readied
|
||||
testHelper.waitWithLatch {
|
||||
testHelper.retryPeriodicallyWithLatch(it) {
|
||||
val outgoingRequest = aliceVerificationService.getExistingVerificationRequests(bob.myUserId)
|
||||
.firstOrNull { it.localId == localId }
|
||||
if (outgoingRequest?.isReady == true) {
|
||||
requestID = outgoingRequest.transactionId!!
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
@ -330,9 +406,20 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
|
|||
}
|
||||
}
|
||||
|
||||
aliceVerificationService.beginKeyVerificationInDMs(
|
||||
VerificationMethod.SAS,
|
||||
requestID!!,
|
||||
roomId,
|
||||
bob.myUserId,
|
||||
bob.sessionParams.credentials.deviceId!!)
|
||||
|
||||
// we should reach SHOW SAS on both
|
||||
var alicePovTx: OutgoingSasVerificationTransaction? = null
|
||||
var bobPovTx: IncomingSasVerificationTransaction? = null
|
||||
|
||||
testHelper.waitWithLatch {
|
||||
testHelper.retryPeriodicallyWithLatch(it) {
|
||||
alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID) as? OutgoingSasVerificationTransaction
|
||||
alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction
|
||||
Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}")
|
||||
alicePovTx?.state == VerificationTxState.ShortCodeReady
|
||||
}
|
||||
|
@ -340,7 +427,7 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
|
|||
// wait for alice to get the ready
|
||||
testHelper.waitWithLatch {
|
||||
testHelper.retryPeriodicallyWithLatch(it) {
|
||||
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
|
||||
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID!!) as? IncomingSasVerificationTransaction
|
||||
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
|
||||
if (bobPovTx?.state == VerificationTxState.OnStarted) {
|
||||
bobPovTx?.performAccept()
|
||||
|
@ -392,4 +479,50 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
|
|||
|
||||
return CryptoTestData(roomId, sessions)
|
||||
}
|
||||
|
||||
fun ensureCanDecrypt(sentEventIds: List<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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,13 +30,21 @@ import org.junit.runners.MethodSorters
|
|||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.crypto.RequestResult
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.getRoomSummary
|
||||
|
@ -52,15 +60,13 @@ import org.matrix.android.sdk.common.CryptoTestHelper
|
|||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
import org.matrix.android.sdk.common.TestMatrixCallback
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
@LargeTest
|
||||
class E2eeSanityTests : InstrumentedTest {
|
||||
|
||||
private val testHelper = CommonTestHelper(context())
|
||||
private val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
|
||||
/**
|
||||
* Simple test that create an e2ee room.
|
||||
* Some new members are added, and a message is sent.
|
||||
|
@ -72,16 +78,24 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
*/
|
||||
@Test
|
||||
fun testSendingE2EEMessages() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val e2eRoomID = cryptoTestData.roomId
|
||||
|
||||
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
||||
// we want to disable key gossiping to just check initial sending of keys
|
||||
aliceSession.cryptoService().enableKeyGossiping(false)
|
||||
cryptoTestData.secondSession?.cryptoService()?.enableKeyGossiping(false)
|
||||
|
||||
// add some more users and invite them
|
||||
val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu")
|
||||
.map {
|
||||
testHelper.createAccount(it, SessionTestParams(true))
|
||||
testHelper.createAccount(it, SessionTestParams(true)).also {
|
||||
it.cryptoService().enableKeyGossiping(false)
|
||||
}
|
||||
}
|
||||
|
||||
Log.v("#E2E TEST", "All accounts created")
|
||||
|
@ -95,18 +109,18 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
|
||||
// All user should accept invite
|
||||
otherAccounts.forEach { otherSession ->
|
||||
waitForAndAcceptInviteInRoom(otherSession, e2eRoomID)
|
||||
waitForAndAcceptInviteInRoom(testHelper, otherSession, e2eRoomID)
|
||||
Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
|
||||
}
|
||||
|
||||
// check that alice see them as joined (not really necessary?)
|
||||
ensureMembersHaveJoined(aliceSession, otherAccounts, e2eRoomID)
|
||||
ensureMembersHaveJoined(testHelper, aliceSession, otherAccounts, e2eRoomID)
|
||||
|
||||
Log.v("#E2E TEST", "All users have joined the room")
|
||||
Log.v("#E2E TEST", "Alice is sending the message")
|
||||
|
||||
val text = "This is my message"
|
||||
val sentEventId: String? = sendMessageInRoom(aliceRoomPOV, text)
|
||||
val sentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, text)
|
||||
// val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first()
|
||||
Assert.assertTrue("Message should be sent", sentEventId != null)
|
||||
|
||||
|
@ -114,10 +128,10 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
otherAccounts.forEach { otherSession ->
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
|
||||
timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
|
||||
timeLineEvent != null &&
|
||||
timeLineEvent.isEncrypted() &&
|
||||
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -136,10 +150,10 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
}
|
||||
|
||||
newAccount.forEach {
|
||||
waitForAndAcceptInviteInRoom(it, e2eRoomID)
|
||||
waitForAndAcceptInviteInRoom(testHelper, it, e2eRoomID)
|
||||
}
|
||||
|
||||
ensureMembersHaveJoined(aliceSession, newAccount, e2eRoomID)
|
||||
ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID)
|
||||
|
||||
// wait a bit
|
||||
testHelper.runBlockingTest {
|
||||
|
@ -164,7 +178,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
Log.v("#E2E TEST", "Alice sends a new message")
|
||||
|
||||
val secondMessage = "2 This is my message"
|
||||
val secondSentEventId: String? = sendMessageInRoom(aliceRoomPOV, secondMessage)
|
||||
val secondSentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, secondMessage)
|
||||
|
||||
// new members should be able to decrypt it
|
||||
newAccount.forEach { otherSession ->
|
||||
|
@ -188,6 +202,14 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testKeyGossipingIsEnabledByDefault() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val session = testHelper.createAccount("alice", SessionTestParams(true))
|
||||
Assert.assertTrue("Key gossiping should be enabled by default", session.cryptoService().isKeyGossipingEnabled())
|
||||
testHelper.signOutAndClose(session)
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick test for basic key backup
|
||||
* 1. Create e2e between Alice and Bob
|
||||
|
@ -204,6 +226,9 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
*/
|
||||
@Test
|
||||
fun testBasicBackupImport() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession!!
|
||||
|
@ -227,16 +252,16 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
val sentEventIds = mutableListOf<String>()
|
||||
val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning")
|
||||
messagesText.forEach { text ->
|
||||
val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
|
||||
val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also {
|
||||
sentEventIds.add(it)
|
||||
}
|
||||
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||
timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||
timeLineEvent != null &&
|
||||
timeLineEvent.isEncrypted() &&
|
||||
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
// we want more so let's discard the session
|
||||
|
@ -289,22 +314,23 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
}
|
||||
}
|
||||
// after initial sync events are not decrypted, so we have to try manually
|
||||
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
|
||||
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
|
||||
|
||||
// Let's now import keys from backup
|
||||
|
||||
newBobSession.cryptoService().keysBackupService().let { keysBackupService ->
|
||||
newBobSession.cryptoService().keysBackupService().let { kbs ->
|
||||
val keyVersionResult = testHelper.doSync<KeysVersionResult?> {
|
||||
keysBackupService.getVersion(version.version, it)
|
||||
kbs.getVersion(version.version, it)
|
||||
}
|
||||
|
||||
val importedResult = testHelper.doSync<ImportRoomKeysResult> {
|
||||
keysBackupService.restoreKeyBackupWithPassword(
|
||||
kbs.restoreKeyBackupWithPassword(
|
||||
keyVersionResult!!,
|
||||
keyBackupPassword,
|
||||
null,
|
||||
null,
|
||||
null, it
|
||||
null,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -312,7 +338,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
}
|
||||
|
||||
// ensure bob can now decrypt
|
||||
ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
||||
cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
||||
|
||||
testHelper.signOutAndClose(newBobSession)
|
||||
}
|
||||
|
@ -323,6 +349,9 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
*/
|
||||
@Test
|
||||
fun testSimpleGossip() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession!!
|
||||
|
@ -330,30 +359,28 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
|
||||
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
||||
|
||||
cryptoTestHelper.initializeCrossSigning(bobSession)
|
||||
|
||||
// let's send a few message to bob
|
||||
val sentEventIds = mutableListOf<String>()
|
||||
val messagesText = listOf("1. Hello", "2. Bob")
|
||||
|
||||
Log.v("#E2E TEST", "Alice sends some messages")
|
||||
messagesText.forEach { text ->
|
||||
val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
|
||||
val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also {
|
||||
sentEventIds.add(it)
|
||||
}
|
||||
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||
timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||
timeLineEvent != null &&
|
||||
timeLineEvent.isEncrypted() &&
|
||||
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure bob can decrypt
|
||||
ensureIsDecrypted(sentEventIds, bobSession, e2eRoomID)
|
||||
ensureIsDecrypted(testHelper, sentEventIds, bobSession, e2eRoomID)
|
||||
|
||||
// Let's now add a new bob session
|
||||
// Create a new session for bob
|
||||
|
@ -363,7 +390,11 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
// check that new bob can't currently decrypt
|
||||
Log.v("#E2E TEST", "check that new bob can't currently decrypt")
|
||||
|
||||
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
|
||||
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
|
||||
// newBobSession.cryptoService().getOutgoingRoomKeyRequests()
|
||||
// .firstOrNull {
|
||||
// it.sessionId ==
|
||||
// }
|
||||
|
||||
// Try to request
|
||||
sentEventIds.forEach { sentEventId ->
|
||||
|
@ -372,12 +403,34 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
}
|
||||
|
||||
// wait a bit
|
||||
testHelper.runBlockingTest {
|
||||
delay(10_000)
|
||||
}
|
||||
// we need to wait a couple of syncs to let sharing occurs
|
||||
// testHelper.waitFewSyncs(newBobSession, 6)
|
||||
|
||||
// Ensure that new bob still can't decrypt (keys must have been withheld)
|
||||
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.KEYS_WITHHELD)
|
||||
sentEventIds.forEach { sentEventId ->
|
||||
val megolmSessionId = newBobSession.getRoom(e2eRoomID)!!
|
||||
.getTimelineEvent(sentEventId)!!
|
||||
.root.content.toModel<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
|
||||
|
||||
|
@ -390,12 +443,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
|
||||
}
|
||||
|
||||
// wait a bit
|
||||
testHelper.runBlockingTest {
|
||||
delay(10_000)
|
||||
}
|
||||
|
||||
ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
||||
cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
testHelper.signOutAndClose(newBobSession)
|
||||
|
@ -406,6 +454,9 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
*/
|
||||
@Test
|
||||
fun testForwardBetterKey() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSessionWithBetterKey = cryptoTestData.secondSession!!
|
||||
|
@ -413,35 +464,33 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
|
||||
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
||||
|
||||
cryptoTestHelper.initializeCrossSigning(bobSessionWithBetterKey)
|
||||
|
||||
// let's send a few message to bob
|
||||
var firstEventId: String
|
||||
val firstMessage = "1. Hello"
|
||||
|
||||
Log.v("#E2E TEST", "Alice sends some messages")
|
||||
firstMessage.let { text ->
|
||||
firstEventId = sendMessageInRoom(aliceRoomPOV, text)!!
|
||||
firstEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
|
||||
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
|
||||
timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
|
||||
timeLineEvent != null &&
|
||||
timeLineEvent.isEncrypted() &&
|
||||
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure bob can decrypt
|
||||
ensureIsDecrypted(listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID)
|
||||
ensureIsDecrypted(testHelper, listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID)
|
||||
|
||||
// Let's add a new unverified session from bob
|
||||
val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true))
|
||||
|
||||
// check that new bob can't currently decrypt
|
||||
Log.v("#E2E TEST", "check that new bob can't currently decrypt")
|
||||
ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
|
||||
cryptoTestHelper.ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, null)
|
||||
|
||||
// Now let alice send a new message. this time the new bob session will be able to decrypt
|
||||
var secondEventId: String
|
||||
|
@ -449,14 +498,14 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
|
||||
Log.v("#E2E TEST", "Alice sends some messages")
|
||||
secondMessage.let { text ->
|
||||
secondEventId = sendMessageInRoom(aliceRoomPOV, text)!!
|
||||
secondEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
|
||||
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
|
||||
timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
|
||||
timeLineEvent != null &&
|
||||
timeLineEvent.isEncrypted() &&
|
||||
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -475,9 +524,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
try {
|
||||
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
||||
fail("Should not be able to decrypt event")
|
||||
} catch (error: MXCryptoError) {
|
||||
val errorType = (error as? MXCryptoError.Base)?.errorType
|
||||
assertEquals(MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, errorType)
|
||||
} catch (_: MXCryptoError) {
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -499,41 +546,45 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
.markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!)
|
||||
|
||||
// now let new session request
|
||||
newBobSession.cryptoService().requestRoomKeyForEvent(firstEventNewBobPov.root)
|
||||
newBobSession.cryptoService().reRequestRoomKeyForEvent(firstEventNewBobPov.root)
|
||||
|
||||
// wait a bit
|
||||
testHelper.runBlockingTest {
|
||||
delay(10_000)
|
||||
}
|
||||
// We need to wait for the key request to be sent out and then a reply to be received
|
||||
|
||||
// old session should have shared the key at earliest known index now
|
||||
// we should be able to decrypt both
|
||||
testHelper.runBlockingTest {
|
||||
try {
|
||||
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
||||
} catch (error: MXCryptoError) {
|
||||
fail("Should be able to decrypt first event now $error")
|
||||
}
|
||||
}
|
||||
testHelper.runBlockingTest {
|
||||
try {
|
||||
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
|
||||
} catch (error: MXCryptoError) {
|
||||
fail("Should be able to decrypt event $error")
|
||||
testHelper.waitWithLatch {
|
||||
testHelper.retryPeriodicallyWithLatch(it) {
|
||||
val canDecryptFirst = try {
|
||||
testHelper.runBlockingTest {
|
||||
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
||||
}
|
||||
true
|
||||
} catch (error: MXCryptoError) {
|
||||
false
|
||||
}
|
||||
val canDecryptSecond = try {
|
||||
testHelper.runBlockingTest {
|
||||
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
|
||||
}
|
||||
true
|
||||
} catch (error: MXCryptoError) {
|
||||
false
|
||||
}
|
||||
canDecryptFirst && canDecryptSecond
|
||||
}
|
||||
}
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
testHelper.signOutAndClose(aliceSession)
|
||||
testHelper.signOutAndClose(bobSessionWithBetterKey)
|
||||
testHelper.signOutAndClose(newBobSession)
|
||||
}
|
||||
|
||||
private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? {
|
||||
private fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? {
|
||||
aliceRoomPOV.sendService().sendTextMessage(text)
|
||||
var sentEventId: String? = null
|
||||
testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
|
||||
val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60))
|
||||
timeline.start()
|
||||
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val decryptedMsg = timeline.getSnapshot()
|
||||
.filter { it.root.getClearType() == EventType.MESSAGE }
|
||||
|
@ -552,7 +603,157 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
return sentEventId
|
||||
}
|
||||
|
||||
private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<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.retryPeriodicallyWithLatch(latch) {
|
||||
otherAccounts.map {
|
||||
|
@ -564,7 +765,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) {
|
||||
private fun waitForAndAcceptInviteInRoom(testHelper: CommonTestHelper, otherSession: Session, e2eRoomID: String) {
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val roomSummary = otherSession.getRoomSummary(e2eRoomID)
|
||||
|
@ -576,7 +777,8 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
}
|
||||
}
|
||||
|
||||
testHelper.runBlockingTest(60_000) {
|
||||
// not sure why it's taking so long :/
|
||||
testHelper.runBlockingTest(90_000) {
|
||||
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
|
||||
try {
|
||||
otherSession.roomService().joinRoom(e2eRoomID)
|
||||
|
@ -594,59 +796,14 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun ensureCanDecrypt(sentEventIds: MutableList<String>, session: Session, e2eRoomID: String, messagesText: List<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) {
|
||||
private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List<String>, session: Session, e2eRoomID: String) {
|
||||
testHelper.waitWithLatch { latch ->
|
||||
sentEventIds.forEach { sentEventId ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||
timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureCannotDecrypt(sentEventIds: List<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")
|
||||
}
|
||||
val timeLineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||
timeLineEvent != null &&
|
||||
timeLineEvent.isEncrypted() &&
|
||||
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,6 @@ import org.junit.runners.MethodSorters
|
|||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||
|
@ -51,10 +50,7 @@ class PreShareKeysTest : InstrumentedTest {
|
|||
// clear any outbound session
|
||||
aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
|
||||
|
||||
val preShareCount = bobSession.cryptoService().getGossipingEvents().count {
|
||||
it.senderId == aliceSession.myUserId &&
|
||||
it.getClearType() == EventType.ROOM_KEY
|
||||
}
|
||||
val preShareCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
|
||||
|
||||
assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount)
|
||||
Log.d("#Test", "Room Key Received from alice $preShareCount")
|
||||
|
@ -66,23 +62,23 @@ class PreShareKeysTest : InstrumentedTest {
|
|||
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val newGossipCount = bobSession.cryptoService().getGossipingEvents().count {
|
||||
it.senderId == aliceSession.myUserId &&
|
||||
it.getClearType() == EventType.ROOM_KEY
|
||||
}
|
||||
newGossipCount > preShareCount
|
||||
val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
|
||||
newKeysCount > preShareCount
|
||||
}
|
||||
}
|
||||
|
||||
val latest = bobSession.cryptoService().getGossipingEvents().lastOrNull {
|
||||
it.senderId == aliceSession.myUserId &&
|
||||
it.getClearType() == EventType.ROOM_KEY
|
||||
}
|
||||
val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
|
||||
val aliceOutboundSessionInRoom = aliceCryptoStore.getCurrentOutboundGroupSessionForRoom(e2eRoomID)!!.outboundGroupSession.sessionIdentifier()
|
||||
|
||||
val content = latest?.getClearContent().toModel<RoomKeyContent>()
|
||||
assertNotNull("Bob should have received and decrypted a room key event from alice", content)
|
||||
assertEquals("Wrong room", e2eRoomID, content!!.roomId)
|
||||
val megolmSessionId = content.sessionId!!
|
||||
val bobCryptoStore = (bobSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
|
||||
val aliceDeviceBobPov = bobCryptoStore.getUserDevice(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!)!!
|
||||
val bobInboundForAlice = bobCryptoStore.getInboundGroupSession(aliceOutboundSessionInRoom, aliceDeviceBobPov.identityKey()!!)
|
||||
assertNotNull("Bob should have received and decrypted a room key event from alice", bobInboundForAlice)
|
||||
assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId)
|
||||
|
||||
val megolmSessionId = bobInboundForAlice.olmInboundGroupSession!!.sessionIdentifier()
|
||||
|
||||
assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId)
|
||||
|
||||
val sharedIndex = aliceSession.cryptoService().getSharedWithInfo(e2eRoomID, megolmSessionId)
|
||||
.getObject(bobSession.myUserId, bobSession.sessionParams.deviceId)
|
||||
|
|
|
@ -19,59 +19,45 @@ package org.matrix.android.sdk.internal.crypto.gossiping
|
|||
import android.util.Log
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import junit.framework.TestCase.fail
|
||||
import org.amshove.kluent.internal.assertEquals
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
|
||||
import org.matrix.android.sdk.api.session.crypto.RequestResult
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
@LargeTest
|
||||
class KeyShareTests : InstrumentedTest {
|
||||
|
||||
private val commonTestHelper = CommonTestHelper(context())
|
||||
|
||||
@Test
|
||||
@Ignore("This test will be ignored until it is fixed")
|
||||
fun test_DoNotSelfShareIfNotTrusted() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
|
||||
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||
Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}")
|
||||
|
||||
// Create an encrypted room and add a message
|
||||
val roomId = commonTestHelper.runBlockingTest {
|
||||
|
@ -86,11 +72,18 @@ class KeyShareTests : InstrumentedTest {
|
|||
assertNotNull(room)
|
||||
Thread.sleep(4_000)
|
||||
assertTrue(room?.roomCryptoService()?.isEncrypted() == true)
|
||||
val sentEventId = commonTestHelper.sendTextMessage(room!!, "My Message", 1).first().eventId
|
||||
|
||||
// Open a new sessionx
|
||||
val sentEvent = commonTestHelper.sendTextMessage(room!!, "My Message", 1).first()
|
||||
val sentEventId = sentEvent.eventId
|
||||
val sentEventText = sentEvent.getLastMessageContent()?.body
|
||||
|
||||
val aliceSession2 = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
|
||||
// Open a new session
|
||||
val aliceSession2 = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(false))
|
||||
// block key requesting for now as decrypt will send requests (room summary is trying to decrypt)
|
||||
aliceSession2.cryptoService().enableKeyGossiping(false)
|
||||
commonTestHelper.syncSession(aliceSession2)
|
||||
|
||||
Log.v("TEST", "=======> AliceSession 2 is ${aliceSession2.sessionParams.deviceId}")
|
||||
|
||||
val roomSecondSessionPOV = aliceSession2.getRoom(roomId)
|
||||
|
||||
|
@ -107,7 +100,10 @@ class KeyShareTests : InstrumentedTest {
|
|||
}
|
||||
|
||||
val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
||||
assertEquals("There should be no request as it's disabled", 0, outgoingRequestsBefore.size)
|
||||
|
||||
// Try to request
|
||||
aliceSession2.cryptoService().enableKeyGossiping(true)
|
||||
aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root)
|
||||
|
||||
val eventMegolmSessionId = receivedEvent.root.content.toModel<EncryptedEventContent>()?.sessionId
|
||||
|
@ -117,10 +113,6 @@ class KeyShareTests : InstrumentedTest {
|
|||
commonTestHelper.waitWithLatch { latch ->
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
||||
.filter { req ->
|
||||
// filter out request that was known before
|
||||
!outgoingRequestsBefore.any { req.requestId == it.requestId }
|
||||
}
|
||||
.let {
|
||||
val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
|
||||
outGoingRequestId = outgoing?.requestId
|
||||
|
@ -141,20 +133,34 @@ class KeyShareTests : InstrumentedTest {
|
|||
commonTestHelper.waitWithLatch { latch ->
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
// DEBUG LOGS
|
||||
aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
|
||||
Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
|
||||
// aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
|
||||
// Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
|
||||
// Log.v("TEST", "=========================")
|
||||
// it.forEach { keyRequest ->
|
||||
// Log.v("TEST", "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
|
||||
// }
|
||||
// Log.v("TEST", "=========================")
|
||||
// }
|
||||
|
||||
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
|
||||
incoming != null
|
||||
}
|
||||
}
|
||||
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
// DEBUG LOGS
|
||||
aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest ->
|
||||
Log.v("TEST", "=========================")
|
||||
it.forEach { keyRequest ->
|
||||
Log.v(
|
||||
"TEST",
|
||||
"[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId} is ${keyRequest.state}"
|
||||
)
|
||||
}
|
||||
Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
|
||||
Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
|
||||
Log.v("TEST", "=========================")
|
||||
}
|
||||
|
||||
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
|
||||
incoming?.state == GossipingRequestState.REJECTED
|
||||
val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
|
||||
val reply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
|
||||
val resultCode = (reply?.result as? RequestResult.Failure)?.code
|
||||
resultCode == WithHeldCode.UNVERIFIED
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -175,254 +181,301 @@ class KeyShareTests : InstrumentedTest {
|
|||
// Re request
|
||||
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
|
||||
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
|
||||
Log.v("TEST", "Incoming request Session 1")
|
||||
Log.v("TEST", "=========================")
|
||||
it.forEach {
|
||||
Log.v("TEST", "requestId ${it.requestId}, for sessionId ${it.requestBody?.sessionId} is ${it.state}")
|
||||
}
|
||||
Log.v("TEST", "=========================")
|
||||
|
||||
it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == GossipingRequestState.ACCEPTED }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Thread.sleep(6_000)
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
aliceSession2.cryptoService().getOutgoingRoomKeyRequests().let {
|
||||
it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == OutgoingGossipingRequestState.CANCELLED }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
commonTestHelper.runBlockingTest {
|
||||
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
fail("should have been able to decrypt")
|
||||
}
|
||||
cryptoTestHelper.ensureCanDecrypt(listOf(receivedEvent.eventId), aliceSession2, roomId, listOf(sentEventText ?: ""))
|
||||
|
||||
commonTestHelper.signOutAndClose(aliceSession)
|
||||
commonTestHelper.signOutAndClose(aliceSession2)
|
||||
}
|
||||
|
||||
// See E2ESanityTest for a test regarding secret sharing
|
||||
|
||||
/**
|
||||
* Test that the sender of a message accepts to re-share to another user
|
||||
* if the key was originally shared with him
|
||||
*/
|
||||
@Test
|
||||
@Ignore("This test will be ignored until it is fixed")
|
||||
fun test_ShareSSSSSecret() {
|
||||
val aliceSession1 = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||
fun test_reShareIfWasIntendedToBeShared() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
|
||||
commonTestHelper.doSync<Unit> {
|
||||
aliceSession1.cryptoService().crossSigningService()
|
||||
.initializeCrossSigning(
|
||||
object : UserInteractiveAuthInterceptor {
|
||||
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||
promise.resume(
|
||||
UserPasswordAuth(
|
||||
user = aliceSession1.myUserId,
|
||||
password = TestConstants.PASSWORD
|
||||
)
|
||||
)
|
||||
}
|
||||
}, it
|
||||
)
|
||||
}
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = testData.firstSession
|
||||
val roomFromAlice = aliceSession.getRoom(testData.roomId)!!
|
||||
val bobSession = testData.secondSession!!
|
||||
|
||||
// Also bootstrap keybackup on first session
|
||||
val creationInfo = commonTestHelper.doSync<MegolmBackupCreationInfo> {
|
||||
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 sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first()
|
||||
val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||
|
||||
val aliceSession2 = commonTestHelper.logIntoAccount(aliceSession1.myUserId, SessionTestParams(true))
|
||||
// bob should be able to decrypt
|
||||
cryptoTestHelper.ensureCanDecrypt(listOf(sentEvent.eventId), bobSession, testData.roomId, listOf(sentEvent.getLastMessageContent()?.body ?: ""))
|
||||
|
||||
val aliceVerificationService1 = aliceSession1.cryptoService().verificationService()
|
||||
val aliceVerificationService2 = aliceSession2.cryptoService().verificationService()
|
||||
|
||||
// force keys download
|
||||
commonTestHelper.doSync<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
|
||||
)
|
||||
// Let's try to request any how.
|
||||
// As it was share previously alice should accept to reshare
|
||||
bobSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
|
||||
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.deviceId ?: "")?.isVerified == true
|
||||
val outgoing = bobSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
|
||||
val aliceReply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
|
||||
aliceReply != null && aliceReply.result is RequestResult.Success
|
||||
}
|
||||
}
|
||||
|
||||
assertNotNull(session1ShortCode)
|
||||
Log.d("#TEST", "session1ShortCode: $session1ShortCode")
|
||||
assertNotNull(session2ShortCode)
|
||||
Log.d("#TEST", "session2ShortCode: $session2ShortCode")
|
||||
assertEquals(session1ShortCode, session2ShortCode)
|
||||
|
||||
// SSK and USK private keys should have been shared
|
||||
|
||||
commonTestHelper.waitWithLatch(60_000) { latch ->
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
Log.d("#TEST", "CAN XS :${aliceSession2.cryptoService().crossSigningService().getMyCrossSigningKeys()}")
|
||||
aliceSession2.cryptoService().crossSigningService().canCrossSign()
|
||||
}
|
||||
}
|
||||
|
||||
// Test that key backup key has been shared to
|
||||
commonTestHelper.waitWithLatch(60_000) { latch ->
|
||||
val keysBackupService = aliceSession2.cryptoService().keysBackupService()
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
Log.d("#TEST", "Recovery :${keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
|
||||
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
|
||||
}
|
||||
}
|
||||
|
||||
commonTestHelper.signOutAndClose(aliceSession1)
|
||||
commonTestHelper.signOutAndClose(aliceSession2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that our own devices accept to reshare to unverified device if it was shared initialy
|
||||
* if the key was originally shared with him
|
||||
*/
|
||||
@Test
|
||||
@Ignore("This test will be ignored until it is fixed")
|
||||
fun test_ImproperKeyShareBug() {
|
||||
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||
fun test_reShareToUnverifiedIfWasIntendedToBeShared() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
|
||||
commonTestHelper.doSync<Unit> {
|
||||
aliceSession.cryptoService().crossSigningService()
|
||||
.initializeCrossSigning(
|
||||
object : UserInteractiveAuthInterceptor {
|
||||
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||
promise.resume(
|
||||
UserPasswordAuth(
|
||||
user = aliceSession.myUserId,
|
||||
password = TestConstants.PASSWORD,
|
||||
session = flowResponse.session
|
||||
)
|
||||
)
|
||||
}
|
||||
}, it
|
||||
)
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true)
|
||||
val aliceSession = testData.firstSession
|
||||
val roomFromAlice = aliceSession.getRoom(testData.roomId)!!
|
||||
|
||||
val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
|
||||
|
||||
// we wait for alice first session to be aware of that session?
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val newSession = aliceSession.cryptoService().getUserDevices(aliceSession.myUserId)
|
||||
.firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
|
||||
newSession != null
|
||||
}
|
||||
}
|
||||
val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first()
|
||||
val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||
|
||||
// Create an encrypted room and send a couple of messages
|
||||
val roomId = commonTestHelper.runBlockingTest {
|
||||
aliceSession.roomService().createRoom(
|
||||
CreateRoomParams().apply {
|
||||
visibility = RoomDirectoryVisibility.PRIVATE
|
||||
enableEncryption()
|
||||
}
|
||||
)
|
||||
// Let's try to request any how.
|
||||
// As it was share previously alice should accept to reshare
|
||||
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
|
||||
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
|
||||
val ownDeviceReply =
|
||||
outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
|
||||
ownDeviceReply != null && ownDeviceReply.result is RequestResult.Success
|
||||
}
|
||||
}
|
||||
val roomAlicePov = aliceSession.getRoom(roomId)
|
||||
assertNotNull(roomAlicePov)
|
||||
Thread.sleep(1_000)
|
||||
assertTrue(roomAlicePov?.roomCryptoService()?.isEncrypted() == true)
|
||||
val secondEventId = commonTestHelper.sendTextMessage(roomAlicePov!!, "Message", 3)[1].eventId
|
||||
}
|
||||
|
||||
// Create bob session
|
||||
/**
|
||||
* Tests that keys reshared with own verified session are done from the earliest known index
|
||||
*/
|
||||
@Test
|
||||
fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
|
||||
val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true))
|
||||
commonTestHelper.doSync<Unit> {
|
||||
bobSession.cryptoService().crossSigningService()
|
||||
.initializeCrossSigning(
|
||||
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
|
||||
)
|
||||
}
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = testData.firstSession
|
||||
val bobSession = testData.secondSession!!
|
||||
val roomFromBob = bobSession.getRoom(testData.roomId)!!
|
||||
|
||||
// Let alice invite bob
|
||||
commonTestHelper.runBlockingTest {
|
||||
roomAlicePov.membershipService().invite(bobSession.myUserId, null)
|
||||
}
|
||||
val sentEvents = commonTestHelper.sendTextMessage(roomFromBob, "Hello", 3)
|
||||
val sentEventMegolmSession = sentEvents.first().root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||
|
||||
commonTestHelper.runBlockingTest {
|
||||
bobSession.roomService().joinRoom(roomAlicePov.roomId, null, emptyList())
|
||||
}
|
||||
// Let alice now add a new session
|
||||
val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(false))
|
||||
aliceNewSession.cryptoService().enableKeyGossiping(false)
|
||||
commonTestHelper.syncSession(aliceNewSession)
|
||||
|
||||
// we want to discard alice outbound session
|
||||
aliceSession.cryptoService().discardOutboundSession(roomAlicePov.roomId)
|
||||
|
||||
// and now resend a new message to reset index to 0
|
||||
commonTestHelper.sendTextMessage(roomAlicePov, "After", 1)
|
||||
|
||||
val roomRoomBobPov = aliceSession.getRoom(roomId)
|
||||
val beforeJoin = roomRoomBobPov!!.getTimelineEvent(secondEventId)
|
||||
|
||||
var dRes = tryOrNull {
|
||||
commonTestHelper.runBlockingTest {
|
||||
bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "")
|
||||
// we wait bob first session to be aware of that session?
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId)
|
||||
.firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
|
||||
newSession != null
|
||||
}
|
||||
}
|
||||
|
||||
assert(dRes == null)
|
||||
val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first()
|
||||
val newEventId = newEvent.eventId
|
||||
val newEventText = newEvent.getLastMessageContent()!!.body
|
||||
|
||||
// Try to re-ask the keys
|
||||
// alice should be able to decrypt the new one
|
||||
cryptoTestHelper.ensureCanDecrypt(listOf(newEventId), aliceNewSession, testData.roomId, listOf(newEventText))
|
||||
// but not the first one!
|
||||
cryptoTestHelper.ensureCannotDecrypt(sentEvents.map { it.eventId }, aliceNewSession, testData.roomId)
|
||||
|
||||
bobSession.cryptoService().reRequestRoomKeyForEvent(beforeJoin!!.root)
|
||||
// All should be using the same session id
|
||||
sentEvents.forEach {
|
||||
assertEquals(sentEventMegolmSession, it.root.content.toModel<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 :/
|
||||
dRes = tryOrNull {
|
||||
commonTestHelper.runBlockingTest {
|
||||
bobSession.cryptoService().decryptEvent(beforeJoin.root, "")
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
|
||||
val ownDeviceReply = outgoing?.results
|
||||
?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
|
||||
val result = ownDeviceReply?.result
|
||||
Log.v("TEST", "own device result is $result")
|
||||
result != null && result is RequestResult.Failure && result.code == WithHeldCode.UNVERIFIED
|
||||
}
|
||||
}
|
||||
Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel<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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||
import androidx.test.filters.LargeTest
|
||||
import org.junit.Assert
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
|
@ -29,6 +28,7 @@ import org.matrix.android.sdk.InstrumentedTest
|
|||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.crypto.RequestResult
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||
|
@ -46,12 +46,11 @@ import org.matrix.android.sdk.common.TestConstants
|
|||
@LargeTest
|
||||
class WithHeldTests : InstrumentedTest {
|
||||
|
||||
private val testHelper = CommonTestHelper(context())
|
||||
private val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
|
||||
@Test
|
||||
@Ignore("This test will be ignored until it is fixed")
|
||||
fun test_WithHeldUnverifiedReason() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
|
||||
// =============================
|
||||
// ARRANGE
|
||||
// =============================
|
||||
|
@ -69,7 +68,6 @@ class WithHeldTests : InstrumentedTest {
|
|||
val roomAlicePOV = aliceSession.getRoom(roomId)!!
|
||||
|
||||
val bobUnverifiedSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
|
||||
|
||||
// =============================
|
||||
// ACT
|
||||
// =============================
|
||||
|
@ -88,6 +86,7 @@ class WithHeldTests : InstrumentedTest {
|
|||
|
||||
val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!!
|
||||
|
||||
val megolmSessionId = eventBobPOV.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||
// =============================
|
||||
// ASSERT
|
||||
// =============================
|
||||
|
@ -103,9 +102,23 @@ class WithHeldTests : InstrumentedTest {
|
|||
val type = (failure as MXCryptoError.Base).errorType
|
||||
val technicalMessage = failure.technicalMessage
|
||||
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
|
||||
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
|
||||
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNAUTHORISED.value, technicalMessage)
|
||||
}
|
||||
|
||||
// Let's see if the reply we got from bob first session is unverified
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
bobUnverifiedSession.cryptoService().getOutgoingRoomKeyRequests()
|
||||
.firstOrNull { it.sessionId == megolmSessionId }
|
||||
?.results
|
||||
?.firstOrNull { it.fromDevice == bobSession.sessionParams.deviceId }
|
||||
?.result
|
||||
?.let {
|
||||
it as? RequestResult.Failure
|
||||
}
|
||||
?.code == WithHeldCode.UNVERIFIED
|
||||
}
|
||||
}
|
||||
// enable back sending to unverified
|
||||
aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false)
|
||||
|
||||
|
@ -130,7 +143,7 @@ class WithHeldTests : InstrumentedTest {
|
|||
val type = (failure as MXCryptoError.Base).errorType
|
||||
val technicalMessage = failure.technicalMessage
|
||||
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
|
||||
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
|
||||
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNAUTHORISED.value, technicalMessage)
|
||||
}
|
||||
|
||||
testHelper.signOutAndClose(aliceSession)
|
||||
|
@ -139,8 +152,10 @@ class WithHeldTests : InstrumentedTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@Ignore("This test will be ignored until it is fixed")
|
||||
fun test_WithHeldNoOlm() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
val aliceSession = testData.firstSession
|
||||
val bobSession = testData.secondSession!!
|
||||
|
@ -220,8 +235,10 @@ class WithHeldTests : InstrumentedTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@Ignore("This test will be ignored until it is fixed")
|
||||
fun test_WithHeldKeyRequest() {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
val aliceSession = testData.firstSession
|
||||
val bobSession = testData.secondSession!!
|
||||
|
@ -267,5 +284,8 @@ class WithHeldTests : InstrumentedTest {
|
|||
wc?.code == WithHeldCode.UNAUTHORISED
|
||||
}
|
||||
}
|
||||
|
||||
testHelper.signOutAndClose(aliceSession)
|
||||
testHelper.signOutAndClose(bobSecondSession)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.crypto.verification.qrcode
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
|
@ -40,7 +39,6 @@ import kotlin.coroutines.resume
|
|||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
@Ignore("This test is flaky ; see issue #5449")
|
||||
class VerificationTest : InstrumentedTest {
|
||||
|
||||
data class ExpectedResult(
|
||||
|
|
|
@ -31,5 +31,11 @@ data class MXCryptoConfig constructor(
|
|||
* If set to false, the request will be forwarded to the application layer; in this
|
||||
* case the application can decide to prompt the user.
|
||||
*/
|
||||
val discardRoomKeyRequestsFromUntrustedDevices: Boolean = true
|
||||
val discardRoomKeyRequestsFromUntrustedDevices: Boolean = true,
|
||||
|
||||
/**
|
||||
* Currently megolm keys are requested to the sender device and to all of our devices.
|
||||
* You can limit request only to your sessions by turning this setting to `true`
|
||||
*/
|
||||
val limitRoomKeyRequestsToMyDevices: Boolean = false,
|
||||
)
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningServic
|
|||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
|
||||
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
|
||||
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
|
||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
|
||||
|
@ -35,8 +36,6 @@ import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo
|
|||
import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
|
@ -76,6 +75,15 @@ interface CryptoService {
|
|||
|
||||
fun setGlobalBlacklistUnverifiedDevices(block: Boolean)
|
||||
|
||||
/**
|
||||
* Enable or disable key gossiping.
|
||||
* Default is true.
|
||||
* If set to false this device won't send key_request nor will accept key forwarded
|
||||
*/
|
||||
fun enableKeyGossiping(enable: Boolean)
|
||||
|
||||
fun isKeyGossipingEnabled(): Boolean
|
||||
|
||||
fun setRoomUnBlacklistUnverifiedDevices(roomId: String)
|
||||
|
||||
fun getDeviceTrackingStatus(userId: String): Int
|
||||
|
@ -94,8 +102,6 @@ interface CryptoService {
|
|||
|
||||
fun reRequestRoomKeyForEvent(event: Event)
|
||||
|
||||
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody)
|
||||
|
||||
fun addRoomKeysRequestListener(listener: GossipingRequestListener)
|
||||
|
||||
fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
|
||||
|
@ -142,14 +148,20 @@ interface CryptoService {
|
|||
fun addNewSessionListener(newSessionListener: NewSessionListener)
|
||||
fun removeSessionListener(listener: NewSessionListener)
|
||||
|
||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
||||
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
|
||||
fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest>
|
||||
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>>
|
||||
|
||||
fun getIncomingRoomKeyRequests(): List<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
|
||||
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -14,14 +14,20 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.crypto.model
|
||||
package org.matrix.android.sdk.api.session.crypto
|
||||
|
||||
enum class OutgoingGossipingRequestState {
|
||||
enum class OutgoingRoomKeyRequestState {
|
||||
UNSENT,
|
||||
SENDING,
|
||||
SENT,
|
||||
CANCELLING,
|
||||
CANCELLED,
|
||||
FAILED_TO_SEND,
|
||||
FAILED_TO_CANCEL
|
||||
SENT_THEN_CANCELED,
|
||||
CANCELLATION_PENDING,
|
||||
CANCELLATION_PENDING_AND_WILL_RESEND;
|
||||
|
||||
companion object {
|
||||
fun pendingStates() = setOf(
|
||||
UNSENT,
|
||||
CANCELLATION_PENDING_AND_WILL_RESEND,
|
||||
CANCELLATION_PENDING
|
||||
)
|
||||
}
|
||||
}
|
|
@ -16,9 +16,8 @@
|
|||
|
||||
package org.matrix.android.sdk.api.session.crypto.keyshare
|
||||
|
||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRequestCancellation
|
||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
|
||||
|
||||
/**
|
||||
* Room keys events listener
|
||||
|
@ -35,12 +34,12 @@ interface GossipingRequestListener {
|
|||
* Returns the secret value to be shared
|
||||
* @return true if is handled
|
||||
*/
|
||||
fun onSecretShareRequest(request: IncomingSecretShareRequest): Boolean
|
||||
fun onSecretShareRequest(request: SecretShareRequest): Boolean
|
||||
|
||||
/**
|
||||
* A room key request cancellation has been received.
|
||||
*
|
||||
* @param request the cancellation request
|
||||
*/
|
||||
fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation)
|
||||
fun onRequestCancelled(request: IncomingRoomKeyRequest)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,9 +16,7 @@
|
|||
|
||||
package org.matrix.android.sdk.api.session.crypto.model
|
||||
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
|
||||
/**
|
||||
* IncomingRoomKeyRequest class defines the incoming room keys request.
|
||||
|
@ -27,38 +25,25 @@ data class IncomingRoomKeyRequest(
|
|||
/**
|
||||
* The user id
|
||||
*/
|
||||
override val userId: String? = null,
|
||||
val userId: String? = null,
|
||||
|
||||
/**
|
||||
* The device id
|
||||
*/
|
||||
override val deviceId: String? = null,
|
||||
val deviceId: String? = null,
|
||||
|
||||
/**
|
||||
* The request id
|
||||
*/
|
||||
override val requestId: String? = null,
|
||||
val requestId: String? = null,
|
||||
|
||||
/**
|
||||
* The request body
|
||||
*/
|
||||
val requestBody: RoomKeyRequestBody? = null,
|
||||
|
||||
val state: GossipingRequestState = GossipingRequestState.NONE,
|
||||
|
||||
/**
|
||||
* The runnable to call to accept to share the keys
|
||||
*/
|
||||
@Transient
|
||||
var share: Runnable? = null,
|
||||
|
||||
/**
|
||||
* The runnable to call to ignore the key share request.
|
||||
*/
|
||||
@Transient
|
||||
var ignore: Runnable? = null,
|
||||
override val localCreationTimestamp: Long?
|
||||
) : IncomingShareRequestCommon {
|
||||
val localCreationTimestamp: Long?
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Factory
|
||||
|
@ -66,18 +51,36 @@ data class IncomingRoomKeyRequest(
|
|||
* @param event the event
|
||||
* @param currentTimeMillis the current time in milliseconds
|
||||
*/
|
||||
fun fromEvent(event: Event, currentTimeMillis: Long): IncomingRoomKeyRequest? {
|
||||
return event.getClearContent()
|
||||
.toModel<RoomKeyShareRequest>()
|
||||
fun fromEvent(trail: AuditTrail): IncomingRoomKeyRequest? {
|
||||
return trail
|
||||
.takeIf { it.type == TrailType.IncomingKeyRequest }
|
||||
?.let {
|
||||
it.info as? IncomingKeyRequestInfo
|
||||
}
|
||||
?.let {
|
||||
IncomingRoomKeyRequest(
|
||||
userId = event.senderId,
|
||||
deviceId = it.requestingDeviceId,
|
||||
userId = it.userId,
|
||||
deviceId = it.deviceId,
|
||||
requestId = it.requestId,
|
||||
requestBody = it.body ?: RoomKeyRequestBody(),
|
||||
localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis
|
||||
requestBody = RoomKeyRequestBody(
|
||||
algorithm = it.alg,
|
||||
roomId = it.roomId,
|
||||
senderKey = it.senderKey,
|
||||
sessionId = it.sessionId
|
||||
),
|
||||
localCreationTimestamp = trail.ageLocalTs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun fromRestRequest(senderId: String, request: RoomKeyShareRequest, clock: Clock): IncomingRoomKeyRequest? {
|
||||
return IncomingRoomKeyRequest(
|
||||
userId = senderId,
|
||||
deviceId = request.requestingDeviceId,
|
||||
requestId = request.requestId,
|
||||
requestBody = request.body,
|
||||
localCreationTimestamp = clock.epochMillis()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -52,7 +52,13 @@ data class RoomKeyWithHeldContent(
|
|||
/**
|
||||
* A human-readable reason for why the key was not sent. The receiving client should only use this string if it does not understand the code.
|
||||
*/
|
||||
@Json(name = "reason") val reason: String? = null
|
||||
@Json(name = "reason") val reason: String? = null,
|
||||
|
||||
/**
|
||||
* the device ID of the device sending the m.room_key.withheld message
|
||||
* MSC3735
|
||||
*/
|
||||
@Json(name = "from_device") val fromDevice: String? = null
|
||||
|
||||
) {
|
||||
val code: WithHeldCode?
|
||||
|
|
|
@ -131,7 +131,7 @@ interface SharedSecretStorageService {
|
|||
|
||||
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?): IntegrityResult
|
||||
|
||||
fun requestSecret(name: String, myOtherDeviceId: String)
|
||||
suspend fun requestSecret(name: String, myOtherDeviceId: String)
|
||||
|
||||
data class KeyRef(
|
||||
val keyId: String?,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -42,12 +42,14 @@ import org.matrix.android.sdk.api.logger.LoggerTag
|
|||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
||||
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
|
||||
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
|
||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
|
||||
|
@ -57,15 +59,13 @@ import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo
|
|||
import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.TrailType
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||
|
@ -76,7 +76,6 @@ import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
|
|||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
|
||||
|
@ -91,6 +90,7 @@ import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask
|
|||
import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
|
||||
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
|
||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor
|
||||
import org.matrix.android.sdk.internal.di.DeviceId
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
|
@ -156,9 +156,10 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
|
||||
private val crossSigningService: DefaultCrossSigningService,
|
||||
//
|
||||
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||
private val incomingKeyRequestManager: IncomingKeyRequestManager,
|
||||
private val secretShareManager: SecretShareManager,
|
||||
//
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||
// Actions
|
||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||
private val megolmSessionDataImporter: MegolmSessionDataImporter,
|
||||
|
@ -178,6 +179,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
private val taskExecutor: TaskExecutor,
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val eventDecryptor: EventDecryptor,
|
||||
private val verificationMessageProcessor: VerificationMessageProcessor,
|
||||
private val liveEventManager: Lazy<StreamEventsManager>
|
||||
) : CryptoService {
|
||||
|
||||
|
@ -192,7 +194,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun onLiveEvent(roomId: String, event: Event) {
|
||||
fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean) {
|
||||
// handle state events
|
||||
if (event.isStateEvent()) {
|
||||
when (event.type) {
|
||||
|
@ -201,9 +203,18 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
|
||||
}
|
||||
}
|
||||
|
||||
// handle verification
|
||||
if (!isInitialSync) {
|
||||
if (event.type != null && verificationMessageProcessor.shouldProcess(event.type)) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) {
|
||||
verificationMessageProcessor.process(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val gossipingBuffer = mutableListOf<Event>()
|
||||
// val gossipingBuffer = mutableListOf<Event>()
|
||||
|
||||
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
|
||||
setDeviceNameTask
|
||||
|
@ -379,27 +390,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
// Open the store
|
||||
cryptoStore.open()
|
||||
|
||||
runCatching {
|
||||
// if (isInitialSync) {
|
||||
// // refresh the devices list for each known room members
|
||||
// deviceListManager.invalidateAllDeviceLists()
|
||||
// deviceListManager.refreshOutdatedDeviceLists()
|
||||
// } else {
|
||||
|
||||
// Why would we do that? it will be called at end of syn
|
||||
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||
// }
|
||||
}.fold(
|
||||
{
|
||||
isStarting.set(false)
|
||||
isStarted.set(true)
|
||||
},
|
||||
{
|
||||
isStarting.set(false)
|
||||
isStarted.set(false)
|
||||
Timber.tag(loggerTag.value).e(it, "Start failed")
|
||||
}
|
||||
)
|
||||
isStarting.set(false)
|
||||
isStarted.set(true)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -407,7 +399,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
*/
|
||||
fun close() = runBlocking(coroutineDispatchers.crypto) {
|
||||
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
|
||||
incomingGossipingRequestManager.close()
|
||||
incomingKeyRequestManager.close()
|
||||
outgoingKeyRequestManager.close()
|
||||
olmDevice.release()
|
||||
cryptoStore.close()
|
||||
}
|
||||
|
@ -472,15 +465,28 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
}
|
||||
|
||||
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
||||
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||
}
|
||||
}
|
||||
|
||||
tryOrNull {
|
||||
gossipingBuffer.toList().let {
|
||||
cryptoStore.saveGossipingEvents(it)
|
||||
// Process pending key requests
|
||||
try {
|
||||
if (toDevices.isEmpty()) {
|
||||
// this is not blocking
|
||||
outgoingKeyRequestManager.requireProcessAllPendingKeyRequests()
|
||||
} else {
|
||||
Timber.tag(loggerTag.value)
|
||||
.w("Don't process key requests yet as there might be more to_device to catchup")
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
// just for safety but should not throw
|
||||
Timber.tag(loggerTag.value).w("failed to process pending request")
|
||||
}
|
||||
|
||||
try {
|
||||
incomingKeyRequestManager.processIncomingRequests()
|
||||
} catch (failure: Throwable) {
|
||||
// just for safety but should not throw
|
||||
Timber.tag(loggerTag.value).w("failed to process incoming room key requests")
|
||||
}
|
||||
gossipingBuffer.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -594,7 +600,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
// (for now at least. Maybe we should alert the user somehow?)
|
||||
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
|
||||
|
||||
if (existingAlgorithm == algorithm && roomEncryptorsStore.get(roomId) != null) {
|
||||
if (existingAlgorithm == algorithm) {
|
||||
// ignore
|
||||
Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in $roomId")
|
||||
return false
|
||||
|
@ -787,19 +793,25 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
when (event.getClearType()) {
|
||||
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
||||
gossipingBuffer.add(event)
|
||||
// Keys are imported directly, not waiting for end of sync
|
||||
onRoomKeyEvent(event)
|
||||
}
|
||||
EventType.REQUEST_SECRET,
|
||||
EventType.REQUEST_SECRET -> {
|
||||
secretShareManager.handleSecretRequest(event)
|
||||
}
|
||||
EventType.ROOM_KEY_REQUEST -> {
|
||||
// save audit trail
|
||||
gossipingBuffer.add(event)
|
||||
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
|
||||
incomingGossipingRequestManager.onGossipingRequestEvent(event)
|
||||
event.getClearContent().toModel<RoomKeyShareRequest>()?.let { req ->
|
||||
// We'll always get these because we send room key requests to
|
||||
// '*' (ie. 'all devices') which includes the sending device,
|
||||
// so ignore requests from ourself because apart from it being
|
||||
// very silly, it won't work because an Olm session cannot send
|
||||
// messages to itself.
|
||||
if (req.requestingDeviceId != deviceId) { // ignore self requests
|
||||
event.senderId?.let { incomingKeyRequestManager.addNewIncomingRequest(it, req) }
|
||||
}
|
||||
}
|
||||
}
|
||||
EventType.SEND_SECRET -> {
|
||||
gossipingBuffer.add(event)
|
||||
onSecretSendReceived(event)
|
||||
}
|
||||
EventType.ROOM_KEY_WITHHELD -> {
|
||||
|
@ -837,50 +849,38 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
val withHeldContent = event.getClearContent().toModel<RoomKeyWithHeldContent>() ?: return Unit.also {
|
||||
Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields")
|
||||
}
|
||||
Timber.tag(loggerTag.value).i("onKeyWithHeldReceived() received from:${event.senderId}, content <$withHeldContent>")
|
||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm)
|
||||
if (alg is IMXWithHeldExtension) {
|
||||
alg.onRoomKeyWithHeldEvent(withHeldContent)
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).e("onKeyWithHeldReceived() from:${event.senderId}: Unable to handle WithHeldContent for ${withHeldContent.algorithm}")
|
||||
return
|
||||
val senderId = event.senderId ?: return Unit.also {
|
||||
Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields")
|
||||
}
|
||||
withHeldContent.sessionId ?: return
|
||||
withHeldContent.algorithm ?: return
|
||||
withHeldContent.roomId ?: return
|
||||
withHeldContent.senderKey ?: return
|
||||
outgoingKeyRequestManager.onRoomKeyWithHeld(
|
||||
sessionId = withHeldContent.sessionId,
|
||||
algorithm = withHeldContent.algorithm,
|
||||
roomId = withHeldContent.roomId,
|
||||
senderKey = withHeldContent.senderKey,
|
||||
fromDevice = withHeldContent.fromDevice,
|
||||
event = Event(
|
||||
type = EventType.ROOM_KEY_WITHHELD,
|
||||
senderId = senderId,
|
||||
content = event.getClearContent()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSecretSendReceived(event: Event) {
|
||||
Timber.tag(loggerTag.value).i("GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}")
|
||||
if (!event.isEncrypted()) {
|
||||
// secret send messages must be encrypted
|
||||
Timber.tag(loggerTag.value).e("GOSSIP onSecretSend() :Received unencrypted secret send event")
|
||||
return
|
||||
}
|
||||
|
||||
// Was that sent by us?
|
||||
if (event.senderId != userId) {
|
||||
Timber.tag(loggerTag.value).e("GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}")
|
||||
return
|
||||
}
|
||||
|
||||
val secretContent = event.getClearContent().toModel<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")
|
||||
private suspend fun onSecretSendReceived(event: Event) {
|
||||
secretShareManager.onSecretSendReceived(event) { secretName, secretValue ->
|
||||
handleSDKLevelGossip(secretName, secretValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if handled by SDK, otherwise should be sent to application layer
|
||||
*/
|
||||
private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean {
|
||||
private fun handleSDKLevelGossip(secretName: String?,
|
||||
secretValue: String): Boolean {
|
||||
return when (secretName) {
|
||||
MASTER_KEY_SSSS_NAME -> {
|
||||
crossSigningService.onSecretMSKGossip(secretValue)
|
||||
|
@ -1095,6 +1095,12 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
cryptoStore.setGlobalBlacklistUnverifiedDevices(block)
|
||||
}
|
||||
|
||||
override fun enableKeyGossiping(enable: Boolean) {
|
||||
cryptoStore.enableKeyGossiping(enable)
|
||||
}
|
||||
|
||||
override fun isKeyGossipingEnabled() = cryptoStore.isKeyGossipingEnabled()
|
||||
|
||||
/**
|
||||
* Tells whether the client should ever send encrypted messages to unverified devices.
|
||||
* The default value is false.
|
||||
|
@ -1158,52 +1164,17 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
setRoomBlacklistUnverifiedDevices(roomId, false)
|
||||
}
|
||||
|
||||
// TODO Check if this method is still necessary
|
||||
/**
|
||||
* Cancel any earlier room key request
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
*/
|
||||
override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||
outgoingGossipingRequestManager.cancelRoomKeyRequest(requestBody)
|
||||
}
|
||||
|
||||
/**
|
||||
* Re request the encryption keys required to decrypt an event.
|
||||
*
|
||||
* @param event the event to decrypt again.
|
||||
*/
|
||||
override fun reRequestRoomKeyForEvent(event: Event) {
|
||||
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
|
||||
Timber.tag(loggerTag.value).e("reRequestRoomKeyForEvent Failed to re-request key, null content")
|
||||
}
|
||||
|
||||
val requestBody = RoomKeyRequestBody(
|
||||
algorithm = wireContent.algorithm,
|
||||
roomId = event.roomId,
|
||||
senderKey = wireContent.senderKey,
|
||||
sessionId = wireContent.sessionId
|
||||
)
|
||||
|
||||
outgoingGossipingRequestManager.resendRoomKeyRequest(requestBody)
|
||||
outgoingKeyRequestManager.requestKeyForEvent(event, true)
|
||||
}
|
||||
|
||||
override fun requestRoomKeyForEvent(event: Event) {
|
||||
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
|
||||
Timber.tag(loggerTag.value).e("requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}")
|
||||
}
|
||||
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
// if (!isStarted()) {
|
||||
// Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init")
|
||||
// internalStart(false)
|
||||
// }
|
||||
roomDecryptorProvider
|
||||
.getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
|
||||
?.requestKeysForEvent(event, false) ?: run {
|
||||
Timber.tag(loggerTag.value).v("requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
|
||||
}
|
||||
}
|
||||
outgoingKeyRequestManager.requestKeyForEvent(event, false)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1212,7 +1183,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
* @param listener listener
|
||||
*/
|
||||
override fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||
incomingGossipingRequestManager.addRoomKeysRequestListener(listener)
|
||||
incomingKeyRequestManager.addRoomKeysRequestListener(listener)
|
||||
secretShareManager.addListener(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1221,42 +1193,10 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
* @param listener listener
|
||||
*/
|
||||
override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||
incomingGossipingRequestManager.removeRoomKeysRequestListener(listener)
|
||||
incomingKeyRequestManager.removeRoomKeysRequestListener(listener)
|
||||
secretShareManager.removeListener(listener)
|
||||
}
|
||||
|
||||
// private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
|
||||
// val deviceKey = deviceInfo.identityKey()
|
||||
//
|
||||
// val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
|
||||
// val now = clock.epochMillis()
|
||||
// if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
|
||||
// Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
|
||||
// lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
|
||||
//
|
||||
// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
// ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
|
||||
//
|
||||
// // Now send a blank message on that session so the other side knows about it.
|
||||
// // (The keyshare request is sent in the clear so that won't do)
|
||||
// // We send this first such that, as long as the toDevice messages arrive in the
|
||||
// // same order we sent them, the other end will get this first, set up the new session,
|
||||
// // then get the keyshare request and send the key over this new session (because it
|
||||
// // is the session it has most recently received a message on).
|
||||
// val payloadJson = mapOf<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
|
||||
*
|
||||
|
@ -1302,27 +1242,41 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
return "DefaultCryptoService of $userId ($deviceId)"
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> {
|
||||
override fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest> {
|
||||
return cryptoStore.getOutgoingRoomKeyRequests()
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
|
||||
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>> {
|
||||
return cryptoStore.getOutgoingRoomKeyRequestsPaged()
|
||||
}
|
||||
|
||||
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
|
||||
return cryptoStore.getIncomingRoomKeyRequestsPaged()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
override fun getGossipingEvents(): List<Event> {
|
||||
override fun getGossipingEvents(): List<AuditTrail> {
|
||||
return cryptoStore.getGossipingEvents()
|
||||
}
|
||||
|
||||
|
@ -1346,8 +1300,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
|
||||
} catch (failure: Throwable) {
|
||||
Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members")
|
||||
callback.onFailure(failure)
|
||||
return@launch
|
||||
// we probably shouldn't block sending on that (but questionable)
|
||||
// but some members won't be able to decrypt
|
||||
}
|
||||
|
||||
val userIds = getRoomUserIds(roomId)
|
||||
|
|
|
@ -315,10 +315,19 @@ internal class DeviceListManager @Inject constructor(
|
|||
} else {
|
||||
Timber.v("## CRYPTO | downloadKeys() : starts")
|
||||
val t0 = clock.epochMillis()
|
||||
val result = doKeyDownloadForUsers(downloadUsers)
|
||||
Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms")
|
||||
result.also {
|
||||
it.addEntriesFromMap(stored)
|
||||
try {
|
||||
val result = doKeyDownloadForUsers(downloadUsers)
|
||||
Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms")
|
||||
result.also {
|
||||
it.addEntriesFromMap(stored)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.w(failure, "## CRYPTO | downloadKeys() : doKeyDownloadForUsers failed after ${clock.epochMillis() - t0} ms")
|
||||
if (forceDownload) {
|
||||
throw failure
|
||||
} else {
|
||||
stored
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
}
|
|
@ -43,7 +43,6 @@ import org.matrix.olm.OlmOutboundGroupSession
|
|||
import org.matrix.olm.OlmSession
|
||||
import org.matrix.olm.OlmUtility
|
||||
import timber.log.Timber
|
||||
import java.net.URLEncoder
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO)
|
||||
|
@ -331,14 +330,6 @@ internal class MXOlmDevice @Inject constructor(
|
|||
Timber.tag(loggerTag.value).e(e, "## createInboundSession() : removeOneTimeKeys failed")
|
||||
}
|
||||
|
||||
Timber.tag(loggerTag.value).v("## createInboundSession() : ciphertext: $ciphertext")
|
||||
try {
|
||||
val sha256 = olmUtility!!.sha256(URLEncoder.encode(ciphertext, "utf-8"))
|
||||
Timber.tag(loggerTag.value).v("## createInboundSession() :ciphertext: SHA256: $sha256")
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(loggerTag.value).e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext")
|
||||
}
|
||||
|
||||
val olmMessage = OlmMessage()
|
||||
olmMessage.mCipherText = ciphertext
|
||||
olmMessage.mType = messageType.toLong()
|
||||
|
@ -589,6 +580,13 @@ internal class MXOlmDevice @Inject constructor(
|
|||
|
||||
// Inbound group session
|
||||
|
||||
sealed interface AddSessionResult {
|
||||
data class Imported(val ratchetIndex: Int) : AddSessionResult
|
||||
abstract class Failure : AddSessionResult
|
||||
object NotImported : Failure()
|
||||
data class NotImportedHigherIndex(val newIndex: Int) : Failure()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an inbound group session to the session store.
|
||||
*
|
||||
|
@ -607,7 +605,7 @@ internal class MXOlmDevice @Inject constructor(
|
|||
senderKey: String,
|
||||
forwardingCurve25519KeyChain: List<String>,
|
||||
keysClaimed: Map<String, String>,
|
||||
exportFormat: Boolean): Boolean {
|
||||
exportFormat: Boolean): AddSessionResult {
|
||||
val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
|
||||
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
|
||||
val existingSession = existingSessionHolder?.wrapper
|
||||
|
@ -615,7 +613,7 @@ internal class MXOlmDevice @Inject constructor(
|
|||
if (existingSession != null) {
|
||||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session")
|
||||
try {
|
||||
val existingFirstKnown = existingSession.firstKnownIndex ?: return false.also {
|
||||
val existingFirstKnown = existingSession.firstKnownIndex ?: return AddSessionResult.NotImported.also {
|
||||
// This is quite unexpected, could throw if native was released?
|
||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
|
||||
candidateSession.olmInboundGroupSession?.releaseSession()
|
||||
|
@ -626,12 +624,12 @@ internal class MXOlmDevice @Inject constructor(
|
|||
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
|
||||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
|
||||
candidateSession.olmInboundGroupSession?.releaseSession()
|
||||
return false
|
||||
return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt())
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
|
||||
candidateSession.olmInboundGroupSession?.releaseSession()
|
||||
return false
|
||||
return AddSessionResult.NotImported
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -641,19 +639,19 @@ internal class MXOlmDevice @Inject constructor(
|
|||
val candidateOlmInboundSession = candidateSession.olmInboundGroupSession
|
||||
if (null == candidateOlmInboundSession) {
|
||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>")
|
||||
return false
|
||||
return AddSessionResult.NotImported
|
||||
}
|
||||
|
||||
try {
|
||||
if (candidateOlmInboundSession.sessionIdentifier() != sessionId) {
|
||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
||||
candidateOlmInboundSession.releaseSession()
|
||||
return false
|
||||
return AddSessionResult.NotImported
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
candidateOlmInboundSession.releaseSession()
|
||||
Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed")
|
||||
return false
|
||||
return AddSessionResult.NotImported
|
||||
}
|
||||
|
||||
candidateSession.senderKey = senderKey
|
||||
|
@ -667,7 +665,7 @@ internal class MXOlmDevice @Inject constructor(
|
|||
inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
|
||||
}
|
||||
|
||||
return true
|
||||
return AddSessionResult.Imported(candidateSession.firstKnownIndex?.toInt() ?: 0)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -790,7 +788,7 @@ internal class MXOlmDevice @Inject constructor(
|
|||
|
||||
if (timelineSet.contains(messageIndexKey)) {
|
||||
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
|
||||
Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason")
|
||||
Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
|
||||
}
|
||||
|
||||
|
|
|
@ -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?
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,12 +16,21 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class RoomEncryptorsStore @Inject constructor() {
|
||||
internal class RoomEncryptorsStore @Inject constructor(
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val megolmEncryptionFactory: MXMegolmEncryptionFactory,
|
||||
private val olmEncryptionFactory: MXOlmEncryptionFactory,
|
||||
) {
|
||||
|
||||
// MXEncrypting instance for each room.
|
||||
private val roomEncryptors = mutableMapOf<String, IMXEncrypting>()
|
||||
|
@ -34,7 +43,18 @@ internal class RoomEncryptorsStore @Inject constructor() {
|
|||
|
||||
fun get(roomId: String): IMXEncrypting? {
|
||||
return synchronized(roomEncryptors) {
|
||||
roomEncryptors[roomId]
|
||||
val cache = roomEncryptors[roomId]
|
||||
if (cache != null) {
|
||||
return@synchronized cache
|
||||
} else {
|
||||
val alg: IMXEncrypting? = when (cryptoStore.getRoomAlgorithm(roomId)) {
|
||||
MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId)
|
||||
MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId)
|
||||
else -> null
|
||||
}
|
||||
alg?.let { roomEncryptors.put(roomId, it) }
|
||||
return@synchronized alg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -17,12 +17,13 @@
|
|||
package org.matrix.android.sdk.internal.crypto.actions
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.RoomDecryptorProvider
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
|
@ -30,12 +31,13 @@ import org.matrix.android.sdk.internal.util.time.Clock
|
|||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class MegolmSessionDataImporter @Inject constructor(
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val clock: Clock,
|
||||
private val loggerTag = LoggerTag("MegolmSessionDataImporter", LoggerTag.CRYPTO)
|
||||
|
||||
internal class MegolmSessionDataImporter @Inject constructor(private val olmDevice: MXOlmDevice,
|
||||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val clock: Clock,
|
||||
) {
|
||||
|
||||
/**
|
||||
|
@ -66,19 +68,23 @@ internal class MegolmSessionDataImporter @Inject constructor(
|
|||
if (null != decrypting) {
|
||||
try {
|
||||
val sessionId = megolmSessionData.sessionId
|
||||
Timber.v("## importRoomKeys retrieve senderKey " + megolmSessionData.senderKey + " sessionId " + sessionId)
|
||||
Timber.tag(loggerTag.value).v("## importRoomKeys retrieve senderKey ${megolmSessionData.senderKey} sessionId $sessionId")
|
||||
|
||||
totalNumbersOfImportedKeys++
|
||||
|
||||
// cancel any outstanding room key requests for this session
|
||||
val roomKeyRequestBody = RoomKeyRequestBody(
|
||||
algorithm = megolmSessionData.algorithm,
|
||||
roomId = megolmSessionData.roomId,
|
||||
senderKey = megolmSessionData.senderKey,
|
||||
sessionId = megolmSessionData.sessionId
|
||||
)
|
||||
|
||||
outgoingGossipingRequestManager.cancelRoomKeyRequest(roomKeyRequestBody)
|
||||
Timber.tag(loggerTag.value).d("Imported megolm session $sessionId from backup=$fromBackup in ${megolmSessionData.roomId}")
|
||||
outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(
|
||||
megolmSessionData.sessionId ?: "",
|
||||
megolmSessionData.roomId ?: "",
|
||||
megolmSessionData.senderKey ?: "",
|
||||
tryOrNull {
|
||||
olmInboundGroupSessionWrappers
|
||||
.firstOrNull { it.olmInboundGroupSession?.sessionIdentifier() == megolmSessionData.sessionId }
|
||||
?.firstKnownIndex?.toInt()
|
||||
} ?: 0
|
||||
)
|
||||
|
||||
// Have another go at decrypting events sent with this session
|
||||
when (decrypting) {
|
||||
|
@ -87,7 +93,7 @@ internal class MegolmSessionDataImporter @Inject constructor(
|
|||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## importRoomKeys() : onNewSession failed")
|
||||
Timber.tag(loggerTag.value).e(e, "## importRoomKeys() : onNewSession failed")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,7 +115,7 @@ internal class MegolmSessionDataImporter @Inject constructor(
|
|||
|
||||
val t1 = clock.epochMillis()
|
||||
|
||||
Timber.v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)")
|
||||
Timber.tag(loggerTag.value).v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)")
|
||||
|
||||
return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys)
|
||||
}
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
package org.matrix.android.sdk.internal.crypto.algorithms
|
||||
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||
|
@ -44,23 +42,4 @@ internal interface IMXDecrypting {
|
|||
* @param event the key event.
|
||||
*/
|
||||
fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {}
|
||||
|
||||
/**
|
||||
* Determine if we have the keys necessary to respond to a room key request
|
||||
*
|
||||
* @param request keyRequest
|
||||
* @return true if we have the keys and could (theoretically) share
|
||||
*/
|
||||
fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean = false
|
||||
|
||||
/**
|
||||
* Send the response to a room key request.
|
||||
*
|
||||
* @param request keyRequest
|
||||
*/
|
||||
fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {}
|
||||
|
||||
fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue: String) {}
|
||||
|
||||
fun requestKeysForEvent(event: Event, withHeld: Boolean)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -17,51 +17,32 @@
|
|||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||
|
||||
import dagger.Lazy
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
||||
import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent
|
||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||
import org.matrix.android.sdk.internal.session.StreamEventsManager
|
||||
import timber.log.Timber
|
||||
|
||||
private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO)
|
||||
|
||||
internal class MXMegolmDecryption(private val userId: String,
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val liveEventManager: Lazy<StreamEventsManager>
|
||||
) : IMXDecrypting, IMXWithHeldExtension {
|
||||
internal class MXMegolmDecryption(
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val liveEventManager: Lazy<StreamEventsManager>
|
||||
) : IMXDecrypting {
|
||||
|
||||
var newSessionListener: NewSessionListener? = null
|
||||
|
||||
|
@ -73,10 +54,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
|
||||
@Throws(MXCryptoError::class)
|
||||
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
// If cross signing is enabled, we don't send request until the keys are trusted
|
||||
// There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once
|
||||
val requestOnFail = cryptoStore.getMyCrossSigningInfo()?.isTrusted() == true
|
||||
return decryptEvent(event, timeline, requestOnFail)
|
||||
return decryptEvent(event, timeline, true)
|
||||
}
|
||||
|
||||
@Throws(MXCryptoError::class)
|
||||
|
@ -126,13 +104,14 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
if (throwable is MXCryptoError.OlmError) {
|
||||
// TODO Check the value of .message
|
||||
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
|
||||
// addEventToPendingList(event, timeline)
|
||||
// The session might has been partially withheld (and only pass ratcheted)
|
||||
// So we know that session, but it's ratcheted and we can't decrypt at that index
|
||||
|
||||
if (requestKeysOnFail) {
|
||||
requestKeysForEvent(event)
|
||||
}
|
||||
// Check if partially withheld
|
||||
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
||||
if (withHeldInfo != null) {
|
||||
if (requestKeysOnFail) {
|
||||
requestKeysForEvent(event, true)
|
||||
}
|
||||
// Encapsulate as withHeld exception
|
||||
throw MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.KEYS_WITHHELD,
|
||||
|
@ -141,10 +120,6 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
)
|
||||
}
|
||||
|
||||
if (requestKeysOnFail) {
|
||||
requestKeysForEvent(event, false)
|
||||
}
|
||||
|
||||
throw MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
|
||||
"UNKNOWN_MESSAGE_INDEX",
|
||||
|
@ -162,27 +137,22 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
)
|
||||
}
|
||||
if (throwable is MXCryptoError.Base) {
|
||||
if (
|
||||
/** if the session is unknown*/
|
||||
throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
|
||||
) {
|
||||
if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
||||
// Check if it was withheld by sender to enrich error code
|
||||
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
||||
if (withHeldInfo != null) {
|
||||
if (requestKeysOnFail) {
|
||||
requestKeysForEvent(event, true)
|
||||
requestKeysForEvent(event)
|
||||
}
|
||||
// Encapsulate as withHeld exception
|
||||
throw MXCryptoError.Base(
|
||||
MXCryptoError.ErrorType.KEYS_WITHHELD,
|
||||
withHeldInfo.code?.value ?: "",
|
||||
withHeldInfo.reason
|
||||
)
|
||||
} else {
|
||||
// This is un-used in Matrix Android SDK2, not sure if needed
|
||||
// addEventToPendingList(event, timeline)
|
||||
if (requestKeysOnFail) {
|
||||
requestKeysForEvent(event, false)
|
||||
}
|
||||
withHeldInfo.reason)
|
||||
}
|
||||
|
||||
if (requestKeysOnFail) {
|
||||
requestKeysForEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -198,55 +168,10 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
*
|
||||
* @param event the event
|
||||
*/
|
||||
override fun requestKeysForEvent(event: Event, withHeld: Boolean) {
|
||||
val sender = event.senderId ?: return
|
||||
val encryptedEventContent = event.content.toModel<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)
|
||||
private fun requestKeysForEvent(event: Event) {
|
||||
outgoingKeyRequestManager.requestKeyForEvent(event, false)
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Add an event to the list of those we couldn't decrypt the first time we
|
||||
// * saw them.
|
||||
// *
|
||||
// * @param event the event to try to decrypt later
|
||||
// * @param timelineId the timeline identifier
|
||||
// */
|
||||
// private fun addEventToPendingList(event: Event, timelineId: String) {
|
||||
// val encryptedEventContent = event.content.toModel<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.
|
||||
*
|
||||
|
@ -266,6 +191,11 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
return
|
||||
}
|
||||
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
||||
if (!cryptoStore.isKeyGossipingEnabled()) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.i("onRoomKeyEvent(), ignore forward adding as per crypto config : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
|
||||
return
|
||||
}
|
||||
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
|
||||
val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>()
|
||||
?: return
|
||||
|
@ -306,7 +236,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
}
|
||||
|
||||
Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
|
||||
val added = olmDevice.addInboundGroupSession(
|
||||
val addSessionResult = olmDevice.addInboundGroupSession(
|
||||
roomKeyContent.sessionId,
|
||||
roomKeyContent.sessionKey,
|
||||
roomKeyContent.roomId,
|
||||
|
@ -316,18 +246,47 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
exportFormat
|
||||
)
|
||||
|
||||
if (added) {
|
||||
when (addSessionResult) {
|
||||
is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex
|
||||
is MXOlmDevice.AddSessionResult.NotImportedHigherIndex -> addSessionResult.newIndex
|
||||
else -> null
|
||||
}?.let { index ->
|
||||
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
||||
val fromDevice = (event.content?.get("sender_key") as? String)?.let { senderDeviceIdentityKey ->
|
||||
cryptoStore.getUserDeviceList(event.senderId ?: "")
|
||||
?.firstOrNull {
|
||||
it.identityKey() == senderDeviceIdentityKey
|
||||
}
|
||||
}?.deviceId
|
||||
|
||||
outgoingKeyRequestManager.onRoomKeyForwarded(
|
||||
sessionId = roomKeyContent.sessionId,
|
||||
algorithm = roomKeyContent.algorithm ?: "",
|
||||
roomId = roomKeyContent.roomId,
|
||||
senderKey = senderKey,
|
||||
fromIndex = index,
|
||||
fromDevice = fromDevice,
|
||||
event = event)
|
||||
|
||||
cryptoStore.saveIncomingForwardKeyAuditTrail(
|
||||
roomId = roomKeyContent.roomId,
|
||||
sessionId = roomKeyContent.sessionId,
|
||||
senderKey = senderKey,
|
||||
algorithm = roomKeyContent.algorithm ?: "",
|
||||
userId = event.senderId ?: "",
|
||||
deviceId = fromDevice ?: "",
|
||||
chainIndex = index.toLong())
|
||||
|
||||
// The index is used to decide if we cancel sent request or if we wait for a better key
|
||||
outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, senderKey, index)
|
||||
}
|
||||
}
|
||||
|
||||
if (addSessionResult is MXOlmDevice.AddSessionResult.Imported) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}")
|
||||
defaultKeysBackupService.maybeBackupKeys()
|
||||
|
||||
val content = RoomKeyRequestBody(
|
||||
algorithm = roomKeyContent.algorithm,
|
||||
roomId = roomKeyContent.roomId,
|
||||
sessionId = roomKeyContent.sessionId,
|
||||
senderKey = senderKey
|
||||
)
|
||||
|
||||
outgoingGossipingRequestManager.cancelRoomKeyRequest(content)
|
||||
|
||||
onNewSession(roomKeyContent.roomId, senderKey, roomKeyContent.sessionId)
|
||||
}
|
||||
}
|
||||
|
@ -343,77 +302,4 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey")
|
||||
newSessionListener?.onNewSession(roomId, senderKey, sessionId)
|
||||
}
|
||||
|
||||
override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean {
|
||||
val roomId = request.requestBody?.roomId ?: return false
|
||||
val senderKey = request.requestBody.senderKey ?: return false
|
||||
val sessionId = request.requestBody.sessionId ?: return false
|
||||
return olmDevice.hasInboundSessionKeys(roomId, senderKey, sessionId)
|
||||
}
|
||||
|
||||
override fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {
|
||||
// sanity checks
|
||||
if (request.requestBody == null) {
|
||||
return
|
||||
}
|
||||
val userId = request.userId ?: return
|
||||
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
val body = request.requestBody
|
||||
val sessionHolder = try {
|
||||
olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session for request $body")
|
||||
return@launch
|
||||
}
|
||||
|
||||
val export = sessionHolder.mutex.withLock {
|
||||
sessionHolder.wrapper.exportKeys()
|
||||
} ?: return@launch Unit.also {
|
||||
Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session ${body.sessionId}")
|
||||
}
|
||||
|
||||
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
|
||||
.mapCatching {
|
||||
val deviceId = request.deviceId
|
||||
val deviceInfo = cryptoStore.getUserDevice(userId, deviceId ?: "")
|
||||
if (deviceInfo == null) {
|
||||
throw RuntimeException()
|
||||
} else {
|
||||
val devicesByUser = mapOf(userId to listOf(deviceInfo))
|
||||
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||
val olmSessionResult = usersDeviceMap.getObject(userId, deviceId)
|
||||
if (olmSessionResult?.sessionId == null) {
|
||||
// no session with this device, probably because there
|
||||
// were no one-time keys.
|
||||
Timber.tag(loggerTag.value).e("no session with this device $deviceId, probably because there were no one-time keys.")
|
||||
return@mapCatching
|
||||
}
|
||||
Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sharing session ${body.sessionId} with device $userId:$deviceId")
|
||||
|
||||
val payloadJson = mapOf(
|
||||
"type" to EventType.FORWARDED_ROOM_KEY,
|
||||
"content" to export
|
||||
)
|
||||
|
||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||
val sendToDeviceMap = MXUsersDevicesMap<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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,46 +17,24 @@
|
|||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||
|
||||
import dagger.Lazy
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.session.StreamEventsManager
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class MXMegolmDecryptionFactory @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val eventsManager: Lazy<StreamEventsManager>
|
||||
) {
|
||||
|
||||
fun create(): MXMegolmDecryption {
|
||||
return MXMegolmDecryption(
|
||||
userId,
|
||||
olmDevice,
|
||||
deviceListManager,
|
||||
outgoingGossipingRequestManager,
|
||||
messageEncrypter,
|
||||
ensureOlmSessionsForDevicesAction,
|
||||
outgoingKeyRequestManager,
|
||||
cryptoStore,
|
||||
sendToDeviceTask,
|
||||
coroutineDispatchers,
|
||||
cryptoCoroutineScope,
|
||||
eventsManager
|
||||
)
|
||||
eventsManager)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.api.session.crypto.model.forEach
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||
|
@ -285,25 +284,14 @@ internal class MXMegolmEncryption(
|
|||
// attempted to share with) rather than the contentMap (those we did
|
||||
// share with), because we don't want to try to claim a one-time-key
|
||||
// for dead devices on every message.
|
||||
val gossipingEventBuffer = arrayListOf<Event>()
|
||||
for ((userId, devicesToShareWith) in devicesByUser) {
|
||||
for ((_, devicesToShareWith) in devicesByUser) {
|
||||
for (deviceInfo in devicesToShareWith) {
|
||||
session.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex)
|
||||
gossipingEventBuffer.add(
|
||||
Event(
|
||||
type = EventType.ROOM_KEY,
|
||||
senderId = myUserId,
|
||||
content = submap.apply {
|
||||
this["session_key"] = ""
|
||||
// we add a fake key for trail
|
||||
this["_dest"] = "$userId|${deviceInfo.deviceId}"
|
||||
}
|
||||
))
|
||||
// XXX is it needed to add it to the audit trail?
|
||||
// For now decided that no, we are more interested by forward trail
|
||||
}
|
||||
}
|
||||
|
||||
cryptoStore.saveGossipingEvents(gossipingEventBuffer)
|
||||
|
||||
if (haveTargets) {
|
||||
t0 = clock.epochMillis()
|
||||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target")
|
||||
|
@ -346,7 +334,8 @@ internal class MXMegolmEncryption(
|
|||
senderKey = senderKey,
|
||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
sessionId = sessionId,
|
||||
codeString = code.value
|
||||
codeString = code.value,
|
||||
fromDevice = myDeviceId
|
||||
)
|
||||
val params = SendToDeviceTask.Params(
|
||||
EventType.ROOM_KEY_WITHHELD,
|
||||
|
|
|
@ -265,8 +265,4 @@ internal class MXOlmDecryption(
|
|||
|
||||
return res["payload"]
|
||||
}
|
||||
|
||||
override fun requestKeysForEvent(event: Event, withHeld: Boolean) {
|
||||
// nop
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.fromBase64
|
||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
|
||||
|
@ -70,6 +71,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val workManagerProvider: WorkManagerProvider,
|
||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||
private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
|
||||
) : CrossSigningService,
|
||||
DeviceListManager.UserDevicesUpdateListener {
|
||||
|
@ -785,7 +787,8 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
// If it's me, recheck trust of all users and devices?
|
||||
val users = ArrayList<String>()
|
||||
if (otherUserId == userId && currentTrust != trusted) {
|
||||
// reRequestAllPendingRoomKeyRequest()
|
||||
// notify key requester
|
||||
outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted)
|
||||
cryptoStore.updateUsersTrust {
|
||||
users.add(it)
|
||||
checkUserTrust(it).isVerified()
|
||||
|
@ -800,19 +803,4 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// private fun reRequestAllPendingRoomKeyRequest() {
|
||||
// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
// Timber.d("## CrossSigning - reRequest pending outgoing room key requests")
|
||||
// cryptoStore.getOutgoingRoomKeyRequests().forEach {
|
||||
// it.requestBody?.let { requestBody ->
|
||||
// if (cryptoStore.getInboundGroupSession(requestBody.sessionId ?: "", requestBody.senderKey ?: "") == null) {
|
||||
// outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody)
|
||||
// } else {
|
||||
// outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -63,16 +63,11 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBack
|
|||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteBackupTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||
|
@ -112,16 +107,11 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
// Tasks
|
||||
private val createKeysBackupVersionTask: CreateKeysBackupVersionTask,
|
||||
private val deleteBackupTask: DeleteBackupTask,
|
||||
private val deleteRoomSessionDataTask: DeleteRoomSessionDataTask,
|
||||
private val deleteRoomSessionsDataTask: DeleteRoomSessionsDataTask,
|
||||
private val deleteSessionDataTask: DeleteSessionsDataTask,
|
||||
private val getKeysBackupLastVersionTask: GetKeysBackupLastVersionTask,
|
||||
private val getKeysBackupVersionTask: GetKeysBackupVersionTask,
|
||||
private val getRoomSessionDataTask: GetRoomSessionDataTask,
|
||||
private val getRoomSessionsDataTask: GetRoomSessionsDataTask,
|
||||
private val getSessionsDataTask: GetSessionsDataTask,
|
||||
private val storeRoomSessionDataTask: StoreRoomSessionDataTask,
|
||||
private val storeSessionsDataTask: StoreRoomSessionsDataTask,
|
||||
private val storeSessionDataTask: StoreSessionsDataTask,
|
||||
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
|
||||
// Task executor
|
||||
|
@ -168,58 +158,63 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
override fun prepareKeysBackupVersion(password: String?,
|
||||
progressListener: ProgressListener?,
|
||||
callback: MatrixCallback<MegolmBackupCreationInfo>) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||
runCatching {
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
val olmPkDecryption = OlmPkDecryption()
|
||||
val signalableMegolmBackupAuthData = if (password != null) {
|
||||
// Generate a private key from the password
|
||||
val backgroundProgressListener = if (progressListener == null) {
|
||||
null
|
||||
} else {
|
||||
object : ProgressListener {
|
||||
override fun onProgress(progress: Int, total: Int) {
|
||||
uiHandler.post {
|
||||
try {
|
||||
progressListener.onProgress(progress, total)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "prepareKeysBackupVersion: onProgress failure")
|
||||
}
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||
try {
|
||||
val olmPkDecryption = OlmPkDecryption()
|
||||
val signalableMegolmBackupAuthData = if (password != null) {
|
||||
// Generate a private key from the password
|
||||
val backgroundProgressListener = if (progressListener == null) {
|
||||
null
|
||||
} else {
|
||||
object : ProgressListener {
|
||||
override fun onProgress(progress: Int, total: Int) {
|
||||
uiHandler.post {
|
||||
try {
|
||||
progressListener.onProgress(progress, total)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "prepareKeysBackupVersion: onProgress failure")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener)
|
||||
SignalableMegolmBackupAuthData(
|
||||
publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey),
|
||||
privateKeySalt = generatePrivateKeyResult.salt,
|
||||
privateKeyIterations = generatePrivateKeyResult.iterations
|
||||
)
|
||||
} else {
|
||||
val publicKey = olmPkDecryption.generateKey()
|
||||
|
||||
SignalableMegolmBackupAuthData(
|
||||
publicKey = publicKey
|
||||
)
|
||||
}
|
||||
|
||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary())
|
||||
|
||||
val signedMegolmBackupAuthData = MegolmBackupAuthData(
|
||||
publicKey = signalableMegolmBackupAuthData.publicKey,
|
||||
privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt,
|
||||
privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations,
|
||||
signatures = objectSigner.signObject(canonicalJson)
|
||||
val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener)
|
||||
SignalableMegolmBackupAuthData(
|
||||
publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey),
|
||||
privateKeySalt = generatePrivateKeyResult.salt,
|
||||
privateKeyIterations = generatePrivateKeyResult.iterations
|
||||
)
|
||||
} else {
|
||||
val publicKey = olmPkDecryption.generateKey()
|
||||
|
||||
MegolmBackupCreationInfo(
|
||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP,
|
||||
authData = signedMegolmBackupAuthData,
|
||||
recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey())
|
||||
SignalableMegolmBackupAuthData(
|
||||
publicKey = publicKey
|
||||
)
|
||||
}
|
||||
}.foldToCallback(callback)
|
||||
|
||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary())
|
||||
|
||||
val signedMegolmBackupAuthData = MegolmBackupAuthData(
|
||||
publicKey = signalableMegolmBackupAuthData.publicKey,
|
||||
privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt,
|
||||
privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations,
|
||||
signatures = objectSigner.signObject(canonicalJson)
|
||||
)
|
||||
|
||||
val creationInfo = MegolmBackupCreationInfo(
|
||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP,
|
||||
authData = signedMegolmBackupAuthData,
|
||||
recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey())
|
||||
)
|
||||
uiHandler.post {
|
||||
callback.onSuccess(creationInfo)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
uiHandler.post {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -267,41 +262,39 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
}
|
||||
|
||||
override fun deleteBackup(version: String, callback: MatrixCallback<Unit>?) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
// If we're currently backing up to this backup... stop.
|
||||
// (We start using it automatically in createKeysBackupVersion so this is symmetrical).
|
||||
if (keysBackupVersion != null && version == keysBackupVersion?.version) {
|
||||
resetKeysBackupData()
|
||||
keysBackupVersion = null
|
||||
keysBackupStateManager.state = KeysBackupState.Unknown
|
||||
}
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||
// If we're currently backing up to this backup... stop.
|
||||
// (We start using it automatically in createKeysBackupVersion so this is symmetrical).
|
||||
if (keysBackupVersion != null && version == keysBackupVersion?.version) {
|
||||
resetKeysBackupData()
|
||||
keysBackupVersion = null
|
||||
keysBackupStateManager.state = KeysBackupState.Unknown
|
||||
}
|
||||
|
||||
deleteBackupTask
|
||||
.configureWith(DeleteBackupTask.Params(version)) {
|
||||
this.callback = object : MatrixCallback<Unit> {
|
||||
private fun eventuallyRestartBackup() {
|
||||
// Do not stay in KeysBackupState.Unknown but check what is available on the homeserver
|
||||
if (state == KeysBackupState.Unknown) {
|
||||
checkAndStartKeysBackup()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
eventuallyRestartBackup()
|
||||
|
||||
uiHandler.post { callback?.onSuccess(Unit) }
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
eventuallyRestartBackup()
|
||||
|
||||
uiHandler.post { callback?.onFailure(failure) }
|
||||
deleteBackupTask
|
||||
.configureWith(DeleteBackupTask.Params(version)) {
|
||||
this.callback = object : MatrixCallback<Unit> {
|
||||
private fun eventuallyRestartBackup() {
|
||||
// Do not stay in KeysBackupState.Unknown but check what is available on the homeserver
|
||||
if (state == KeysBackupState.Unknown) {
|
||||
checkAndStartKeysBackup()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
eventuallyRestartBackup()
|
||||
|
||||
uiHandler.post { callback?.onSuccess(Unit) }
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
eventuallyRestartBackup()
|
||||
|
||||
uiHandler.post { callback?.onFailure(failure) }
|
||||
}
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -480,10 +473,11 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
|
||||
if (authData == null) {
|
||||
Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data")
|
||||
|
||||
callback.onFailure(IllegalArgumentException("Missing element"))
|
||||
uiHandler.post {
|
||||
callback.onFailure(IllegalArgumentException("Missing element"))
|
||||
}
|
||||
} else {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
|
||||
// Get current signatures, or create an empty set
|
||||
val myUserSignatures = authData.signatures?.get(userId).orEmpty().toMutableMap()
|
||||
|
@ -536,11 +530,15 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
|
||||
checkAndStartWithKeysBackupVersion(newKeysBackupVersion)
|
||||
|
||||
callback.onSuccess(data)
|
||||
uiHandler.post {
|
||||
callback.onSuccess(data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
uiHandler.post {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -554,15 +552,14 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
callback: MatrixCallback<Unit>) {
|
||||
Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}")
|
||||
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||
val isValid = withContext(coroutineDispatchers.crypto) {
|
||||
isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)
|
||||
}
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||
val isValid = isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)
|
||||
|
||||
if (!isValid) {
|
||||
Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.")
|
||||
|
||||
callback.onFailure(IllegalArgumentException("Invalid recovery key or password"))
|
||||
uiHandler.post {
|
||||
callback.onFailure(IllegalArgumentException("Invalid recovery key or password"))
|
||||
}
|
||||
} else {
|
||||
trustKeysBackupVersion(keysBackupVersion, true, callback)
|
||||
}
|
||||
|
@ -574,15 +571,14 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
callback: MatrixCallback<Unit>) {
|
||||
Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}")
|
||||
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||
val recoveryKey = withContext(coroutineDispatchers.crypto) {
|
||||
recoveryKeyFromPassword(password, keysBackupVersion, null)
|
||||
}
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||
val recoveryKey = recoveryKeyFromPassword(password, keysBackupVersion, null)
|
||||
|
||||
if (recoveryKey == null) {
|
||||
Timber.w("trustKeysBackupVersionWithPassphrase: Key backup is missing required data")
|
||||
|
||||
callback.onFailure(IllegalArgumentException("Missing element"))
|
||||
uiHandler.post {
|
||||
callback.onFailure(IllegalArgumentException("Missing element"))
|
||||
}
|
||||
} else {
|
||||
// Check trust using the recovery key
|
||||
trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback)
|
||||
|
@ -593,30 +589,28 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
override fun onSecretKeyGossip(secret: String) {
|
||||
Timber.i("## CrossSigning - onSecretKeyGossip")
|
||||
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||
try {
|
||||
when (val keysBackupLastVersionResult = getKeysBackupLastVersionTask.execute(Unit)) {
|
||||
KeysBackupLastVersionResult.NoKeysBackup -> {
|
||||
Timber.d("No keys backup found")
|
||||
}
|
||||
is KeysBackupLastVersionResult.KeysBackup -> {
|
||||
val keysBackupVersion = keysBackupLastVersionResult.keysVersionResult
|
||||
val recoveryKey = computeRecoveryKey(secret.fromBase64())
|
||||
if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) {
|
||||
awaitCallback<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 keysBackupVersion = getKeysBackupLastVersionTask.execute(Unit).toKeysVersionResult()
|
||||
?: return@launch Unit.also {
|
||||
Timber.d("Failed to get backup last version")
|
||||
}
|
||||
val recoveryKey = computeRecoveryKey(secret.fromBase64())
|
||||
if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) {
|
||||
awaitCallback<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) {
|
||||
Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}")
|
||||
|
@ -679,9 +673,9 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
||||
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
|
||||
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
val decryption = withContext(coroutineDispatchers.crypto) {
|
||||
val decryption = withContext(coroutineDispatchers.computation) {
|
||||
// Check if the recovery is valid before going any further
|
||||
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
|
||||
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
|
||||
|
@ -754,7 +748,19 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
}
|
||||
result
|
||||
}
|
||||
}.foldToCallback(callback)
|
||||
}.foldToCallback(object : MatrixCallback<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>) {
|
||||
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
|
||||
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
val progressListener = if (stepProgressListener != null) {
|
||||
object : ProgressListener {
|
||||
|
@ -791,7 +797,19 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, it)
|
||||
}
|
||||
}
|
||||
}.foldToCallback(callback)
|
||||
}.foldToCallback(object : MatrixCallback<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) {
|
||||
// Get all keys for the room
|
||||
val data = getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version))
|
||||
val data = withContext(coroutineDispatchers.io) {
|
||||
getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version))
|
||||
}
|
||||
// Convert to KeysBackupData
|
||||
KeysBackupData(mutableMapOf(roomId to data))
|
||||
} else {
|
||||
// Get all keys
|
||||
getSessionsDataTask.execute(GetSessionsDataTask.Params(version))
|
||||
withContext(coroutineDispatchers.io) {
|
||||
getSessionsDataTask.execute(GetSessionsDataTask.Params(version))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec
|
|||
import org.matrix.android.sdk.api.session.securestorage.SsssPassphrase
|
||||
import org.matrix.android.sdk.api.util.fromBase64
|
||||
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.generatePrivateKeyWithPassword
|
||||
import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256
|
||||
import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption
|
||||
|
@ -57,7 +57,7 @@ import kotlin.experimental.and
|
|||
internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
private val accountDataService: SessionAccountDataService,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val secretShareManager: SecretShareManager,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope
|
||||
) : SharedSecretStorageService {
|
||||
|
@ -380,10 +380,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||
return IntegrityResult.Success(keyInfo.content.passphrase != null)
|
||||
}
|
||||
|
||||
override fun requestSecret(name: String, myOtherDeviceId: String) {
|
||||
outgoingGossipingRequestManager.sendSecretShareRequest(
|
||||
name,
|
||||
mapOf(userId to listOf(myOtherDeviceId))
|
||||
)
|
||||
override suspend fun requestSecret(name: String, myOtherDeviceId: String) {
|
||||
secretShareManager.requestSecretTo(myOtherDeviceId, name)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,23 +19,22 @@ package org.matrix.android.sdk.internal.crypto.store
|
|||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
||||
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
|
||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
|
||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
||||
import org.matrix.android.sdk.api.session.crypto.model.TrailType
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
|
||||
|
@ -82,6 +81,15 @@ internal interface IMXCryptoStore {
|
|||
*/
|
||||
fun setGlobalBlacklistUnverifiedDevices(block: Boolean)
|
||||
|
||||
/**
|
||||
* Enable or disable key gossiping.
|
||||
* Default is true.
|
||||
* If set to false this device won't send key_request nor will accept key forwarded
|
||||
*/
|
||||
fun enableKeyGossiping(enable: Boolean)
|
||||
|
||||
fun isKeyGossipingEnabled(): Boolean
|
||||
|
||||
/**
|
||||
* Provides the rooms ids list in which the messages are not encrypted for the unverified devices.
|
||||
*
|
||||
|
@ -125,18 +133,6 @@ internal interface IMXCryptoStore {
|
|||
*/
|
||||
fun getDeviceTrackingStatuses(): Map<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.
|
||||
*
|
||||
|
@ -377,7 +373,9 @@ internal interface IMXCryptoStore {
|
|||
* @param requestBody the request body
|
||||
* @return an OutgoingRoomKeyRequest instance or null
|
||||
*/
|
||||
fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest?
|
||||
fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest?
|
||||
fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest?
|
||||
fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List<OutgoingKeyRequest>
|
||||
|
||||
/**
|
||||
* Look for an existing outgoing room key request, and if none is found, add a new one.
|
||||
|
@ -385,39 +383,59 @@ internal interface IMXCryptoStore {
|
|||
* @param request the request
|
||||
* @return either the same instance as passed in, or the existing one.
|
||||
*/
|
||||
fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<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) {
|
||||
updateGossipingRequestState(
|
||||
requestUserId = request.userId,
|
||||
requestDeviceId = request.deviceId,
|
||||
requestId = request.requestId,
|
||||
state = state
|
||||
)
|
||||
}
|
||||
fun saveForwardKeyAuditTrail(
|
||||
roomId: String,
|
||||
sessionId: String,
|
||||
senderKey: String,
|
||||
algorithm: String,
|
||||
userId: String,
|
||||
deviceId: String,
|
||||
chainIndex: Long?
|
||||
)
|
||||
|
||||
fun updateGossipingRequestState(requestUserId: String?,
|
||||
requestDeviceId: String?,
|
||||
requestId: String?,
|
||||
state: GossipingRequestState)
|
||||
|
||||
/**
|
||||
* Search an IncomingRoomKeyRequest
|
||||
*
|
||||
* @param userId the user id
|
||||
* @param deviceId the device id
|
||||
* @param requestId the request id
|
||||
* @return an IncomingRoomKeyRequest if it exists, else null
|
||||
*/
|
||||
fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest?
|
||||
|
||||
fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState)
|
||||
fun saveIncomingForwardKeyAuditTrail(
|
||||
roomId: String,
|
||||
sessionId: String,
|
||||
senderKey: String,
|
||||
algorithm: String,
|
||||
userId: String,
|
||||
deviceId: String,
|
||||
chainIndex: Long?
|
||||
)
|
||||
|
||||
fun addNewSessionListener(listener: NewSessionListener)
|
||||
|
||||
|
@ -477,17 +495,15 @@ internal interface IMXCryptoStore {
|
|||
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
|
||||
// Dev tools
|
||||
|
||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
||||
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
|
||||
fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest>
|
||||
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
|
||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
|
||||
fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
|
||||
fun getGossipingEvents(): List<Event>
|
||||
fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest>
|
||||
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>>
|
||||
fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>>
|
||||
fun <T> getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData<PagedList<T>>
|
||||
fun getGossipingEvents(): List<AuditTrail>
|
||||
|
||||
fun setDeviceKeysUploaded(uploaded: Boolean)
|
||||
fun areDeviceKeysUploaded(): Boolean
|
||||
fun tidyUpDataBase()
|
||||
fun logDbUsageInfo()
|
||||
fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest>
|
||||
}
|
||||
|
|
|
@ -24,37 +24,40 @@ import com.zhuinden.monarchy.Monarchy
|
|||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.Sort
|
||||
import io.realm.kotlin.createObject
|
||||
import io.realm.kotlin.where
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
||||
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
|
||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
|
||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingKeyRequestInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
||||
import org.matrix.android.sdk.api.session.crypto.model.TrailType
|
||||
import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.internal.crypto.GossipRequestType
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailMapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper
|
||||
|
@ -63,10 +66,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity
|
|||
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
||||
|
@ -74,8 +73,8 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSess
|
|||
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
|
||||
|
@ -89,7 +88,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.query.get
|
|||
import org.matrix.android.sdk.internal.crypto.store.db.query.getById
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate
|
||||
import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper
|
||||
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
||||
import org.matrix.android.sdk.internal.database.tools.RealmDebugTools
|
||||
import org.matrix.android.sdk.internal.di.CryptoDatabase
|
||||
import org.matrix.android.sdk.internal.di.DeviceId
|
||||
|
@ -106,6 +104,8 @@ import java.util.concurrent.Executors
|
|||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("RealmCryptoStore", LoggerTag.CRYPTO)
|
||||
|
||||
@SessionScope
|
||||
internal class RealmCryptoStore @Inject constructor(
|
||||
@CryptoDatabase private val realmConfiguration: RealmConfiguration,
|
||||
|
@ -273,12 +273,13 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
|
||||
override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<DeviceInfoEntity>()
|
||||
.equalTo(DeviceInfoEntityFields.IDENTITY_KEY, identityKey)
|
||||
.findFirst()
|
||||
?.let { deviceInfo ->
|
||||
CryptoMapper.mapToModel(deviceInfo)
|
||||
return doWithRealm(realmConfiguration) { realm ->
|
||||
realm.where<DeviceInfoEntity>()
|
||||
.contains(DeviceInfoEntityFields.KEYS_MAP_JSON, identityKey)
|
||||
.findAll()
|
||||
.mapNotNull { CryptoMapper.mapToModel(it) }
|
||||
.firstOrNull {
|
||||
it.identityKey() == identityKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -743,14 +744,23 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
if (sessionIdentifier != null) {
|
||||
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey)
|
||||
|
||||
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
|
||||
primaryKey = key
|
||||
sessionId = sessionIdentifier
|
||||
senderKey = session.senderKey
|
||||
putInboundGroupSession(session)
|
||||
}
|
||||
val existing = realm.where<OlmInboundGroupSessionEntity>()
|
||||
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
|
||||
.findFirst()
|
||||
|
||||
realm.insertOrUpdate(realmOlmInboundGroupSession)
|
||||
if (existing != null) {
|
||||
// we want to keep the existing backup status
|
||||
existing.putInboundGroupSession(session)
|
||||
} else {
|
||||
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
|
||||
primaryKey = key
|
||||
sessionId = sessionIdentifier
|
||||
senderKey = session.senderKey
|
||||
putInboundGroupSession(session)
|
||||
}
|
||||
|
||||
realm.insertOrUpdate(realmOlmInboundGroupSession)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -879,18 +889,33 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
return
|
||||
}
|
||||
|
||||
doRealmTransaction(realmConfiguration) {
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
|
||||
try {
|
||||
val sessionIdentifier = olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier()
|
||||
val key = OlmInboundGroupSessionEntity.createPrimaryKey(
|
||||
olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier(),
|
||||
sessionIdentifier,
|
||||
olmInboundGroupSessionWrapper.senderKey
|
||||
)
|
||||
|
||||
it.where<OlmInboundGroupSessionEntity>()
|
||||
val existing = realm.where<OlmInboundGroupSessionEntity>()
|
||||
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
|
||||
.findFirst()
|
||||
?.backedUp = true
|
||||
|
||||
if (existing != null) {
|
||||
existing.backedUp = true
|
||||
} else {
|
||||
// ... might be in cache but not yet persisted, create a record to persist backedup state
|
||||
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
|
||||
primaryKey = key
|
||||
sessionId = sessionIdentifier
|
||||
senderKey = olmInboundGroupSessionWrapper.senderKey
|
||||
putInboundGroupSession(olmInboundGroupSessionWrapper)
|
||||
backedUp = true
|
||||
}
|
||||
|
||||
realm.insertOrUpdate(realmOlmInboundGroupSession)
|
||||
}
|
||||
} catch (e: OlmException) {
|
||||
Timber.e(e, "OlmException")
|
||||
}
|
||||
|
@ -929,6 +954,18 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun enableKeyGossiping(enable: Boolean) {
|
||||
doRealmTransaction(realmConfiguration) {
|
||||
it.where<CryptoMetadataEntity>().findFirst()?.globalEnableKeyGossiping = enable
|
||||
}
|
||||
}
|
||||
|
||||
override fun isKeyGossipingEnabled(): Boolean {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<CryptoMetadataEntity>().findFirst()?.globalEnableKeyGossiping
|
||||
} ?: true
|
||||
}
|
||||
|
||||
override fun getGlobalBlacklistUnverifiedDevices(): Boolean {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<CryptoMetadataEntity>().findFirst()?.globalBlacklistUnverifiedDevices
|
||||
|
@ -1010,12 +1047,13 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
?: defaultValue
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? {
|
||||
override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest? {
|
||||
return monarchy.fetchAllCopiedSync { realm ->
|
||||
realm.where<OutgoingGossipingRequestEntity>()
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
}.mapNotNull {
|
||||
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||
realm.where<OutgoingKeyRequestEntity>()
|
||||
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId)
|
||||
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId)
|
||||
}.map {
|
||||
it.toOutgoingKeyRequest()
|
||||
}.firstOrNull {
|
||||
it.requestBody?.algorithm == requestBody.algorithm &&
|
||||
it.requestBody?.roomId == requestBody.roomId &&
|
||||
|
@ -1024,41 +1062,37 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? {
|
||||
override fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest? {
|
||||
return monarchy.fetchAllCopiedSync { realm ->
|
||||
realm.where<OutgoingGossipingRequestEntity>()
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName)
|
||||
}.mapNotNull {
|
||||
it.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
||||
realm.where<OutgoingKeyRequestEntity>()
|
||||
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
|
||||
}.map {
|
||||
it.toOutgoingKeyRequest()
|
||||
}.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 ->
|
||||
realm.where<IncomingGossipingRequestEntity>()
|
||||
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
}.mapNotNull {
|
||||
it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
|
||||
realm.where<OutgoingKeyRequestEntity>()
|
||||
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId)
|
||||
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId)
|
||||
}.map {
|
||||
it.toOutgoingKeyRequest()
|
||||
}.filter {
|
||||
it.requestBody?.algorithm == algorithm &&
|
||||
it.requestBody?.senderKey == senderKey
|
||||
}
|
||||
}
|
||||
|
||||
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
|
||||
override fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>> {
|
||||
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||
realm.where<IncomingGossipingRequestEntity>()
|
||||
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
.sort(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Sort.DESCENDING)
|
||||
realm.where<AuditTrailEntity>().sort(AuditTrailEntityFields.AGE_LOCAL_TS, Sort.DESCENDING)
|
||||
}
|
||||
val dataSourceFactory = realmDataSourceFactory.map {
|
||||
it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
|
||||
?: IncomingRoomKeyRequest(
|
||||
requestBody = null,
|
||||
deviceId = "",
|
||||
userId = "",
|
||||
requestId = "",
|
||||
state = GossipingRequestState.NONE,
|
||||
localCreationTimestamp = 0
|
||||
)
|
||||
AuditTrailMapper.map(it)
|
||||
// mm we can't map not null...
|
||||
?: createUnknownTrail()
|
||||
}
|
||||
return monarchy.findAllPagedWithChanges(
|
||||
realmDataSourceFactory,
|
||||
|
@ -1073,12 +1107,33 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override fun getGossipingEventsTrail(): LiveData<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 ->
|
||||
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 trail = monarchy.findAllPagedWithChanges(
|
||||
val dataSourceFactory = realmDataSourceFactory.map { entity ->
|
||||
(AuditTrailMapper.map(entity)
|
||||
// mm we can't map not null...
|
||||
?: createUnknownTrail()
|
||||
).let { mapper.invoke(it) }
|
||||
}
|
||||
return monarchy.findAllPagedWithChanges(
|
||||
realmDataSourceFactory,
|
||||
LivePagedListBuilder(
|
||||
dataSourceFactory,
|
||||
|
@ -1089,28 +1144,36 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
.build()
|
||||
)
|
||||
)
|
||||
return trail
|
||||
}
|
||||
|
||||
override fun getGossipingEvents(): List<Event> {
|
||||
override fun getGossipingEvents(): List<AuditTrail> {
|
||||
return monarchy.fetchAllCopiedSync { realm ->
|
||||
realm.where<GossipingEventEntity>()
|
||||
}.map {
|
||||
it.toModel()
|
||||
realm.where<AuditTrailEntity>()
|
||||
}.mapNotNull {
|
||||
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
|
||||
var request: OutgoingRoomKeyRequest? = null
|
||||
lateinit var request: OutgoingKeyRequest
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
|
||||
val existing = realm.where<OutgoingGossipingRequestEntity>()
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
val existing = realm.where<OutgoingKeyRequestEntity>()
|
||||
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId)
|
||||
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId)
|
||||
.findAll()
|
||||
.mapNotNull {
|
||||
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||
}.firstOrNull {
|
||||
.map {
|
||||
it.toOutgoingKeyRequest()
|
||||
}.also {
|
||||
if (it.size > 1) {
|
||||
// there should be one or zero but not more, worth warning
|
||||
Timber.tag(loggerTag.value).w("There should not be more than one active key request per session")
|
||||
}
|
||||
}
|
||||
.firstOrNull {
|
||||
it.requestBody?.algorithm == requestBody.algorithm &&
|
||||
it.requestBody?.sessionId == requestBody.sessionId &&
|
||||
it.requestBody?.senderKey == requestBody.senderKey &&
|
||||
|
@ -1118,13 +1181,14 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
|
||||
if (existing == null) {
|
||||
request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply {
|
||||
request = realm.createObject(OutgoingKeyRequestEntity::class.java).apply {
|
||||
this.requestId = RequestIdHelper.createUniqueRequestId()
|
||||
this.setRecipients(recipients)
|
||||
this.requestState = OutgoingGossipingRequestState.UNSENT
|
||||
this.type = GossipRequestType.KEY
|
||||
this.requestedInfoStr = requestBody.toJson()
|
||||
}.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||
this.requestedIndex = fromIndex
|
||||
this.requestState = OutgoingRoomKeyRequestState.UNSENT
|
||||
this.setRequestBody(requestBody)
|
||||
this.creationTimeStamp = clock.epochMillis()
|
||||
}.toOutgoingKeyRequest()
|
||||
} else {
|
||||
request = existing
|
||||
}
|
||||
|
@ -1132,284 +1196,175 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
return request
|
||||
}
|
||||
|
||||
override fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest? {
|
||||
var request: OutgoingSecretRequest? = null
|
||||
|
||||
// Insert the request and return the one passed in parameter
|
||||
override fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) {
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
val existing = realm.where<OutgoingGossipingRequestEntity>()
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName)
|
||||
.findAll()
|
||||
.mapNotNull {
|
||||
it.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
||||
}.firstOrNull()
|
||||
if (existing == null) {
|
||||
request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply {
|
||||
this.type = GossipRequestType.SECRET
|
||||
setRecipients(recipients)
|
||||
this.requestState = OutgoingGossipingRequestState.UNSENT
|
||||
this.requestId = RequestIdHelper.createUniqueRequestId()
|
||||
this.requestedInfoStr = secretName
|
||||
}.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
||||
} else {
|
||||
request = existing
|
||||
}
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
override fun saveGossipingEvents(events: List<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
|
||||
)
|
||||
}
|
||||
realm.where<OutgoingKeyRequestEntity>()
|
||||
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
|
||||
.findFirst()?.apply {
|
||||
this.requestState = newState
|
||||
if (newState == OutgoingRoomKeyRequestState.UNSENT) {
|
||||
// clear the old replies
|
||||
this.replies.deleteAllFromRealm()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) {
|
||||
doRealmTransactionAsync(realmConfiguration) { realm ->
|
||||
|
||||
// After a clear cache, we might have a
|
||||
|
||||
realm.createObject(IncomingGossipingRequestEntity::class.java).let {
|
||||
it.otherDeviceId = request.deviceId
|
||||
it.otherUserId = request.userId
|
||||
it.requestId = request.requestId ?: ""
|
||||
it.requestState = GossipingRequestState.PENDING
|
||||
it.localCreationTimestamp = ageLocalTS ?: clock.epochMillis()
|
||||
if (request is IncomingSecretShareRequest) {
|
||||
it.type = GossipRequestType.SECRET
|
||||
it.requestedInfoStr = request.secretName
|
||||
} else if (request is IncomingRoomKeyRequest) {
|
||||
it.type = GossipRequestType.KEY
|
||||
it.requestedInfoStr = request.requestBody?.toJson()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun storeIncomingGossipingRequests(requests: List<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 updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) {
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
realm.where<OutgoingKeyRequestEntity>()
|
||||
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
|
||||
.findFirst()?.apply {
|
||||
this.requestedIndex = newIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateOutgoingRoomKeyReply(roomId: String,
|
||||
sessionId: String,
|
||||
algorithm: String,
|
||||
senderKey: String,
|
||||
fromDevice: String?,
|
||||
event: Event) {
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
realm.where<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> {
|
||||
// return doRealmQueryAndCopyList(realmConfiguration) {
|
||||
// it.where<GossipingEventEntity>()
|
||||
// .findAll()
|
||||
// }.map {
|
||||
// it.toIncomingSecretShareRequest()
|
||||
// }
|
||||
// }
|
||||
override fun saveWithheldAuditTrail(roomId: String,
|
||||
sessionId: String,
|
||||
senderKey: String,
|
||||
algorithm: String,
|
||||
code: WithHeldCode,
|
||||
userId: String,
|
||||
deviceId: String) {
|
||||
monarchy.writeAsync { realm ->
|
||||
val now = clock.epochMillis()
|
||||
realm.createObject<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
|
||||
|
@ -1513,37 +1468,34 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> {
|
||||
override fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest> {
|
||||
return monarchy.fetchAllMappedSync({ realm ->
|
||||
realm
|
||||
.where(OutgoingGossipingRequestEntity::class.java)
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
.where(OutgoingKeyRequestEntity::class.java)
|
||||
}, { entity ->
|
||||
entity.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||
entity.toOutgoingKeyRequest()
|
||||
})
|
||||
.filterNotNull()
|
||||
}
|
||||
|
||||
override fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest> {
|
||||
override fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest> {
|
||||
return monarchy.fetchAllMappedSync({ realm ->
|
||||
realm
|
||||
.where(OutgoingGossipingRequestEntity::class.java)
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
|
||||
.where(OutgoingKeyRequestEntity::class.java)
|
||||
.`in`(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, inStates.map { it.name }.toTypedArray())
|
||||
}, { entity ->
|
||||
entity.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
||||
entity.toOutgoingKeyRequest()
|
||||
})
|
||||
.filterNotNull()
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
|
||||
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>> {
|
||||
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||
realm
|
||||
.where(OutgoingGossipingRequestEntity::class.java)
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
.where(OutgoingKeyRequestEntity::class.java)
|
||||
}
|
||||
val dataSourceFactory = realmDataSourceFactory.map {
|
||||
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||
?: OutgoingRoomKeyRequest(requestBody = null, requestId = "?", recipients = emptyMap(), state = OutgoingGossipingRequestState.CANCELLED)
|
||||
it.toOutgoingKeyRequest()
|
||||
}
|
||||
val trail = monarchy.findAllPagedWithChanges(
|
||||
realmDataSourceFactory,
|
||||
|
@ -1722,26 +1674,20 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
|
||||
// Only keep one week history
|
||||
realm.where<IncomingGossipingRequestEntity>()
|
||||
.lessThan(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, prevWeekTs)
|
||||
// Clean the old ones?
|
||||
realm.where<OutgoingKeyRequestEntity>()
|
||||
.lessThan(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, prevWeekTs)
|
||||
.findAll()
|
||||
.also { Timber.i("## Crypto Clean up ${it.size} IncomingGossipingRequestEntity") }
|
||||
.also { Timber.i("## Crypto Clean up ${it.size} OutgoingKeyRequestEntity") }
|
||||
.deleteAllFromRealm()
|
||||
|
||||
// Clean the cancelled ones?
|
||||
realm.where<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 month history
|
||||
|
||||
// Only keep one week history
|
||||
realm.where<GossipingEventEntity>()
|
||||
.lessThan(GossipingEventEntityFields.AGE_LOCAL_TS, prevWeekTs)
|
||||
val prevMonthTs = clock.epochMillis() - 4 * 7 * 24 * 60 * 60 * 1_000L
|
||||
realm.where<AuditTrailEntity>()
|
||||
.lessThan(AuditTrailEntityFields.AGE_LOCAL_TS, prevMonthTs)
|
||||
.findAll()
|
||||
.also { Timber.i("## Crypto Clean up ${it.size} GossipingEventEntityFields") }
|
||||
.also { Timber.i("## Crypto Clean up ${it.size} AuditTrailEntity") }
|
||||
.deleteAllFromRealm()
|
||||
|
||||
// Can we do something for WithHeldSessionEntity?
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo
|
|||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo013
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
@ -50,7 +51,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(
|
|||
// 0, 1, 2: legacy Riot-Android
|
||||
// 3: migrate to RiotX schema
|
||||
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
|
||||
val schemaVersion = 15L
|
||||
val schemaVersion = 16L
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion")
|
||||
|
@ -70,5 +71,6 @@ internal class RealmCryptoStoreMigration @Inject constructor(
|
|||
if (oldVersion < 13) MigrateCryptoTo013(realm).perform()
|
||||
if (oldVersion < 14) MigrateCryptoTo014(realm).perform()
|
||||
if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
|
||||
if (oldVersion < 16) MigrateCryptoTo016(realm).perform()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,19 +17,19 @@
|
|||
package org.matrix.android.sdk.internal.crypto.store.db
|
||||
|
||||
import io.realm.annotations.RealmModule
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeyRequestReplyEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
|
||||
|
@ -51,9 +51,9 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEnti
|
|||
KeyInfoEntity::class,
|
||||
CrossSigningInfoEntity::class,
|
||||
TrustLevelEntity::class,
|
||||
GossipingEventEntity::class,
|
||||
IncomingGossipingRequestEntity::class,
|
||||
OutgoingGossipingRequestEntity::class,
|
||||
AuditTrailEntity::class,
|
||||
OutgoingKeyRequestEntity::class,
|
||||
KeyRequestReplyEntity::class,
|
||||
MyDeviceLastSeenInfoEntity::class,
|
||||
WithHeldSessionEntity::class,
|
||||
SharedSessionEntity::class,
|
||||
|
|
|
@ -17,9 +17,6 @@
|
|||
package org.matrix.android.sdk.internal.crypto.store.db.migration
|
||||
|
||||
import io.realm.DynamicRealm
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
|
||||
import org.matrix.android.sdk.internal.util.database.RealmMigrator
|
||||
|
||||
internal class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) {
|
||||
|
@ -29,38 +26,37 @@ internal class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5)
|
|||
realm.schema.remove("IncomingRoomKeyRequestEntity")
|
||||
|
||||
// Not need to migrate existing request, just start fresh?
|
||||
|
||||
realm.schema.create("GossipingEventEntity")
|
||||
.addField(GossipingEventEntityFields.TYPE, String::class.java)
|
||||
.addIndex(GossipingEventEntityFields.TYPE)
|
||||
.addField(GossipingEventEntityFields.CONTENT, String::class.java)
|
||||
.addField(GossipingEventEntityFields.SENDER, String::class.java)
|
||||
.addIndex(GossipingEventEntityFields.SENDER)
|
||||
.addField(GossipingEventEntityFields.DECRYPTION_RESULT_JSON, String::class.java)
|
||||
.addField(GossipingEventEntityFields.DECRYPTION_ERROR_CODE, String::class.java)
|
||||
.addField(GossipingEventEntityFields.AGE_LOCAL_TS, Long::class.java)
|
||||
.setNullable(GossipingEventEntityFields.AGE_LOCAL_TS, true)
|
||||
.addField(GossipingEventEntityFields.SEND_STATE_STR, String::class.java)
|
||||
.addField("type", String::class.java)
|
||||
.addIndex("type")
|
||||
.addField("content", String::class.java)
|
||||
.addField("sender", String::class.java)
|
||||
.addIndex("sender")
|
||||
.addField("decryptionResultJson", String::class.java)
|
||||
.addField("decryptionErrorCode", String::class.java)
|
||||
.addField("ageLocalTs", Long::class.java)
|
||||
.setNullable("ageLocalTs", true)
|
||||
.addField("sendStateStr", String::class.java)
|
||||
|
||||
realm.schema.create("IncomingGossipingRequestEntity")
|
||||
.addField(IncomingGossipingRequestEntityFields.REQUEST_ID, String::class.java)
|
||||
.addIndex(IncomingGossipingRequestEntityFields.REQUEST_ID)
|
||||
.addField(IncomingGossipingRequestEntityFields.TYPE_STR, String::class.java)
|
||||
.addIndex(IncomingGossipingRequestEntityFields.TYPE_STR)
|
||||
.addField(IncomingGossipingRequestEntityFields.OTHER_USER_ID, String::class.java)
|
||||
.addField(IncomingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java)
|
||||
.addField(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, String::class.java)
|
||||
.addField(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java)
|
||||
.addField(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Long::class.java)
|
||||
.setNullable(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, true)
|
||||
.addField("requestId", String::class.java)
|
||||
.addIndex("requestId")
|
||||
.addField("typeStr", String::class.java)
|
||||
.addIndex("typeStr")
|
||||
.addField("otherUserId", String::class.java)
|
||||
.addField("requestedInfoStr", String::class.java)
|
||||
.addField("otherDeviceId", String::class.java)
|
||||
.addField("requestStateStr", String::class.java)
|
||||
.addField("localCreationTimestamp", Long::class.java)
|
||||
.setNullable("localCreationTimestamp", true)
|
||||
|
||||
realm.schema.create("OutgoingGossipingRequestEntity")
|
||||
.addField(OutgoingGossipingRequestEntityFields.REQUEST_ID, String::class.java)
|
||||
.addIndex(OutgoingGossipingRequestEntityFields.REQUEST_ID)
|
||||
.addField(OutgoingGossipingRequestEntityFields.RECIPIENTS_DATA, String::class.java)
|
||||
.addField(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java)
|
||||
.addField(OutgoingGossipingRequestEntityFields.TYPE_STR, String::class.java)
|
||||
.addIndex(OutgoingGossipingRequestEntityFields.TYPE_STR)
|
||||
.addField(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java)
|
||||
.addField("requestId", String::class.java)
|
||||
.addIndex("requestId")
|
||||
.addField("recipientsData", String::class.java)
|
||||
.addField("requestedInfoStr", String::class.java)
|
||||
.addField("typeStr", String::class.java)
|
||||
.addIndex("typeStr")
|
||||
.addField("requestStateStr", String::class.java)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -14,18 +14,15 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.crypto.model
|
||||
package org.matrix.android.sdk.internal.crypto.store.db.model
|
||||
|
||||
enum class GossipingRequestState {
|
||||
NONE,
|
||||
PENDING,
|
||||
REJECTED,
|
||||
ACCEPTING,
|
||||
ACCEPTED,
|
||||
FAILED_TO_ACCEPTED,
|
||||
import io.realm.RealmObject
|
||||
import io.realm.annotations.Index
|
||||
|
||||
// USER_REJECTED,
|
||||
UNABLE_TO_PROCESS,
|
||||
CANCELLED_BY_REQUESTER,
|
||||
RE_REQUESTED
|
||||
internal open class AuditTrailEntity(
|
||||
var ageLocalTs: Long? = null,
|
||||
@Index var type: String? = null,
|
||||
var contentJson: String? = null
|
||||
) : RealmObject() {
|
||||
companion object
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,6 +33,8 @@ internal open class CryptoMetadataEntity(
|
|||
var deviceSyncToken: String? = null,
|
||||
// Settings for blacklisting unverified devices.
|
||||
var globalBlacklistUnverifiedDevices: Boolean = false,
|
||||
// setting to enable or disable key gossiping
|
||||
var globalEnableKeyGossiping: Boolean = true,
|
||||
// The keys backup version currently used. Null means no backup.
|
||||
var backupVersion: String? = null,
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package org.matrix.android.sdk.internal.crypto.tasks
|
||||
|
||||
import dagger.Lazy
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
|
||||
|
@ -39,7 +40,7 @@ internal interface EncryptEventTask : Task<EncryptEventTask.Params, Event> {
|
|||
|
||||
internal class DefaultEncryptEventTask @Inject constructor(
|
||||
private val localEchoRepository: LocalEchoRepository,
|
||||
private val cryptoService: CryptoService
|
||||
private val cryptoService: Lazy<CryptoService>
|
||||
) : EncryptEventTask {
|
||||
override suspend fun execute(params: EncryptEventTask.Params): Event {
|
||||
// don't want to wait for any query
|
||||
|
@ -59,7 +60,7 @@ internal class DefaultEncryptEventTask @Inject constructor(
|
|||
// try {
|
||||
// let it throws
|
||||
awaitCallback<MXEncryptEventContentResult> {
|
||||
cryptoService.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
|
||||
cryptoService.get().encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
|
||||
}.let { result ->
|
||||
val modifiedContent = HashMap(result.eventContent)
|
||||
params.keepKeys?.forEach { toKeep ->
|
||||
|
@ -80,7 +81,7 @@ internal class DefaultEncryptEventTask @Inject constructor(
|
|||
).toContent(),
|
||||
forwardingCurve25519KeyChain = emptyList(),
|
||||
senderCurve25519Key = result.eventContent["sender_key"] as? String,
|
||||
claimedEd25519Key = cryptoService.getMyDevice().fingerprint()
|
||||
claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint()
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package org.matrix.android.sdk.internal.crypto.tasks
|
||||
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||
|
@ -46,7 +47,9 @@ internal class DefaultSendEventTask @Inject constructor(
|
|||
params.event.roomId
|
||||
?.takeIf { params.encrypt }
|
||||
?.let { roomId ->
|
||||
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
|
||||
tryOrNull {
|
||||
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
|
||||
}
|
||||
}
|
||||
|
||||
val event = handleEncryption(params)
|
||||
|
|
|
@ -47,7 +47,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
|
|||
localEchoRepository.updateSendState(localId, event.roomId, SendState.SENDING)
|
||||
val response = executeRequest(globalErrorReceiver) {
|
||||
roomAPI.send(
|
||||
localId,
|
||||
txId = localId,
|
||||
roomId = event.roomId ?: "",
|
||||
content = event.content,
|
||||
eventType = event.type ?: ""
|
||||
|
|
|
@ -23,8 +23,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerific
|
|||
import org.matrix.android.sdk.api.session.crypto.verification.SasMode
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import timber.log.Timber
|
||||
|
@ -35,8 +35,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
|
|||
override val deviceId: String?,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
crossSigningService: CrossSigningService,
|
||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||
outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||
secretShareManager: SecretShareManager,
|
||||
deviceFingerprint: String,
|
||||
transactionId: String,
|
||||
otherUserID: String,
|
||||
|
@ -47,8 +47,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
|
|||
deviceId,
|
||||
cryptoStore,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager,
|
||||
outgoingKeyRequestManager,
|
||||
secretShareManager,
|
||||
deviceFingerprint,
|
||||
transactionId,
|
||||
otherUserID,
|
||||
|
|
|
@ -20,8 +20,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
|||
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import timber.log.Timber
|
||||
|
@ -32,8 +32,8 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
|||
deviceId: String?,
|
||||
cryptoStore: IMXCryptoStore,
|
||||
crossSigningService: CrossSigningService,
|
||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||
outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||
secretShareManager: SecretShareManager,
|
||||
deviceFingerprint: String,
|
||||
transactionId: String,
|
||||
otherUserId: String,
|
||||
|
@ -44,8 +44,8 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
|||
deviceId,
|
||||
cryptoStore,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager,
|
||||
outgoingKeyRequestManager,
|
||||
secretShareManager,
|
||||
deviceFingerprint,
|
||||
transactionId,
|
||||
otherUserId,
|
||||
|
|
|
@ -59,9 +59,9 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification
|
|||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.ValidVerificationDone
|
||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel
|
||||
|
@ -95,8 +95,8 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
@UserId private val userId: String,
|
||||
@DeviceId private val deviceId: String?,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||
private val secretShareManager: SecretShareManager,
|
||||
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||
|
@ -551,8 +551,8 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
deviceId,
|
||||
cryptoStore,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager,
|
||||
outgoingKeyRequestManager,
|
||||
secretShareManager,
|
||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||
startReq.transactionId,
|
||||
otherUserId,
|
||||
|
@ -771,8 +771,15 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
return
|
||||
}
|
||||
|
||||
val roomId = event.roomId
|
||||
if (roomId == null) {
|
||||
Timber.e("## SAS Verification missing roomId for event")
|
||||
// TODO cancel?
|
||||
return
|
||||
}
|
||||
|
||||
handleReadyReceived(event.senderId, readyReq) {
|
||||
verificationTransportRoomMessageFactory.createTransport(event.roomId!!, it)
|
||||
verificationTransportRoomMessageFactory.createTransport(roomId, it)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -814,21 +821,15 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
getExistingTransaction(userId, doneReq.transactionId)
|
||||
?: getOldTransaction(userId, doneReq.transactionId)
|
||||
?.let { vt ->
|
||||
val otherDeviceId = vt.otherDeviceId
|
||||
val otherDeviceId = vt.otherDeviceId ?: return@let
|
||||
if (!crossSigningService.canCrossSign()) {
|
||||
outgoingGossipingRequestManager.sendSecretShareRequest(
|
||||
MASTER_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*"))
|
||||
)
|
||||
outgoingGossipingRequestManager.sendSecretShareRequest(
|
||||
SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*"))
|
||||
)
|
||||
outgoingGossipingRequestManager.sendSecretShareRequest(
|
||||
USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*"))
|
||||
)
|
||||
cryptoCoroutineScope.launch {
|
||||
secretShareManager.requestSecretTo(otherDeviceId, MASTER_KEY_SSSS_NAME)
|
||||
secretShareManager.requestSecretTo(otherDeviceId, SELF_SIGNING_KEY_SSSS_NAME)
|
||||
secretShareManager.requestSecretTo(otherDeviceId, USER_SIGNING_KEY_SSSS_NAME)
|
||||
secretShareManager.requestSecretTo(otherDeviceId, KEYBACKUP_SECRET_SSSS_NAME)
|
||||
}
|
||||
}
|
||||
outgoingGossipingRequestManager.sendSecretShareRequest(
|
||||
KEYBACKUP_SECRET_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*"))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -922,8 +923,8 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
otherUserId = senderId,
|
||||
otherDeviceId = readyReq.fromDevice,
|
||||
crossSigningService = crossSigningService,
|
||||
outgoingGossipingRequestManager = outgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager = incomingGossipingRequestManager,
|
||||
outgoingKeyRequestManager = outgoingKeyRequestManager,
|
||||
secretShareManager = secretShareManager,
|
||||
cryptoStore = cryptoStore,
|
||||
qrCodeData = qrCodeData,
|
||||
userId = userId,
|
||||
|
@ -1124,8 +1125,8 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
deviceId,
|
||||
cryptoStore,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager,
|
||||
outgoingKeyRequestManager,
|
||||
secretShareManager,
|
||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||
txID,
|
||||
otherUserId,
|
||||
|
@ -1188,6 +1189,7 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
}
|
||||
.distinct()
|
||||
|
||||
requestsForUser.add(verificationRequest)
|
||||
transport.sendVerificationRequest(methodValues, validLocalId, otherUserId, roomId, null) { syncedId, info ->
|
||||
// We need to update with the syncedID
|
||||
updatePendingRequest(
|
||||
|
@ -1199,7 +1201,6 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
requestsForUser.add(verificationRequest)
|
||||
dispatchRequestAdded(verificationRequest)
|
||||
|
||||
return verificationRequest
|
||||
|
@ -1323,8 +1324,8 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
deviceId,
|
||||
cryptoStore,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager,
|
||||
outgoingKeyRequestManager,
|
||||
secretShareManager,
|
||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||
transactionId,
|
||||
otherUserId,
|
||||
|
@ -1465,8 +1466,8 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
otherUserId = otherUserId,
|
||||
otherDeviceId = otherDeviceId,
|
||||
crossSigningService = crossSigningService,
|
||||
outgoingGossipingRequestManager = outgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager = incomingGossipingRequestManager,
|
||||
outgoingKeyRequestManager = outgoingKeyRequestManager,
|
||||
secretShareManager = secretShareManager,
|
||||
cryptoStore = cryptoStore,
|
||||
qrCodeData = qrCodeData,
|
||||
userId = userId,
|
||||
|
|
|
@ -20,8 +20,8 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningServic
|
|||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import timber.log.Timber
|
||||
|
||||
|
@ -31,8 +31,8 @@ import timber.log.Timber
|
|||
internal abstract class DefaultVerificationTransaction(
|
||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||
private val crossSigningService: CrossSigningService,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||
private val secretShareManager: SecretShareManager,
|
||||
private val userId: String,
|
||||
override val transactionId: String,
|
||||
override val otherUserId: String,
|
||||
|
@ -86,7 +86,7 @@ internal abstract class DefaultVerificationTransaction(
|
|||
}
|
||||
|
||||
if (otherUserId == userId) {
|
||||
incomingGossipingRequestManager.onVerificationCompleteForDevice(otherDeviceId!!)
|
||||
secretShareManager.onVerificationCompleteForDevice(otherDeviceId!!)
|
||||
|
||||
// If me it's reasonable to sign and upload the device signature
|
||||
// Notice that i might not have the private keys, so may not be able to do it
|
||||
|
|
|
@ -23,8 +23,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.SasMode
|
|||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.extensions.toUnsignedInt
|
||||
|
@ -42,8 +42,8 @@ internal abstract class SASDefaultVerificationTransaction(
|
|||
open val deviceId: String?,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
crossSigningService: CrossSigningService,
|
||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||
outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||
secretShareManager: SecretShareManager,
|
||||
private val deviceFingerprint: String,
|
||||
transactionId: String,
|
||||
otherUserId: String,
|
||||
|
@ -52,8 +52,8 @@ internal abstract class SASDefaultVerificationTransaction(
|
|||
) : DefaultVerificationTransaction(
|
||||
setDeviceVerificationAction,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager,
|
||||
outgoingKeyRequestManager,
|
||||
secretShareManager,
|
||||
userId,
|
||||
transactionId,
|
||||
otherUserId,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -15,13 +15,9 @@
|
|||
*/
|
||||
package org.matrix.android.sdk.internal.crypto.verification
|
||||
|
||||
import io.realm.Realm
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.LocalEcho
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
|
||||
|
@ -29,22 +25,18 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
|||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent
|
||||
import org.matrix.android.sdk.internal.crypto.EventDecryptor
|
||||
import org.matrix.android.sdk.internal.database.model.EventInsertType
|
||||
import org.matrix.android.sdk.internal.di.DeviceId
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class VerificationMessageProcessor @Inject constructor(
|
||||
private val eventDecryptor: EventDecryptor,
|
||||
private val clock: Clock,
|
||||
private val verificationService: DefaultVerificationService,
|
||||
@UserId private val userId: String,
|
||||
@DeviceId private val deviceId: String?
|
||||
) : EventInsertLiveProcessor {
|
||||
@DeviceId private val deviceId: String?,
|
||||
private val clock: Clock,
|
||||
) {
|
||||
|
||||
private val transactionsHandledByOtherDevice = ArrayList<String>()
|
||||
|
||||
|
@ -60,40 +52,20 @@ internal class VerificationMessageProcessor @Inject constructor(
|
|||
EventType.ENCRYPTED
|
||||
)
|
||||
|
||||
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
|
||||
if (insertType != EventInsertType.INCREMENTAL_SYNC) {
|
||||
return false
|
||||
}
|
||||
return allowedTypes.contains(eventType) && !LocalEcho.isLocalEchoId(eventId)
|
||||
fun shouldProcess(eventType: String): Boolean {
|
||||
return allowedTypes.contains(eventType)
|
||||
}
|
||||
|
||||
override suspend fun process(realm: Realm, event: Event) {
|
||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
|
||||
suspend fun process(event: Event) {
|
||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.getClearType()} from ${event.senderId}")
|
||||
|
||||
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
|
||||
// the message should be ignored by the receiver.
|
||||
|
||||
if (!VerificationService.isValidRequest(event.ageLocalTs ?: event.originServerTs, clock.epochMillis())) return Unit.also {
|
||||
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated")
|
||||
if (event.ageLocalTs != null && !VerificationService.isValidRequest(event.ageLocalTs, clock.epochMillis())) return Unit.also {
|
||||
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated age:$event.ageLocalTs ms")
|
||||
}
|
||||
|
||||
// decrypt if needed?
|
||||
if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
||||
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
||||
// for now decrypt sync
|
||||
try {
|
||||
val result = eventDecryptor.decryptEvent(event, "")
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
|
||||
verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event)
|
||||
}
|
||||
}
|
||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
|
||||
|
||||
// Relates to is not encrypted
|
||||
|
@ -102,7 +74,6 @@ internal class VerificationMessageProcessor @Inject constructor(
|
|||
if (event.senderId == userId) {
|
||||
// If it's send from me, we need to keep track of Requests or Start
|
||||
// done from another device of mine
|
||||
|
||||
if (EventType.MESSAGE == event.getClearType()) {
|
||||
val msgType = event.getClearContent().toModel<MessageContent>()?.msgType
|
||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
|
||||
|
@ -137,6 +108,8 @@ internal class VerificationMessageProcessor @Inject constructor(
|
|||
transactionsHandledByOtherDevice.remove(it)
|
||||
verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
}
|
||||
} else if (EventType.ENCRYPTED == event.getClearType()) {
|
||||
verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event)
|
||||
}
|
||||
|
||||
Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}")
|
||||
|
|
|
@ -15,14 +15,8 @@
|
|||
*/
|
||||
package org.matrix.android.sdk.internal.crypto.verification
|
||||
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Data
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.Operation
|
||||
import androidx.work.WorkInfo
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest
|
||||
|
@ -45,27 +39,28 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification
|
|||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
|
||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
internal class VerificationTransportRoomMessage(
|
||||
private val workManagerProvider: WorkManagerProvider,
|
||||
private val sessionId: String,
|
||||
private val sendVerificationMessageTask: SendVerificationMessageTask,
|
||||
private val userId: String,
|
||||
private val userDeviceId: String?,
|
||||
private val roomId: String,
|
||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val tx: DefaultVerificationTransaction?,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
cryptoCoroutineScope: CoroutineScope,
|
||||
private val clock: Clock,
|
||||
) : VerificationTransport {
|
||||
|
||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val verificationSenderScope = CoroutineScope(cryptoCoroutineScope.coroutineContext + dispatcher)
|
||||
private val sequencer = SemaphoreCoroutineSequencer()
|
||||
|
||||
override fun <T> sendToOther(type: String,
|
||||
verificationInfo: VerificationInfo<T>,
|
||||
nextState: VerificationTxState,
|
||||
|
@ -79,70 +74,22 @@ internal class VerificationTransportRoomMessage(
|
|||
content = verificationInfo.toEventContent()!!
|
||||
)
|
||||
|
||||
val workerParams = WorkerParamsFactory.toData(
|
||||
SendVerificationMessageWorker.Params(
|
||||
sessionId = sessionId,
|
||||
eventId = event.eventId ?: ""
|
||||
)
|
||||
)
|
||||
val enqueueInfo = enqueueSendWork(workerParams)
|
||||
|
||||
// I cannot just listen to the given work request, because when used in a uniqueWork,
|
||||
// The callback is called while it is still Running ...
|
||||
|
||||
// Futures.addCallback(enqueueInfo.first.result, object : FutureCallback<Operation.State.SUCCESS> {
|
||||
// override fun onSuccess(result: Operation.State.SUCCESS?) {
|
||||
// if (onDone != null) {
|
||||
// onDone()
|
||||
// } else {
|
||||
// tx?.state = nextState
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onFailure(t: Throwable) {
|
||||
// Timber.e("## SAS verification [${tx?.transactionId}] failed to send toDevice in state : ${tx?.state}, reason: ${t.localizedMessage}")
|
||||
// tx?.cancel(onErrorReason)
|
||||
// }
|
||||
// }, listenerExecutor)
|
||||
|
||||
val workLiveData = workManagerProvider.workManager
|
||||
.getWorkInfosForUniqueWorkLiveData(uniqueQueueName())
|
||||
|
||||
val observer = object : Observer<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
|
||||
}
|
||||
}
|
||||
}
|
||||
verificationSenderScope.launch {
|
||||
sequencer.post {
|
||||
try {
|
||||
val params = SendVerificationMessageTask.Params(event)
|
||||
sendVerificationMessageTask.executeRetry(params, 5)
|
||||
// Do I need to update local echo state to sent?
|
||||
if (onDone != null) {
|
||||
onDone()
|
||||
} else {
|
||||
tx?.state = nextState
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
tx?.cancel(onErrorReason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO listen to DB to get synced info
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
workLiveData.observeForever(observer)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendVerificationRequest(supportedMethods: List<String>,
|
||||
|
@ -173,60 +120,24 @@ internal class VerificationTransportRoomMessage(
|
|||
val content = info.toContent()
|
||||
|
||||
val event = createEventAndLocalEcho(
|
||||
localId,
|
||||
EventType.MESSAGE,
|
||||
roomId,
|
||||
content
|
||||
localId = localId,
|
||||
type = EventType.MESSAGE,
|
||||
roomId = roomId,
|
||||
content = content
|
||||
)
|
||||
|
||||
val workerParams = WorkerParamsFactory.toData(
|
||||
SendVerificationMessageWorker.Params(
|
||||
sessionId = sessionId,
|
||||
eventId = event.eventId ?: ""
|
||||
)
|
||||
)
|
||||
|
||||
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SendVerificationMessageWorker>()
|
||||
.setConstraints(WorkManagerProvider.workConstraints)
|
||||
.setInputData(workerParams)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
|
||||
workManagerProvider.workManager
|
||||
.beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
|
||||
.enqueue()
|
||||
|
||||
// I cannot just listen to the given work request, because when used in a uniqueWork,
|
||||
// The callback is called while it is still Running ...
|
||||
|
||||
val workLiveData = workManagerProvider.workManager
|
||||
.getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork")
|
||||
|
||||
val observer = object : Observer<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)
|
||||
}
|
||||
verificationSenderScope.launch {
|
||||
val params = SendVerificationMessageTask.Params(event)
|
||||
sequencer.post {
|
||||
try {
|
||||
val eventId = sendVerificationMessageTask.executeRetry(params, 5)
|
||||
// Do I need to update local echo state to sent?
|
||||
callback(eventId, validInfo)
|
||||
} catch (failure: Throwable) {
|
||||
callback(null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO listen to DB to get synced info
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
workLiveData.observeForever(observer)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String?, code: CancelCode) {
|
||||
|
@ -236,13 +147,17 @@ internal class VerificationTransportRoomMessage(
|
|||
roomId = roomId,
|
||||
content = MessageVerificationCancelContent.create(transactionId, code).toContent()
|
||||
)
|
||||
val workerParams = WorkerParamsFactory.toData(
|
||||
SendVerificationMessageWorker.Params(
|
||||
sessionId = sessionId,
|
||||
eventId = event.eventId ?: ""
|
||||
)
|
||||
)
|
||||
enqueueSendWork(workerParams)
|
||||
|
||||
verificationSenderScope.launch {
|
||||
sequencer.post {
|
||||
try {
|
||||
val params = SendVerificationMessageTask.Params(event)
|
||||
sendVerificationMessageTask.executeRetry(params, 5)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.w(failure, "Failed to cancel verification transaction")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun done(transactionId: String,
|
||||
|
@ -258,47 +173,21 @@ internal class VerificationTransportRoomMessage(
|
|||
)
|
||||
).toContent()
|
||||
)
|
||||
val workerParams = WorkerParamsFactory.toData(
|
||||
SendVerificationMessageWorker.Params(
|
||||
sessionId = sessionId,
|
||||
eventId = event.eventId ?: ""
|
||||
)
|
||||
)
|
||||
val enqueueInfo = enqueueSendWork(workerParams)
|
||||
|
||||
val workLiveData = workManagerProvider.workManager
|
||||
.getWorkInfosForUniqueWorkLiveData(uniqueQueueName())
|
||||
val observer = object : Observer<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)
|
||||
}
|
||||
verificationSenderScope.launch {
|
||||
sequencer.post {
|
||||
try {
|
||||
val params = SendVerificationMessageTask.Params(event)
|
||||
sendVerificationMessageTask.executeRetry(params, 5)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.w(failure, "Failed to complete (done) verification")
|
||||
// should we call onDone?
|
||||
} finally {
|
||||
onDone?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO listen to DB to get synced info
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
workLiveData.observeForever(observer)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enqueueSendWork(workerParams: Data): Pair<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,
|
||||
keyAgreementProtocol: String,
|
||||
hash: String,
|
||||
|
|
|
@ -16,39 +16,35 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.crypto.verification
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
|
||||
import org.matrix.android.sdk.internal.di.DeviceId
|
||||
import org.matrix.android.sdk.internal.di.SessionId
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class VerificationTransportRoomMessageFactory @Inject constructor(
|
||||
private val workManagerProvider: WorkManagerProvider,
|
||||
@SessionId
|
||||
private val sessionId: String,
|
||||
private val sendVerificationMessageTask: SendVerificationMessageTask,
|
||||
@UserId
|
||||
private val userId: String,
|
||||
@DeviceId
|
||||
private val deviceId: String?,
|
||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val clock: Clock,
|
||||
) {
|
||||
|
||||
fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage {
|
||||
return VerificationTransportRoomMessage(
|
||||
workManagerProvider,
|
||||
sessionId,
|
||||
userId,
|
||||
deviceId,
|
||||
roomId,
|
||||
localEchoEventFactory,
|
||||
tx,
|
||||
taskExecutor.executorScope,
|
||||
clock
|
||||
sendVerificationMessageTask = sendVerificationMessageTask,
|
||||
userId = userId,
|
||||
userDeviceId = deviceId,
|
||||
roomId = roomId,
|
||||
localEchoEventFactory = localEchoEventFactory,
|
||||
tx = tx,
|
||||
cryptoCoroutineScope = cryptoCoroutineScope,
|
||||
clock = clock,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,8 +22,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerification
|
|||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.util.fromBase64
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64Safe
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
|
@ -37,8 +37,8 @@ internal class DefaultQrCodeVerificationTransaction(
|
|||
override val otherUserId: String,
|
||||
override var otherDeviceId: String?,
|
||||
private val crossSigningService: CrossSigningService,
|
||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||
outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||
secretShareManager: SecretShareManager,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
// Not null only if other user is able to scan QR code
|
||||
private val qrCodeData: QrCodeData?,
|
||||
|
@ -48,8 +48,8 @@ internal class DefaultQrCodeVerificationTransaction(
|
|||
) : DefaultVerificationTransaction(
|
||||
setDeviceVerificationAction,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager,
|
||||
outgoingKeyRequestManager,
|
||||
secretShareManager,
|
||||
userId,
|
||||
transactionId,
|
||||
otherUserId,
|
||||
|
|
|
@ -21,12 +21,8 @@ import dagger.Component
|
|||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker
|
||||
import org.matrix.android.sdk.internal.crypto.CryptoModule
|
||||
import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker
|
||||
import org.matrix.android.sdk.internal.crypto.SendGossipWorker
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
|
||||
import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker
|
||||
import org.matrix.android.sdk.internal.di.MatrixComponent
|
||||
import org.matrix.android.sdk.internal.federation.FederationModule
|
||||
import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
|
||||
|
@ -133,14 +129,6 @@ internal interface SessionComponent {
|
|||
|
||||
fun inject(worker: AddPusherWorker)
|
||||
|
||||
fun inject(worker: SendVerificationMessageWorker)
|
||||
|
||||
fun inject(worker: SendGossipRequestWorker)
|
||||
|
||||
fun inject(worker: CancelGossipRequestWorker)
|
||||
|
||||
fun inject(worker: SendGossipWorker)
|
||||
|
||||
fun inject(worker: UpdateTrustWorker)
|
||||
|
||||
@Component.Factory
|
||||
|
|
|
@ -49,7 +49,6 @@ import org.matrix.android.sdk.api.util.md5
|
|||
import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
|
||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor
|
||||
import org.matrix.android.sdk.internal.database.EventInsertLiveObserver
|
||||
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
||||
import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory
|
||||
|
@ -318,10 +317,6 @@ internal abstract class SessionModule {
|
|||
@IntoSet
|
||||
abstract fun bindRoomCreateEventProcessor(processor: RoomCreateEventProcessor): EventInsertLiveProcessor
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindVerificationMessageProcessor(processor: VerificationMessageProcessor): EventInsertLiveProcessor
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindCallEventProcessor(processor: CallEventProcessor): EventInsertLiveProcessor
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.membership.joining
|
|||
|
||||
import io.realm.RealmConfiguration
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult
|
||||
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
|
||||
|
@ -53,6 +54,7 @@ internal class DefaultJoinRoomTask @Inject constructor(
|
|||
private val readMarkersTask: SetReadMarkersTask,
|
||||
@SessionDatabase
|
||||
private val realmConfiguration: RealmConfiguration,
|
||||
private val coroutineDispatcher: MatrixCoroutineDispatchers,
|
||||
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
|
||||
private val globalErrorReceiver: GlobalErrorReceiver,
|
||||
private val clock: Clock,
|
||||
|
|
|
@ -382,7 +382,10 @@ internal class RoomSyncHandler @Inject constructor(
|
|||
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
|
||||
|
||||
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) {
|
||||
continue
|
||||
}
|
||||
|
@ -454,7 +457,7 @@ internal class RoomSyncHandler @Inject constructor(
|
|||
}
|
||||
}
|
||||
// Give info to crypto module
|
||||
cryptoService.onLiveEvent(roomEntity.roomId, event)
|
||||
cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync)
|
||||
|
||||
// Try to remove local echo
|
||||
event.unsignedData?.transactionId?.also {
|
||||
|
|
|
@ -22,11 +22,7 @@ import androidx.work.ListenableWorker
|
|||
import androidx.work.WorkerFactory
|
||||
import androidx.work.WorkerParameters
|
||||
import org.matrix.android.sdk.internal.SessionManager
|
||||
import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker
|
||||
import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker
|
||||
import org.matrix.android.sdk.internal.crypto.SendGossipWorker
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
|
||||
import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker
|
||||
import org.matrix.android.sdk.internal.di.MatrixScope
|
||||
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
|
||||
import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker
|
||||
|
@ -56,8 +52,6 @@ internal class MatrixWorkerFactory @Inject constructor(private val sessionManage
|
|||
CheckFactoryWorker(appContext, workerParameters, true)
|
||||
AddPusherWorker::class.java.name ->
|
||||
AddPusherWorker(appContext, workerParameters, sessionManager)
|
||||
CancelGossipRequestWorker::class.java.name ->
|
||||
CancelGossipRequestWorker(appContext, workerParameters, sessionManager)
|
||||
GetGroupDataWorker::class.java.name ->
|
||||
GetGroupDataWorker(appContext, workerParameters, sessionManager)
|
||||
MultipleEventSendingDispatcherWorker::class.java.name ->
|
||||
|
@ -66,12 +60,6 @@ internal class MatrixWorkerFactory @Inject constructor(private val sessionManage
|
|||
RedactEventWorker(appContext, workerParameters, sessionManager)
|
||||
SendEventWorker::class.java.name ->
|
||||
SendEventWorker(appContext, workerParameters, sessionManager)
|
||||
SendGossipRequestWorker::class.java.name ->
|
||||
SendGossipRequestWorker(appContext, workerParameters, sessionManager)
|
||||
SendGossipWorker::class.java.name ->
|
||||
SendGossipWorker(appContext, workerParameters, sessionManager)
|
||||
SendVerificationMessageWorker::class.java.name ->
|
||||
SendVerificationMessageWorker(appContext, workerParameters, sessionManager)
|
||||
SyncWorker::class.java.name ->
|
||||
SyncWorker(appContext, workerParameters, sessionManager)
|
||||
UpdateTrustWorker::class.java.name ->
|
||||
|
|
|
@ -22,16 +22,17 @@ import im.vector.app.core.date.DateFormatKind
|
|||
import im.vector.app.core.date.VectorDateFormatter
|
||||
import im.vector.app.features.popup.DefaultVectorAlert
|
||||
import im.vector.app.features.popup.PopupAlertManager
|
||||
import im.vector.app.features.session.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
||||
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
|
||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRequestCancellation
|
||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
||||
|
@ -60,6 +61,9 @@ class KeyRequestHandler @Inject constructor(
|
|||
|
||||
var session: Session? = null
|
||||
|
||||
// This functionality is disabled in element for now. As it could be prone to social attacks
|
||||
var enablePromptingForRequest = false
|
||||
|
||||
fun start(session: Session) {
|
||||
this.session = session
|
||||
session.cryptoService().verificationService().addListener(this)
|
||||
|
@ -72,10 +76,9 @@ class KeyRequestHandler @Inject constructor(
|
|||
session = null
|
||||
}
|
||||
|
||||
override fun onSecretShareRequest(request: IncomingSecretShareRequest): Boolean {
|
||||
override fun onSecretShareRequest(request: SecretShareRequest): Boolean {
|
||||
// By default Element will not prompt if the SDK has decided that the request should not be fulfilled
|
||||
Timber.v("## onSecretShareRequest() : Ignoring $request")
|
||||
request.ignore?.run()
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -85,6 +88,8 @@ class KeyRequestHandler @Inject constructor(
|
|||
* @param request the key request.
|
||||
*/
|
||||
override fun onRoomKeyRequest(request: IncomingRoomKeyRequest) {
|
||||
if (!enablePromptingForRequest) return
|
||||
|
||||
val userId = request.userId
|
||||
val deviceId = request.deviceId
|
||||
val requestId = request.requestId
|
||||
|
@ -195,15 +200,14 @@ class KeyRequestHandler @Inject constructor(
|
|||
}
|
||||
|
||||
private fun denyAllRequests(mappingKey: String) {
|
||||
alertsToRequests[mappingKey]?.forEach {
|
||||
it.ignore?.run()
|
||||
}
|
||||
alertsToRequests.remove(mappingKey)
|
||||
}
|
||||
|
||||
private fun shareAllSessions(mappingKey: String) {
|
||||
alertsToRequests[mappingKey]?.forEach {
|
||||
it.share?.run()
|
||||
session?.coroutineScope?.launch {
|
||||
session?.cryptoService()?.manuallyAcceptRoomKeyRequest(it)
|
||||
}
|
||||
}
|
||||
alertsToRequests.remove(mappingKey)
|
||||
}
|
||||
|
@ -213,7 +217,7 @@ class KeyRequestHandler @Inject constructor(
|
|||
*
|
||||
* @param request the cancellation request.
|
||||
*/
|
||||
override fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) {
|
||||
override fun onRequestCancelled(request: IncomingRoomKeyRequest) {
|
||||
// see if we can find the request in the queue
|
||||
val userId = request.userId
|
||||
val deviceId = request.deviceId
|
||||
|
|
|
@ -20,7 +20,6 @@ import im.vector.app.R
|
|||
import im.vector.app.core.platform.ViewModelTask
|
||||
import im.vector.app.core.platform.WaitingViewData
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
|
@ -149,15 +148,8 @@ class BackupToQuadSMigrationTask @Inject constructor(
|
|||
// save for gossiping
|
||||
keysBackupService.saveBackupRecoveryKey(recoveryKey, version.version)
|
||||
|
||||
// while we are there let's restore, but do not block
|
||||
session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
|
||||
version,
|
||||
recoveryKey,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
NoOpMatrixCallback()
|
||||
)
|
||||
// It's not a good idea to download the full backup, it might take very long
|
||||
// and use a lot of resources
|
||||
|
||||
return Result.Success
|
||||
} catch (failure: Throwable) {
|
||||
|
|
|
@ -42,7 +42,6 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
|
|||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
|
||||
|
@ -424,6 +423,11 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun tentativeRestoreBackup(res: Map<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) {
|
||||
try {
|
||||
val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also {
|
||||
|
@ -434,17 +438,13 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
|||
session.cryptoService().keysBackupService().getCurrentVersion(it)
|
||||
}.toKeysVersionResult() ?: return@launch
|
||||
|
||||
awaitCallback<ImportRoomKeysResult> {
|
||||
session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
|
||||
version,
|
||||
computeRecoveryKey(secret.fromBase64()),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
it
|
||||
)
|
||||
val recoveryKey = computeRecoveryKey(secret.fromBase64())
|
||||
val isValid = awaitCallback<Boolean> {
|
||||
session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(recoveryKey, it)
|
||||
}
|
||||
if (isValid) {
|
||||
session.cryptoService().keysBackupService().saveBackupRecoveryKey(recoveryKey, version.version)
|
||||
}
|
||||
|
||||
awaitCallback<Unit> {
|
||||
session.cryptoService().keysBackupService().trustKeysBackupVersion(version, true, it)
|
||||
}
|
||||
|
|
|
@ -27,10 +27,8 @@ import im.vector.app.core.extensions.cleanup
|
|||
import im.vector.app.core.extensions.configureWith
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.utils.createJSonViewerStyleProvider
|
||||
import im.vector.app.databinding.FragmentGenericRecyclerBinding
|
||||
import org.billcarsonfr.jsonviewer.JSonViewerDialog
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
|
||||
import javax.inject.Inject
|
||||
|
||||
class GossipingEventsPaperTrailFragment @Inject constructor(
|
||||
|
@ -64,17 +62,17 @@ class GossipingEventsPaperTrailFragment @Inject constructor(
|
|||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun didTap(event: Event) {
|
||||
if (event.isEncrypted()) {
|
||||
event.toClearContentStringWithIndent()
|
||||
} else {
|
||||
event.toContentStringWithIndent()
|
||||
}?.let {
|
||||
JSonViewerDialog.newInstance(
|
||||
it,
|
||||
-1,
|
||||
createJSonViewerStyleProvider(colorProvider)
|
||||
).show(childFragmentManager, "JSON_VIEWER")
|
||||
}
|
||||
override fun didTap(event: AuditTrail) {
|
||||
// if (event.isEncrypted()) {
|
||||
// event.toClearContentStringWithIndent()
|
||||
// } else {
|
||||
// event.toContentStringWithIndent()
|
||||
// }?.let {
|
||||
// JSonViewerDialog.newInstance(
|
||||
// it,
|
||||
// -1,
|
||||
// createJSonViewerStyleProvider(colorProvider)
|
||||
// ).show(childFragmentManager, "JSON_VIEWER")
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,10 +32,10 @@ import im.vector.app.core.platform.EmptyAction
|
|||
import im.vector.app.core.platform.EmptyViewEvents
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
|
||||
|
||||
data class GossipingEventsPaperTrailState(
|
||||
val events: Async<PagedList<Event>> = Uninitialized
|
||||
val events: Async<PagedList<AuditTrail>> = Uninitialized
|
||||
) : MavericksState
|
||||
|
||||
class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted initialState: GossipingEventsPaperTrailState,
|
||||
|
|
|
@ -17,62 +17,43 @@
|
|||
package im.vector.app.features.settings.devtools
|
||||
|
||||
import im.vector.app.core.resources.DateProvider
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent
|
||||
import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
|
||||
import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.TrailType
|
||||
import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo
|
||||
import org.threeten.bp.format.DateTimeFormatter
|
||||
|
||||
class GossipingEventsSerializer {
|
||||
private val full24DateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
|
||||
|
||||
fun serialize(eventList: List<Event>): String {
|
||||
fun serialize(eventList: List<AuditTrail>): String {
|
||||
return buildString {
|
||||
eventList.forEach {
|
||||
val clearType = it.getClearType()
|
||||
append("[${getFormattedDate(it.ageLocalTs)}] $clearType from:${it.senderId} - ")
|
||||
when (clearType) {
|
||||
EventType.ROOM_KEY_REQUEST -> {
|
||||
val content = it.getClearContent().toModel<RoomKeyShareRequest>()
|
||||
append("reqId:${content?.requestId} action:${content?.action} ")
|
||||
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
|
||||
append("sessionId: ${content.body?.sessionId} ")
|
||||
}
|
||||
append("requestedBy: ${content?.requestingDeviceId}")
|
||||
eventList.forEach { trail ->
|
||||
val type = trail.type
|
||||
val info = trail.info
|
||||
append("[${getFormattedDate(trail.ageLocalTs)}] ${type.name} ")
|
||||
append("sessionId: ${info.sessionId} ")
|
||||
when (type) {
|
||||
TrailType.IncomingKeyRequest -> {
|
||||
append("from:${info.userId}|${info.deviceId} - ")
|
||||
}
|
||||
EventType.FORWARDED_ROOM_KEY -> {
|
||||
val encryptedContent = it.content.toModel<OlmEventContent>()
|
||||
val content = it.getClearContent().toModel<ForwardedRoomKeyContent>()
|
||||
|
||||
append("sessionId:${content?.sessionId} From Device (sender key):${encryptedContent?.senderKey}")
|
||||
span("\nFrom Device (sender key):") {
|
||||
textStyle = "bold"
|
||||
TrailType.OutgoingKeyForward -> {
|
||||
append("to:${info.userId}|${info.deviceId} - ")
|
||||
(trail.info as? ForwardInfo)?.let {
|
||||
append("chainIndex: ${it.chainIndex} ")
|
||||
}
|
||||
}
|
||||
EventType.ROOM_KEY -> {
|
||||
val content = it.getClearContent()
|
||||
append("sessionId:${content?.get("session_id")} roomId:${content?.get("room_id")} dest:${content?.get("_dest") ?: "me"}")
|
||||
}
|
||||
EventType.SEND_SECRET -> {
|
||||
val content = it.getClearContent().toModel<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} ")
|
||||
TrailType.OutgoingKeyWithheld -> {
|
||||
append("to:${info.userId}|${info.deviceId} - ")
|
||||
(trail.info as? WithheldInfo)?.let {
|
||||
append("code: ${it.code} ")
|
||||
}
|
||||
append("requestedBy:${content?.requestingDeviceId}")
|
||||
}
|
||||
EventType.ENCRYPTED -> {
|
||||
append("Failed to Decrypt")
|
||||
TrailType.IncomingKeyForward -> {
|
||||
append("from:${info.userId}|${info.deviceId} - ")
|
||||
(trail.info as? ForwardInfo)?.let {
|
||||
append("chainIndex: ${it.chainIndex} ")
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
append("??")
|
||||
|
|
|
@ -18,7 +18,6 @@ package im.vector.app.features.settings.devtools
|
|||
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.paging.PagedListEpoxyController
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.date.DateFormatKind
|
||||
import im.vector.app.core.date.VectorDateFormatter
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
|
@ -26,137 +25,75 @@ import im.vector.app.core.ui.list.GenericItem_
|
|||
import im.vector.app.core.utils.createUIHandler
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent
|
||||
import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
|
||||
import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.TrailType
|
||||
import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo
|
||||
import javax.inject.Inject
|
||||
|
||||
class GossipingTrailPagedEpoxyController @Inject constructor(
|
||||
private val vectorDateFormatter: VectorDateFormatter,
|
||||
private val colorProvider: ColorProvider
|
||||
) : PagedListEpoxyController<Event>(
|
||||
) : PagedListEpoxyController<AuditTrail>(
|
||||
// Important it must match the PageList builder notify Looper
|
||||
modelBuildingHandler = createUIHandler()
|
||||
) {
|
||||
|
||||
interface InteractionListener {
|
||||
fun didTap(event: Event)
|
||||
fun didTap(event: AuditTrail)
|
||||
}
|
||||
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
override fun buildItemModel(currentPosition: Int, item: Event?): EpoxyModel<*> {
|
||||
override fun buildItemModel(currentPosition: Int, item: AuditTrail?): EpoxyModel<*> {
|
||||
val host = this
|
||||
val event = item ?: return GenericItem_().apply { id(currentPosition) }
|
||||
return GenericItem_().apply {
|
||||
id(event.hashCode())
|
||||
itemClickAction { host.interactionListener?.didTap(event) }
|
||||
title(
|
||||
if (event.isEncrypted()) {
|
||||
"${event.getClearType()} [encrypted]"
|
||||
} else {
|
||||
event.type
|
||||
}?.toEpoxyCharSequence()
|
||||
)
|
||||
title(event.type.name.toEpoxyCharSequence())
|
||||
description(
|
||||
span {
|
||||
+host.vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
|
||||
span("\nfrom: ") {
|
||||
span("\n${host.senderFromTo(event.type)}: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${event.senderId}"
|
||||
+"${event.info.userId}|${event.info.deviceId}"
|
||||
span("\nroomId: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+event.info.roomId
|
||||
span("\nsessionId: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+event.info.sessionId
|
||||
apply {
|
||||
if (event.getClearType() == EventType.ROOM_KEY_REQUEST) {
|
||||
val content = event.getClearContent().toModel<RoomKeyShareRequest>()
|
||||
span("\nreqId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.requestId}"
|
||||
span("\naction:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.action}"
|
||||
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
|
||||
span("\nsessionId:") {
|
||||
when (event.type) {
|
||||
TrailType.OutgoingKeyForward -> {
|
||||
val fInfo = event.info as ForwardInfo
|
||||
span("\nchainIndex: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content.body?.sessionId}"
|
||||
+"${fInfo.chainIndex}"
|
||||
}
|
||||
span("\nrequestedBy: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${content?.requestingDeviceId}"
|
||||
} else if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
||||
val encryptedContent = event.content.toModel<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:") {
|
||||
TrailType.OutgoingKeyWithheld -> {
|
||||
val fInfo = event.info as WithheldInfo
|
||||
span("\ncode: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content.secretName}"
|
||||
+"${fInfo.code}"
|
||||
}
|
||||
span("\nrequestedBy: ") {
|
||||
textStyle = "bold"
|
||||
TrailType.IncomingKeyRequest -> {
|
||||
// no additional info
|
||||
}
|
||||
+"${content?.requestingDeviceId}"
|
||||
} else if (event.getClearType() == EventType.ENCRYPTED) {
|
||||
span("**Failed to Decrypt** ${event.mCryptoError}") {
|
||||
textColor = host.colorProvider.getColorFromAttribute(R.attr.colorError)
|
||||
TrailType.IncomingKeyForward -> {
|
||||
val fInfo = event.info as ForwardInfo
|
||||
span("\nchainIndex: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${fInfo.chainIndex}"
|
||||
}
|
||||
TrailType.Unknown -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -164,4 +101,14 @@ class GossipingTrailPagedEpoxyController @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun senderFromTo(type: TrailType): String {
|
||||
return when (type) {
|
||||
TrailType.OutgoingKeyWithheld,
|
||||
TrailType.OutgoingKeyForward -> "to"
|
||||
TrailType.IncomingKeyRequest,
|
||||
TrailType.IncomingKeyForward -> "from"
|
||||
TrailType.Unknown -> ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ class IncomingKeyRequestPagedController @Inject constructor(
|
|||
textStyle = "bold"
|
||||
}
|
||||
span("${roomKeyRequest.userId}")
|
||||
+"\n"
|
||||
+host.vectorDateFormatter.format(roomKeyRequest.localCreationTimestamp, DateFormatKind.DEFAULT_DATE_AND_TIME)
|
||||
span("\nsessionId:") {
|
||||
textStyle = "bold"
|
||||
|
@ -62,10 +63,6 @@ class IncomingKeyRequestPagedController @Inject constructor(
|
|||
textStyle = "bold"
|
||||
}
|
||||
+"${roomKeyRequest.deviceId}"
|
||||
span("\nstate: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+roomKeyRequest.state.name
|
||||
}.toEpoxyCharSequence()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -32,12 +32,12 @@ import im.vector.app.core.platform.EmptyViewEvents
|
|||
import im.vector.app.core.platform.VectorViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
|
||||
|
||||
data class KeyRequestListViewState(
|
||||
val incomingRequests: Async<PagedList<IncomingRoomKeyRequest>> = Uninitialized,
|
||||
val outgoingRoomKeyRequests: Async<PagedList<OutgoingRoomKeyRequest>> = Uninitialized
|
||||
val outgoingRoomKeyRequests: Async<PagedList<OutgoingKeyRequest>> = Uninitialized
|
||||
) : MavericksState
|
||||
|
||||
class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState: KeyRequestListViewState,
|
||||
|
|
|
@ -22,10 +22,10 @@ import im.vector.app.core.ui.list.GenericItem_
|
|||
import im.vector.app.core.utils.createUIHandler
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
|
||||
import javax.inject.Inject
|
||||
|
||||
class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyController<OutgoingRoomKeyRequest>(
|
||||
class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyController<OutgoingKeyRequest>(
|
||||
// Important it must match the PageList builder notify Looper
|
||||
modelBuildingHandler = createUIHandler()
|
||||
) {
|
||||
|
@ -36,7 +36,7 @@ class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyCo
|
|||
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
override fun buildItemModel(currentPosition: Int, item: OutgoingRoomKeyRequest?): EpoxyModel<*> {
|
||||
override fun buildItemModel(currentPosition: Int, item: OutgoingKeyRequest?): EpoxyModel<*> {
|
||||
val roomKeyRequest = item ?: return GenericItem_().apply { id(currentPosition) }
|
||||
|
||||
return GenericItem_().apply {
|
||||
|
|
|
@ -66,7 +66,7 @@ class FakeSharedSecretStorageService : SharedSecretStorageService {
|
|||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue