mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-22 09:25:49 +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.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
import org.amshove.kluent.fail
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
|
@ -31,8 +32,16 @@ import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
||||||
|
@ -40,13 +49,19 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxStat
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.getRoom
|
import org.matrix.android.sdk.api.session.getRoom
|
||||||
import org.matrix.android.sdk.api.session.room.Room
|
import org.matrix.android.sdk.api.session.room.Room
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||||
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||||
|
import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner
|
||||||
|
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
import org.matrix.android.sdk.api.util.awaitCallback
|
||||||
|
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.coroutines.Continuation
|
import kotlin.coroutines.Continuation
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
@ -296,33 +311,94 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize cross-signing, set up megolm backup and save all in 4S
|
||||||
|
*/
|
||||||
|
fun bootstrapSecurity(session: Session) {
|
||||||
|
initializeCrossSigning(session)
|
||||||
|
val ssssService = session.sharedSecretStorageService()
|
||||||
|
testHelper.runBlockingTest {
|
||||||
|
val keyInfo = ssssService.generateKey(
|
||||||
|
UUID.randomUUID().toString(),
|
||||||
|
null,
|
||||||
|
"ssss_key",
|
||||||
|
EmptyKeySigner()
|
||||||
|
)
|
||||||
|
ssssService.setDefaultKey(keyInfo.keyId)
|
||||||
|
|
||||||
|
ssssService.storeSecret(
|
||||||
|
MASTER_KEY_SSSS_NAME,
|
||||||
|
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master!!,
|
||||||
|
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
|
||||||
|
)
|
||||||
|
|
||||||
|
ssssService.storeSecret(
|
||||||
|
SELF_SIGNING_KEY_SSSS_NAME,
|
||||||
|
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned!!,
|
||||||
|
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
|
||||||
|
)
|
||||||
|
|
||||||
|
ssssService.storeSecret(
|
||||||
|
USER_SIGNING_KEY_SSSS_NAME,
|
||||||
|
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user!!,
|
||||||
|
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
|
||||||
|
)
|
||||||
|
|
||||||
|
// set up megolm backup
|
||||||
|
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
|
||||||
|
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
|
||||||
|
}
|
||||||
|
val version = awaitCallback<KeysVersion> {
|
||||||
|
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
|
||||||
|
}
|
||||||
|
// Save it for gossiping
|
||||||
|
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
|
||||||
|
|
||||||
|
extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret ->
|
||||||
|
ssssService.storeSecret(
|
||||||
|
KEYBACKUP_SECRET_SSSS_NAME,
|
||||||
|
secret,
|
||||||
|
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) {
|
fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) {
|
||||||
assertTrue(alice.cryptoService().crossSigningService().canCrossSign())
|
assertTrue(alice.cryptoService().crossSigningService().canCrossSign())
|
||||||
assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
|
assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
|
||||||
|
|
||||||
val requestID = UUID.randomUUID().toString()
|
|
||||||
val aliceVerificationService = alice.cryptoService().verificationService()
|
val aliceVerificationService = alice.cryptoService().verificationService()
|
||||||
val bobVerificationService = bob.cryptoService().verificationService()
|
val bobVerificationService = bob.cryptoService().verificationService()
|
||||||
|
|
||||||
aliceVerificationService.beginKeyVerificationInDMs(
|
val localId = UUID.randomUUID().toString()
|
||||||
VerificationMethod.SAS,
|
aliceVerificationService.requestKeyVerificationInDMs(
|
||||||
requestID,
|
localId = localId,
|
||||||
roomId,
|
methods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
|
||||||
bob.myUserId,
|
otherUserId = bob.myUserId,
|
||||||
bob.sessionParams.credentials.deviceId!!
|
roomId = roomId
|
||||||
)
|
).transactionId
|
||||||
|
|
||||||
// we should reach SHOW SAS on both
|
|
||||||
var alicePovTx: OutgoingSasVerificationTransaction? = null
|
|
||||||
var bobPovTx: IncomingSasVerificationTransaction? = null
|
|
||||||
|
|
||||||
// wait for alice to get the ready
|
|
||||||
testHelper.waitWithLatch {
|
testHelper.waitWithLatch {
|
||||||
testHelper.retryPeriodicallyWithLatch(it) {
|
testHelper.retryPeriodicallyWithLatch(it) {
|
||||||
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
|
bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull {
|
||||||
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
|
it.requestInfo?.fromDevice == alice.sessionParams.deviceId
|
||||||
if (bobPovTx?.state == VerificationTxState.OnStarted) {
|
} != null
|
||||||
bobPovTx?.performAccept()
|
}
|
||||||
|
}
|
||||||
|
val incomingRequest = bobVerificationService.getExistingVerificationRequests(alice.myUserId).first {
|
||||||
|
it.requestInfo?.fromDevice == alice.sessionParams.deviceId
|
||||||
|
}
|
||||||
|
bobVerificationService.readyPendingVerification(listOf(VerificationMethod.SAS), alice.myUserId, incomingRequest.transactionId!!)
|
||||||
|
|
||||||
|
var requestID: String? = null
|
||||||
|
// wait for it to be readied
|
||||||
|
testHelper.waitWithLatch {
|
||||||
|
testHelper.retryPeriodicallyWithLatch(it) {
|
||||||
|
val outgoingRequest = aliceVerificationService.getExistingVerificationRequests(bob.myUserId)
|
||||||
|
.firstOrNull { it.localId == localId }
|
||||||
|
if (outgoingRequest?.isReady == true) {
|
||||||
|
requestID = outgoingRequest.transactionId!!
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@ -330,9 +406,20 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aliceVerificationService.beginKeyVerificationInDMs(
|
||||||
|
VerificationMethod.SAS,
|
||||||
|
requestID!!,
|
||||||
|
roomId,
|
||||||
|
bob.myUserId,
|
||||||
|
bob.sessionParams.credentials.deviceId!!)
|
||||||
|
|
||||||
|
// we should reach SHOW SAS on both
|
||||||
|
var alicePovTx: OutgoingSasVerificationTransaction? = null
|
||||||
|
var bobPovTx: IncomingSasVerificationTransaction? = null
|
||||||
|
|
||||||
testHelper.waitWithLatch {
|
testHelper.waitWithLatch {
|
||||||
testHelper.retryPeriodicallyWithLatch(it) {
|
testHelper.retryPeriodicallyWithLatch(it) {
|
||||||
alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID) as? OutgoingSasVerificationTransaction
|
alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction
|
||||||
Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}")
|
Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}")
|
||||||
alicePovTx?.state == VerificationTxState.ShortCodeReady
|
alicePovTx?.state == VerificationTxState.ShortCodeReady
|
||||||
}
|
}
|
||||||
|
@ -340,7 +427,7 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
|
||||||
// wait for alice to get the ready
|
// wait for alice to get the ready
|
||||||
testHelper.waitWithLatch {
|
testHelper.waitWithLatch {
|
||||||
testHelper.retryPeriodicallyWithLatch(it) {
|
testHelper.retryPeriodicallyWithLatch(it) {
|
||||||
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
|
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID!!) as? IncomingSasVerificationTransaction
|
||||||
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
|
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
|
||||||
if (bobPovTx?.state == VerificationTxState.OnStarted) {
|
if (bobPovTx?.state == VerificationTxState.OnStarted) {
|
||||||
bobPovTx?.performAccept()
|
bobPovTx?.performAccept()
|
||||||
|
@ -392,4 +479,50 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
|
||||||
|
|
||||||
return CryptoTestData(roomId, sessions)
|
return CryptoTestData(roomId, sessions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ensureCanDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, messagesText: List<String>) {
|
||||||
|
sentEventIds.forEachIndexed { index, sentEventId ->
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root
|
||||||
|
testHelper.runBlockingTest {
|
||||||
|
try {
|
||||||
|
session.cryptoService().decryptEvent(event, "").let { result ->
|
||||||
|
event.mxDecryptionResult = OlmDecryptionResult(
|
||||||
|
payload = result.clearEvent,
|
||||||
|
senderKey = result.senderCurve25519Key,
|
||||||
|
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||||
|
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error: MXCryptoError) {
|
||||||
|
// nop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}")
|
||||||
|
event.getClearType() == EventType.MESSAGE &&
|
||||||
|
messagesText[index] == event.getClearContent()?.toModel<MessageContent>()?.body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ensureCannotDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType? = null) {
|
||||||
|
sentEventIds.forEach { sentEventId ->
|
||||||
|
val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root
|
||||||
|
testHelper.runBlockingTest {
|
||||||
|
try {
|
||||||
|
session.cryptoService().decryptEvent(event, "")
|
||||||
|
fail("Should not be able to decrypt event")
|
||||||
|
} catch (error: MXCryptoError) {
|
||||||
|
val errorType = (error as? MXCryptoError.Base)?.errorType
|
||||||
|
if (expectedError == null) {
|
||||||
|
assertNotNull(errorType)
|
||||||
|
} else {
|
||||||
|
assertEquals("Unexpected reason", expectedError, errorType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,13 +30,21 @@ import org.junit.runners.MethodSorters
|
||||||
import org.matrix.android.sdk.InstrumentedTest
|
import org.matrix.android.sdk.InstrumentedTest
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.RequestResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.getRoom
|
import org.matrix.android.sdk.api.session.getRoom
|
||||||
import org.matrix.android.sdk.api.session.getRoomSummary
|
import org.matrix.android.sdk.api.session.getRoomSummary
|
||||||
|
@ -52,15 +60,13 @@ import org.matrix.android.sdk.common.CryptoTestHelper
|
||||||
import org.matrix.android.sdk.common.SessionTestParams
|
import org.matrix.android.sdk.common.SessionTestParams
|
||||||
import org.matrix.android.sdk.common.TestConstants
|
import org.matrix.android.sdk.common.TestConstants
|
||||||
import org.matrix.android.sdk.common.TestMatrixCallback
|
import org.matrix.android.sdk.common.TestMatrixCallback
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
@RunWith(JUnit4::class)
|
@RunWith(JUnit4::class)
|
||||||
@FixMethodOrder(MethodSorters.JVM)
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
@LargeTest
|
@LargeTest
|
||||||
class E2eeSanityTests : InstrumentedTest {
|
class E2eeSanityTests : InstrumentedTest {
|
||||||
|
|
||||||
private val testHelper = CommonTestHelper(context())
|
|
||||||
private val cryptoTestHelper = CryptoTestHelper(testHelper)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple test that create an e2ee room.
|
* Simple test that create an e2ee room.
|
||||||
* Some new members are added, and a message is sent.
|
* Some new members are added, and a message is sent.
|
||||||
|
@ -72,16 +78,24 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testSendingE2EEMessages() {
|
fun testSendingE2EEMessages() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||||
val aliceSession = cryptoTestData.firstSession
|
val aliceSession = cryptoTestData.firstSession
|
||||||
val e2eRoomID = cryptoTestData.roomId
|
val e2eRoomID = cryptoTestData.roomId
|
||||||
|
|
||||||
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
||||||
|
// we want to disable key gossiping to just check initial sending of keys
|
||||||
|
aliceSession.cryptoService().enableKeyGossiping(false)
|
||||||
|
cryptoTestData.secondSession?.cryptoService()?.enableKeyGossiping(false)
|
||||||
|
|
||||||
// add some more users and invite them
|
// add some more users and invite them
|
||||||
val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu")
|
val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu")
|
||||||
.map {
|
.map {
|
||||||
testHelper.createAccount(it, SessionTestParams(true))
|
testHelper.createAccount(it, SessionTestParams(true)).also {
|
||||||
|
it.cryptoService().enableKeyGossiping(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.v("#E2E TEST", "All accounts created")
|
Log.v("#E2E TEST", "All accounts created")
|
||||||
|
@ -95,18 +109,18 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
|
|
||||||
// All user should accept invite
|
// All user should accept invite
|
||||||
otherAccounts.forEach { otherSession ->
|
otherAccounts.forEach { otherSession ->
|
||||||
waitForAndAcceptInviteInRoom(otherSession, e2eRoomID)
|
waitForAndAcceptInviteInRoom(testHelper, otherSession, e2eRoomID)
|
||||||
Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
|
Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// check that alice see them as joined (not really necessary?)
|
// check that alice see them as joined (not really necessary?)
|
||||||
ensureMembersHaveJoined(aliceSession, otherAccounts, e2eRoomID)
|
ensureMembersHaveJoined(testHelper, aliceSession, otherAccounts, e2eRoomID)
|
||||||
|
|
||||||
Log.v("#E2E TEST", "All users have joined the room")
|
Log.v("#E2E TEST", "All users have joined the room")
|
||||||
Log.v("#E2E TEST", "Alice is sending the message")
|
Log.v("#E2E TEST", "Alice is sending the message")
|
||||||
|
|
||||||
val text = "This is my message"
|
val text = "This is my message"
|
||||||
val sentEventId: String? = sendMessageInRoom(aliceRoomPOV, text)
|
val sentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, text)
|
||||||
// val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first()
|
// val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first()
|
||||||
Assert.assertTrue("Message should be sent", sentEventId != null)
|
Assert.assertTrue("Message should be sent", sentEventId != null)
|
||||||
|
|
||||||
|
@ -114,10 +128,10 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
otherAccounts.forEach { otherSession ->
|
otherAccounts.forEach { otherSession ->
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
|
val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
|
||||||
timelineEvent != null &&
|
timeLineEvent != null &&
|
||||||
timelineEvent.isEncrypted() &&
|
timeLineEvent.isEncrypted() &&
|
||||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,10 +150,10 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
newAccount.forEach {
|
newAccount.forEach {
|
||||||
waitForAndAcceptInviteInRoom(it, e2eRoomID)
|
waitForAndAcceptInviteInRoom(testHelper, it, e2eRoomID)
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureMembersHaveJoined(aliceSession, newAccount, e2eRoomID)
|
ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID)
|
||||||
|
|
||||||
// wait a bit
|
// wait a bit
|
||||||
testHelper.runBlockingTest {
|
testHelper.runBlockingTest {
|
||||||
|
@ -164,7 +178,7 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
Log.v("#E2E TEST", "Alice sends a new message")
|
Log.v("#E2E TEST", "Alice sends a new message")
|
||||||
|
|
||||||
val secondMessage = "2 This is my message"
|
val secondMessage = "2 This is my message"
|
||||||
val secondSentEventId: String? = sendMessageInRoom(aliceRoomPOV, secondMessage)
|
val secondSentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, secondMessage)
|
||||||
|
|
||||||
// new members should be able to decrypt it
|
// new members should be able to decrypt it
|
||||||
newAccount.forEach { otherSession ->
|
newAccount.forEach { otherSession ->
|
||||||
|
@ -188,6 +202,14 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
cryptoTestData.cleanUp(testHelper)
|
cryptoTestData.cleanUp(testHelper)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testKeyGossipingIsEnabledByDefault() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val session = testHelper.createAccount("alice", SessionTestParams(true))
|
||||||
|
Assert.assertTrue("Key gossiping should be enabled by default", session.cryptoService().isKeyGossipingEnabled())
|
||||||
|
testHelper.signOutAndClose(session)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Quick test for basic key backup
|
* Quick test for basic key backup
|
||||||
* 1. Create e2e between Alice and Bob
|
* 1. Create e2e between Alice and Bob
|
||||||
|
@ -204,6 +226,9 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testBasicBackupImport() {
|
fun testBasicBackupImport() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||||
val aliceSession = cryptoTestData.firstSession
|
val aliceSession = cryptoTestData.firstSession
|
||||||
val bobSession = cryptoTestData.secondSession!!
|
val bobSession = cryptoTestData.secondSession!!
|
||||||
|
@ -227,16 +252,16 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
val sentEventIds = mutableListOf<String>()
|
val sentEventIds = mutableListOf<String>()
|
||||||
val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning")
|
val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning")
|
||||||
messagesText.forEach { text ->
|
messagesText.forEach { text ->
|
||||||
val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
|
val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also {
|
||||||
sentEventIds.add(it)
|
sentEventIds.add(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||||
timelineEvent != null &&
|
timeLineEvent != null &&
|
||||||
timelineEvent.isEncrypted() &&
|
timeLineEvent.isEncrypted() &&
|
||||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// we want more so let's discard the session
|
// we want more so let's discard the session
|
||||||
|
@ -289,22 +314,23 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// after initial sync events are not decrypted, so we have to try manually
|
// after initial sync events are not decrypted, so we have to try manually
|
||||||
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
|
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
|
||||||
|
|
||||||
// Let's now import keys from backup
|
// Let's now import keys from backup
|
||||||
|
|
||||||
newBobSession.cryptoService().keysBackupService().let { keysBackupService ->
|
newBobSession.cryptoService().keysBackupService().let { kbs ->
|
||||||
val keyVersionResult = testHelper.doSync<KeysVersionResult?> {
|
val keyVersionResult = testHelper.doSync<KeysVersionResult?> {
|
||||||
keysBackupService.getVersion(version.version, it)
|
kbs.getVersion(version.version, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val importedResult = testHelper.doSync<ImportRoomKeysResult> {
|
val importedResult = testHelper.doSync<ImportRoomKeysResult> {
|
||||||
keysBackupService.restoreKeyBackupWithPassword(
|
kbs.restoreKeyBackupWithPassword(
|
||||||
keyVersionResult!!,
|
keyVersionResult!!,
|
||||||
keyBackupPassword,
|
keyBackupPassword,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null, it
|
null,
|
||||||
|
it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,7 +338,7 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure bob can now decrypt
|
// ensure bob can now decrypt
|
||||||
ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
||||||
|
|
||||||
testHelper.signOutAndClose(newBobSession)
|
testHelper.signOutAndClose(newBobSession)
|
||||||
}
|
}
|
||||||
|
@ -323,6 +349,9 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testSimpleGossip() {
|
fun testSimpleGossip() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||||
val aliceSession = cryptoTestData.firstSession
|
val aliceSession = cryptoTestData.firstSession
|
||||||
val bobSession = cryptoTestData.secondSession!!
|
val bobSession = cryptoTestData.secondSession!!
|
||||||
|
@ -330,30 +359,28 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
|
|
||||||
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
||||||
|
|
||||||
cryptoTestHelper.initializeCrossSigning(bobSession)
|
|
||||||
|
|
||||||
// let's send a few message to bob
|
// let's send a few message to bob
|
||||||
val sentEventIds = mutableListOf<String>()
|
val sentEventIds = mutableListOf<String>()
|
||||||
val messagesText = listOf("1. Hello", "2. Bob")
|
val messagesText = listOf("1. Hello", "2. Bob")
|
||||||
|
|
||||||
Log.v("#E2E TEST", "Alice sends some messages")
|
Log.v("#E2E TEST", "Alice sends some messages")
|
||||||
messagesText.forEach { text ->
|
messagesText.forEach { text ->
|
||||||
val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
|
val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also {
|
||||||
sentEventIds.add(it)
|
sentEventIds.add(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||||
timelineEvent != null &&
|
timeLineEvent != null &&
|
||||||
timelineEvent.isEncrypted() &&
|
timeLineEvent.isEncrypted() &&
|
||||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure bob can decrypt
|
// Ensure bob can decrypt
|
||||||
ensureIsDecrypted(sentEventIds, bobSession, e2eRoomID)
|
ensureIsDecrypted(testHelper, sentEventIds, bobSession, e2eRoomID)
|
||||||
|
|
||||||
// Let's now add a new bob session
|
// Let's now add a new bob session
|
||||||
// Create a new session for bob
|
// Create a new session for bob
|
||||||
|
@ -363,7 +390,11 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
// check that new bob can't currently decrypt
|
// check that new bob can't currently decrypt
|
||||||
Log.v("#E2E TEST", "check that new bob can't currently decrypt")
|
Log.v("#E2E TEST", "check that new bob can't currently decrypt")
|
||||||
|
|
||||||
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
|
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
|
||||||
|
// newBobSession.cryptoService().getOutgoingRoomKeyRequests()
|
||||||
|
// .firstOrNull {
|
||||||
|
// it.sessionId ==
|
||||||
|
// }
|
||||||
|
|
||||||
// Try to request
|
// Try to request
|
||||||
sentEventIds.forEach { sentEventId ->
|
sentEventIds.forEach { sentEventId ->
|
||||||
|
@ -372,12 +403,34 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait a bit
|
// wait a bit
|
||||||
testHelper.runBlockingTest {
|
// we need to wait a couple of syncs to let sharing occurs
|
||||||
delay(10_000)
|
// testHelper.waitFewSyncs(newBobSession, 6)
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that new bob still can't decrypt (keys must have been withheld)
|
// Ensure that new bob still can't decrypt (keys must have been withheld)
|
||||||
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.KEYS_WITHHELD)
|
sentEventIds.forEach { sentEventId ->
|
||||||
|
val megolmSessionId = newBobSession.getRoom(e2eRoomID)!!
|
||||||
|
.getTimelineEvent(sentEventId)!!
|
||||||
|
.root.content.toModel<EncryptedEventContent>()!!.sessionId
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests()
|
||||||
|
.first {
|
||||||
|
it.sessionId == megolmSessionId &&
|
||||||
|
it.roomId == e2eRoomID
|
||||||
|
}
|
||||||
|
.results.also {
|
||||||
|
Log.w("##TEST", "result list is $it")
|
||||||
|
}
|
||||||
|
.firstOrNull { it.userId == aliceSession.myUserId }
|
||||||
|
?.result
|
||||||
|
aliceReply != null &&
|
||||||
|
aliceReply is RequestResult.Failure &&
|
||||||
|
WithHeldCode.UNAUTHORISED == aliceReply.code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
|
||||||
|
|
||||||
// Now mark new bob session as verified
|
// Now mark new bob session as verified
|
||||||
|
|
||||||
|
@ -390,12 +443,7 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
|
newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait a bit
|
cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
||||||
testHelper.runBlockingTest {
|
|
||||||
delay(10_000)
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
|
||||||
|
|
||||||
cryptoTestData.cleanUp(testHelper)
|
cryptoTestData.cleanUp(testHelper)
|
||||||
testHelper.signOutAndClose(newBobSession)
|
testHelper.signOutAndClose(newBobSession)
|
||||||
|
@ -406,6 +454,9 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testForwardBetterKey() {
|
fun testForwardBetterKey() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||||
val aliceSession = cryptoTestData.firstSession
|
val aliceSession = cryptoTestData.firstSession
|
||||||
val bobSessionWithBetterKey = cryptoTestData.secondSession!!
|
val bobSessionWithBetterKey = cryptoTestData.secondSession!!
|
||||||
|
@ -413,35 +464,33 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
|
|
||||||
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
|
||||||
|
|
||||||
cryptoTestHelper.initializeCrossSigning(bobSessionWithBetterKey)
|
|
||||||
|
|
||||||
// let's send a few message to bob
|
// let's send a few message to bob
|
||||||
var firstEventId: String
|
var firstEventId: String
|
||||||
val firstMessage = "1. Hello"
|
val firstMessage = "1. Hello"
|
||||||
|
|
||||||
Log.v("#E2E TEST", "Alice sends some messages")
|
Log.v("#E2E TEST", "Alice sends some messages")
|
||||||
firstMessage.let { text ->
|
firstMessage.let { text ->
|
||||||
firstEventId = sendMessageInRoom(aliceRoomPOV, text)!!
|
firstEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
|
||||||
|
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val timelineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
|
val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
|
||||||
timelineEvent != null &&
|
timeLineEvent != null &&
|
||||||
timelineEvent.isEncrypted() &&
|
timeLineEvent.isEncrypted() &&
|
||||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure bob can decrypt
|
// Ensure bob can decrypt
|
||||||
ensureIsDecrypted(listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID)
|
ensureIsDecrypted(testHelper, listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID)
|
||||||
|
|
||||||
// Let's add a new unverified session from bob
|
// Let's add a new unverified session from bob
|
||||||
val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true))
|
val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true))
|
||||||
|
|
||||||
// check that new bob can't currently decrypt
|
// check that new bob can't currently decrypt
|
||||||
Log.v("#E2E TEST", "check that new bob can't currently decrypt")
|
Log.v("#E2E TEST", "check that new bob can't currently decrypt")
|
||||||
ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
|
cryptoTestHelper.ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, null)
|
||||||
|
|
||||||
// Now let alice send a new message. this time the new bob session will be able to decrypt
|
// Now let alice send a new message. this time the new bob session will be able to decrypt
|
||||||
var secondEventId: String
|
var secondEventId: String
|
||||||
|
@ -449,14 +498,14 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
|
|
||||||
Log.v("#E2E TEST", "Alice sends some messages")
|
Log.v("#E2E TEST", "Alice sends some messages")
|
||||||
secondMessage.let { text ->
|
secondMessage.let { text ->
|
||||||
secondEventId = sendMessageInRoom(aliceRoomPOV, text)!!
|
secondEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
|
||||||
|
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
|
val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
|
||||||
timelineEvent != null &&
|
timeLineEvent != null &&
|
||||||
timelineEvent.isEncrypted() &&
|
timeLineEvent.isEncrypted() &&
|
||||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -475,9 +524,7 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
try {
|
try {
|
||||||
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
||||||
fail("Should not be able to decrypt event")
|
fail("Should not be able to decrypt event")
|
||||||
} catch (error: MXCryptoError) {
|
} catch (_: MXCryptoError) {
|
||||||
val errorType = (error as? MXCryptoError.Base)?.errorType
|
|
||||||
assertEquals(MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, errorType)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -499,41 +546,45 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
.markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!)
|
.markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!)
|
||||||
|
|
||||||
// now let new session request
|
// now let new session request
|
||||||
newBobSession.cryptoService().requestRoomKeyForEvent(firstEventNewBobPov.root)
|
newBobSession.cryptoService().reRequestRoomKeyForEvent(firstEventNewBobPov.root)
|
||||||
|
|
||||||
// wait a bit
|
// We need to wait for the key request to be sent out and then a reply to be received
|
||||||
testHelper.runBlockingTest {
|
|
||||||
delay(10_000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// old session should have shared the key at earliest known index now
|
// old session should have shared the key at earliest known index now
|
||||||
// we should be able to decrypt both
|
// we should be able to decrypt both
|
||||||
testHelper.runBlockingTest {
|
testHelper.waitWithLatch {
|
||||||
try {
|
testHelper.retryPeriodicallyWithLatch(it) {
|
||||||
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
val canDecryptFirst = try {
|
||||||
} catch (error: MXCryptoError) {
|
testHelper.runBlockingTest {
|
||||||
fail("Should be able to decrypt first event now $error")
|
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
||||||
}
|
}
|
||||||
}
|
true
|
||||||
testHelper.runBlockingTest {
|
} catch (error: MXCryptoError) {
|
||||||
try {
|
false
|
||||||
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
|
}
|
||||||
} catch (error: MXCryptoError) {
|
val canDecryptSecond = try {
|
||||||
fail("Should be able to decrypt event $error")
|
testHelper.runBlockingTest {
|
||||||
|
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} catch (error: MXCryptoError) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
canDecryptFirst && canDecryptSecond
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cryptoTestData.cleanUp(testHelper)
|
testHelper.signOutAndClose(aliceSession)
|
||||||
|
testHelper.signOutAndClose(bobSessionWithBetterKey)
|
||||||
testHelper.signOutAndClose(newBobSession)
|
testHelper.signOutAndClose(newBobSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? {
|
private fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? {
|
||||||
aliceRoomPOV.sendService().sendTextMessage(text)
|
aliceRoomPOV.sendService().sendTextMessage(text)
|
||||||
var sentEventId: String? = null
|
var sentEventId: String? = null
|
||||||
testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
|
testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
|
||||||
val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60))
|
val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60))
|
||||||
timeline.start()
|
timeline.start()
|
||||||
|
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val decryptedMsg = timeline.getSnapshot()
|
val decryptedMsg = timeline.getSnapshot()
|
||||||
.filter { it.root.getClearType() == EventType.MESSAGE }
|
.filter { it.root.getClearType() == EventType.MESSAGE }
|
||||||
|
@ -552,7 +603,157 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
return sentEventId
|
return sentEventId
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {
|
/**
|
||||||
|
* Test that if a better key is forwared (lower index, it is then used)
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun testSelfInteractiveVerificationAndGossip() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
|
val aliceSession = testHelper.createAccount("alice", SessionTestParams(true))
|
||||||
|
cryptoTestHelper.bootstrapSecurity(aliceSession)
|
||||||
|
|
||||||
|
// now let's create a new login from alice
|
||||||
|
|
||||||
|
val aliceNewSession = testHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
|
||||||
|
|
||||||
|
val oldCompleteLatch = CountDownLatch(1)
|
||||||
|
lateinit var oldCode: String
|
||||||
|
aliceSession.cryptoService().verificationService().addListener(object : VerificationService.Listener {
|
||||||
|
|
||||||
|
override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
|
||||||
|
val readyInfo = pr.readyInfo
|
||||||
|
if (readyInfo != null) {
|
||||||
|
aliceSession.cryptoService().verificationService().beginKeyVerification(
|
||||||
|
VerificationMethod.SAS,
|
||||||
|
aliceSession.myUserId,
|
||||||
|
readyInfo.fromDevice,
|
||||||
|
readyInfo.transactionId
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||||
|
Log.d("##TEST", "exitsingPov: $tx")
|
||||||
|
val sasTx = tx as OutgoingSasVerificationTransaction
|
||||||
|
when (sasTx.uxState) {
|
||||||
|
OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> {
|
||||||
|
// for the test we just accept?
|
||||||
|
oldCode = sasTx.getDecimalCodeRepresentation()
|
||||||
|
sasTx.userHasVerifiedShortCode()
|
||||||
|
}
|
||||||
|
OutgoingSasVerificationTransaction.UxState.VERIFIED -> {
|
||||||
|
// we can release this latch?
|
||||||
|
oldCompleteLatch.countDown()
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
val newCompleteLatch = CountDownLatch(1)
|
||||||
|
lateinit var newCode: String
|
||||||
|
aliceNewSession.cryptoService().verificationService().addListener(object : VerificationService.Listener {
|
||||||
|
|
||||||
|
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
|
||||||
|
// let's ready
|
||||||
|
aliceNewSession.cryptoService().verificationService().readyPendingVerification(
|
||||||
|
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
|
||||||
|
aliceSession.myUserId,
|
||||||
|
pr.transactionId!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var matchOnce = true
|
||||||
|
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||||
|
Log.d("##TEST", "newPov: $tx")
|
||||||
|
|
||||||
|
val sasTx = tx as IncomingSasVerificationTransaction
|
||||||
|
when (sasTx.uxState) {
|
||||||
|
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
|
||||||
|
// no need to accept as there was a request first it will auto accept
|
||||||
|
}
|
||||||
|
IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
|
||||||
|
if (matchOnce) {
|
||||||
|
sasTx.userHasVerifiedShortCode()
|
||||||
|
newCode = sasTx.getDecimalCodeRepresentation()
|
||||||
|
matchOnce = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IncomingSasVerificationTransaction.UxState.VERIFIED -> {
|
||||||
|
newCompleteLatch.countDown()
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// initiate self verification
|
||||||
|
aliceSession.cryptoService().verificationService().requestKeyVerification(
|
||||||
|
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
|
||||||
|
aliceNewSession.myUserId,
|
||||||
|
listOf(aliceNewSession.sessionParams.deviceId!!)
|
||||||
|
)
|
||||||
|
testHelper.await(oldCompleteLatch)
|
||||||
|
testHelper.await(newCompleteLatch)
|
||||||
|
assertEquals("Decimal code should have matched", oldCode, newCode)
|
||||||
|
|
||||||
|
// Assert that devices are verified
|
||||||
|
val newDeviceFromOldPov: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId)
|
||||||
|
val oldDeviceFromNewPov: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId)
|
||||||
|
|
||||||
|
Assert.assertTrue("new device should be verified from old point of view", newDeviceFromOldPov!!.isVerified)
|
||||||
|
Assert.assertTrue("old device should be verified from new point of view", oldDeviceFromNewPov!!.isVerified)
|
||||||
|
|
||||||
|
// wait for secret gossiping to happen
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"MSK Private parts should be the same",
|
||||||
|
aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master,
|
||||||
|
aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"USK Private parts should be the same",
|
||||||
|
aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user,
|
||||||
|
aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"SSK Private parts should be the same",
|
||||||
|
aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned,
|
||||||
|
aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned
|
||||||
|
)
|
||||||
|
|
||||||
|
// Let's check that we have the megolm backup key
|
||||||
|
assertEquals(
|
||||||
|
"Megolm key should be the same",
|
||||||
|
aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey,
|
||||||
|
aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"Megolm version should be the same",
|
||||||
|
aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version,
|
||||||
|
aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version
|
||||||
|
)
|
||||||
|
|
||||||
|
testHelper.signOutAndClose(aliceSession)
|
||||||
|
testHelper.signOutAndClose(aliceNewSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
otherAccounts.map {
|
otherAccounts.map {
|
||||||
|
@ -564,7 +765,7 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) {
|
private fun waitForAndAcceptInviteInRoom(testHelper: CommonTestHelper, otherSession: Session, e2eRoomID: String) {
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val roomSummary = otherSession.getRoomSummary(e2eRoomID)
|
val roomSummary = otherSession.getRoomSummary(e2eRoomID)
|
||||||
|
@ -576,7 +777,8 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
testHelper.runBlockingTest(60_000) {
|
// not sure why it's taking so long :/
|
||||||
|
testHelper.runBlockingTest(90_000) {
|
||||||
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
|
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
|
||||||
try {
|
try {
|
||||||
otherSession.roomService().joinRoom(e2eRoomID)
|
otherSession.roomService().joinRoom(e2eRoomID)
|
||||||
|
@ -594,59 +796,14 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureCanDecrypt(sentEventIds: MutableList<String>, session: Session, e2eRoomID: String, messagesText: List<String>) {
|
private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List<String>, session: Session, e2eRoomID: String) {
|
||||||
sentEventIds.forEachIndexed { index, sentEventId ->
|
|
||||||
testHelper.waitWithLatch { latch ->
|
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
|
||||||
val event = session.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
|
|
||||||
testHelper.runBlockingTest {
|
|
||||||
try {
|
|
||||||
session.cryptoService().decryptEvent(event, "").let { result ->
|
|
||||||
event.mxDecryptionResult = OlmDecryptionResult(
|
|
||||||
payload = result.clearEvent,
|
|
||||||
senderKey = result.senderCurve25519Key,
|
|
||||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
|
||||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (error: MXCryptoError) {
|
|
||||||
// nop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event.getClearType() == EventType.MESSAGE &&
|
|
||||||
messagesText[index] == event.getClearContent()?.toModel<MessageContent>()?.body
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ensureIsDecrypted(sentEventIds: List<String>, session: Session, e2eRoomID: String) {
|
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
sentEventIds.forEach { sentEventId ->
|
sentEventIds.forEach { sentEventId ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val timelineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
val timeLineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||||
timelineEvent != null &&
|
timeLineEvent != null &&
|
||||||
timelineEvent.isEncrypted() &&
|
timeLineEvent.isEncrypted() &&
|
||||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ensureCannotDecrypt(sentEventIds: List<String>, newBobSession: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType?) {
|
|
||||||
sentEventIds.forEach { sentEventId ->
|
|
||||||
val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
|
|
||||||
testHelper.runBlockingTest {
|
|
||||||
try {
|
|
||||||
newBobSession.cryptoService().decryptEvent(event, "")
|
|
||||||
fail("Should not be able to decrypt event")
|
|
||||||
} catch (error: MXCryptoError) {
|
|
||||||
val errorType = (error as? MXCryptoError.Base)?.errorType
|
|
||||||
if (expectedError == null) {
|
|
||||||
Assert.assertNotNull(errorType)
|
|
||||||
} else {
|
|
||||||
assertEquals(expectedError, errorType, "Message expected to be UISI")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,6 @@ import org.junit.runners.MethodSorters
|
||||||
import org.matrix.android.sdk.InstrumentedTest
|
import org.matrix.android.sdk.InstrumentedTest
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.getRoom
|
import org.matrix.android.sdk.api.session.getRoom
|
||||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||||
|
@ -51,10 +50,7 @@ class PreShareKeysTest : InstrumentedTest {
|
||||||
// clear any outbound session
|
// clear any outbound session
|
||||||
aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
|
aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
|
||||||
|
|
||||||
val preShareCount = bobSession.cryptoService().getGossipingEvents().count {
|
val preShareCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
|
||||||
it.senderId == aliceSession.myUserId &&
|
|
||||||
it.getClearType() == EventType.ROOM_KEY
|
|
||||||
}
|
|
||||||
|
|
||||||
assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount)
|
assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount)
|
||||||
Log.d("#Test", "Room Key Received from alice $preShareCount")
|
Log.d("#Test", "Room Key Received from alice $preShareCount")
|
||||||
|
@ -66,23 +62,23 @@ class PreShareKeysTest : InstrumentedTest {
|
||||||
|
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val newGossipCount = bobSession.cryptoService().getGossipingEvents().count {
|
val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
|
||||||
it.senderId == aliceSession.myUserId &&
|
newKeysCount > preShareCount
|
||||||
it.getClearType() == EventType.ROOM_KEY
|
|
||||||
}
|
|
||||||
newGossipCount > preShareCount
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val latest = bobSession.cryptoService().getGossipingEvents().lastOrNull {
|
val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
|
||||||
it.senderId == aliceSession.myUserId &&
|
val aliceOutboundSessionInRoom = aliceCryptoStore.getCurrentOutboundGroupSessionForRoom(e2eRoomID)!!.outboundGroupSession.sessionIdentifier()
|
||||||
it.getClearType() == EventType.ROOM_KEY
|
|
||||||
}
|
|
||||||
|
|
||||||
val content = latest?.getClearContent().toModel<RoomKeyContent>()
|
val bobCryptoStore = (bobSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
|
||||||
assertNotNull("Bob should have received and decrypted a room key event from alice", content)
|
val aliceDeviceBobPov = bobCryptoStore.getUserDevice(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!)!!
|
||||||
assertEquals("Wrong room", e2eRoomID, content!!.roomId)
|
val bobInboundForAlice = bobCryptoStore.getInboundGroupSession(aliceOutboundSessionInRoom, aliceDeviceBobPov.identityKey()!!)
|
||||||
val megolmSessionId = content.sessionId!!
|
assertNotNull("Bob should have received and decrypted a room key event from alice", bobInboundForAlice)
|
||||||
|
assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId)
|
||||||
|
|
||||||
|
val megolmSessionId = bobInboundForAlice.olmInboundGroupSession!!.sessionIdentifier()
|
||||||
|
|
||||||
|
assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId)
|
||||||
|
|
||||||
val sharedIndex = aliceSession.cryptoService().getSharedWithInfo(e2eRoomID, megolmSessionId)
|
val sharedIndex = aliceSession.cryptoService().getSharedWithInfo(e2eRoomID, megolmSessionId)
|
||||||
.getObject(bobSession.myUserId, bobSession.sessionParams.deviceId)
|
.getObject(bobSession.myUserId, bobSession.sessionParams.deviceId)
|
||||||
|
|
|
@ -19,59 +19,45 @@ package org.matrix.android.sdk.internal.crypto.gossiping
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
import junit.framework.TestCase.assertEquals
|
|
||||||
import junit.framework.TestCase.assertNotNull
|
import junit.framework.TestCase.assertNotNull
|
||||||
import junit.framework.TestCase.assertTrue
|
import junit.framework.TestCase.assertTrue
|
||||||
import junit.framework.TestCase.fail
|
import junit.framework.TestCase.fail
|
||||||
|
import org.amshove.kluent.internal.assertEquals
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
import org.junit.FixMethodOrder
|
import org.junit.FixMethodOrder
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.MethodSorters
|
import org.junit.runners.MethodSorters
|
||||||
import org.matrix.android.sdk.InstrumentedTest
|
import org.matrix.android.sdk.InstrumentedTest
|
||||||
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
|
||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
import org.matrix.android.sdk.api.session.crypto.RequestResult
|
||||||
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
|
||||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.getRoom
|
import org.matrix.android.sdk.api.session.getRoom
|
||||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
||||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||||
import org.matrix.android.sdk.common.CommonTestHelper
|
import org.matrix.android.sdk.common.CommonTestHelper
|
||||||
|
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||||
import org.matrix.android.sdk.common.SessionTestParams
|
import org.matrix.android.sdk.common.SessionTestParams
|
||||||
import org.matrix.android.sdk.common.TestConstants
|
import org.matrix.android.sdk.common.TestConstants
|
||||||
import kotlin.coroutines.Continuation
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@FixMethodOrder(MethodSorters.JVM)
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
@LargeTest
|
@LargeTest
|
||||||
class KeyShareTests : InstrumentedTest {
|
class KeyShareTests : InstrumentedTest {
|
||||||
|
|
||||||
private val commonTestHelper = CommonTestHelper(context())
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun test_DoNotSelfShareIfNotTrusted() {
|
fun test_DoNotSelfShareIfNotTrusted() {
|
||||||
|
val commonTestHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||||
|
|
||||||
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||||
|
Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}")
|
||||||
|
|
||||||
// Create an encrypted room and add a message
|
// Create an encrypted room and add a message
|
||||||
val roomId = commonTestHelper.runBlockingTest {
|
val roomId = commonTestHelper.runBlockingTest {
|
||||||
|
@ -86,11 +72,18 @@ class KeyShareTests : InstrumentedTest {
|
||||||
assertNotNull(room)
|
assertNotNull(room)
|
||||||
Thread.sleep(4_000)
|
Thread.sleep(4_000)
|
||||||
assertTrue(room?.roomCryptoService()?.isEncrypted() == true)
|
assertTrue(room?.roomCryptoService()?.isEncrypted() == true)
|
||||||
val sentEventId = commonTestHelper.sendTextMessage(room!!, "My Message", 1).first().eventId
|
|
||||||
|
|
||||||
// Open a new sessionx
|
val sentEvent = commonTestHelper.sendTextMessage(room!!, "My Message", 1).first()
|
||||||
|
val sentEventId = sentEvent.eventId
|
||||||
|
val sentEventText = sentEvent.getLastMessageContent()?.body
|
||||||
|
|
||||||
val aliceSession2 = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
|
// Open a new session
|
||||||
|
val aliceSession2 = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(false))
|
||||||
|
// block key requesting for now as decrypt will send requests (room summary is trying to decrypt)
|
||||||
|
aliceSession2.cryptoService().enableKeyGossiping(false)
|
||||||
|
commonTestHelper.syncSession(aliceSession2)
|
||||||
|
|
||||||
|
Log.v("TEST", "=======> AliceSession 2 is ${aliceSession2.sessionParams.deviceId}")
|
||||||
|
|
||||||
val roomSecondSessionPOV = aliceSession2.getRoom(roomId)
|
val roomSecondSessionPOV = aliceSession2.getRoom(roomId)
|
||||||
|
|
||||||
|
@ -107,7 +100,10 @@ class KeyShareTests : InstrumentedTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
||||||
|
assertEquals("There should be no request as it's disabled", 0, outgoingRequestsBefore.size)
|
||||||
|
|
||||||
// Try to request
|
// Try to request
|
||||||
|
aliceSession2.cryptoService().enableKeyGossiping(true)
|
||||||
aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root)
|
aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root)
|
||||||
|
|
||||||
val eventMegolmSessionId = receivedEvent.root.content.toModel<EncryptedEventContent>()?.sessionId
|
val eventMegolmSessionId = receivedEvent.root.content.toModel<EncryptedEventContent>()?.sessionId
|
||||||
|
@ -117,10 +113,6 @@ class KeyShareTests : InstrumentedTest {
|
||||||
commonTestHelper.waitWithLatch { latch ->
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
||||||
.filter { req ->
|
|
||||||
// filter out request that was known before
|
|
||||||
!outgoingRequestsBefore.any { req.requestId == it.requestId }
|
|
||||||
}
|
|
||||||
.let {
|
.let {
|
||||||
val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
|
val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
|
||||||
outGoingRequestId = outgoing?.requestId
|
outGoingRequestId = outgoing?.requestId
|
||||||
|
@ -141,20 +133,34 @@ class KeyShareTests : InstrumentedTest {
|
||||||
commonTestHelper.waitWithLatch { latch ->
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
// DEBUG LOGS
|
// DEBUG LOGS
|
||||||
aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
|
// aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
|
||||||
Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
|
// Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
|
||||||
|
// Log.v("TEST", "=========================")
|
||||||
|
// it.forEach { keyRequest ->
|
||||||
|
// Log.v("TEST", "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
|
||||||
|
// }
|
||||||
|
// Log.v("TEST", "=========================")
|
||||||
|
// }
|
||||||
|
|
||||||
|
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
|
||||||
|
incoming != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
// DEBUG LOGS
|
||||||
|
aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest ->
|
||||||
Log.v("TEST", "=========================")
|
Log.v("TEST", "=========================")
|
||||||
it.forEach { keyRequest ->
|
Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
|
||||||
Log.v(
|
Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
|
||||||
"TEST",
|
|
||||||
"[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId} is ${keyRequest.state}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Log.v("TEST", "=========================")
|
Log.v("TEST", "=========================")
|
||||||
}
|
}
|
||||||
|
|
||||||
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
|
val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
|
||||||
incoming?.state == GossipingRequestState.REJECTED
|
val reply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
|
||||||
|
val resultCode = (reply?.result as? RequestResult.Failure)?.code
|
||||||
|
resultCode == WithHeldCode.UNVERIFIED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,254 +181,301 @@ class KeyShareTests : InstrumentedTest {
|
||||||
// Re request
|
// Re request
|
||||||
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
|
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
|
||||||
|
|
||||||
commonTestHelper.waitWithLatch { latch ->
|
cryptoTestHelper.ensureCanDecrypt(listOf(receivedEvent.eventId), aliceSession2, roomId, listOf(sentEventText ?: ""))
|
||||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
|
||||||
aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
|
|
||||||
Log.v("TEST", "Incoming request Session 1")
|
|
||||||
Log.v("TEST", "=========================")
|
|
||||||
it.forEach {
|
|
||||||
Log.v("TEST", "requestId ${it.requestId}, for sessionId ${it.requestBody?.sessionId} is ${it.state}")
|
|
||||||
}
|
|
||||||
Log.v("TEST", "=========================")
|
|
||||||
|
|
||||||
it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == GossipingRequestState.ACCEPTED }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread.sleep(6_000)
|
|
||||||
commonTestHelper.waitWithLatch { latch ->
|
|
||||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
|
||||||
aliceSession2.cryptoService().getOutgoingRoomKeyRequests().let {
|
|
||||||
it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == OutgoingGossipingRequestState.CANCELLED }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
commonTestHelper.runBlockingTest {
|
|
||||||
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
|
|
||||||
}
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
fail("should have been able to decrypt")
|
|
||||||
}
|
|
||||||
|
|
||||||
commonTestHelper.signOutAndClose(aliceSession)
|
commonTestHelper.signOutAndClose(aliceSession)
|
||||||
commonTestHelper.signOutAndClose(aliceSession2)
|
commonTestHelper.signOutAndClose(aliceSession2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See E2ESanityTest for a test regarding secret sharing
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that the sender of a message accepts to re-share to another user
|
||||||
|
* if the key was originally shared with him
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
fun test_reShareIfWasIntendedToBeShared() {
|
||||||
fun test_ShareSSSSSecret() {
|
val commonTestHelper = CommonTestHelper(context())
|
||||||
val aliceSession1 = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||||
|
|
||||||
commonTestHelper.doSync<Unit> {
|
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||||
aliceSession1.cryptoService().crossSigningService()
|
val aliceSession = testData.firstSession
|
||||||
.initializeCrossSigning(
|
val roomFromAlice = aliceSession.getRoom(testData.roomId)!!
|
||||||
object : UserInteractiveAuthInterceptor {
|
val bobSession = testData.secondSession!!
|
||||||
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
|
||||||
promise.resume(
|
|
||||||
UserPasswordAuth(
|
|
||||||
user = aliceSession1.myUserId,
|
|
||||||
password = TestConstants.PASSWORD
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, it
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also bootstrap keybackup on first session
|
val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first()
|
||||||
val creationInfo = commonTestHelper.doSync<MegolmBackupCreationInfo> {
|
val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||||
aliceSession1.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
|
|
||||||
}
|
|
||||||
val version = commonTestHelper.doSync<KeysVersion> {
|
|
||||||
aliceSession1.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
|
|
||||||
}
|
|
||||||
// Save it for gossiping
|
|
||||||
aliceSession1.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
|
|
||||||
|
|
||||||
val aliceSession2 = commonTestHelper.logIntoAccount(aliceSession1.myUserId, SessionTestParams(true))
|
// bob should be able to decrypt
|
||||||
|
cryptoTestHelper.ensureCanDecrypt(listOf(sentEvent.eventId), bobSession, testData.roomId, listOf(sentEvent.getLastMessageContent()?.body ?: ""))
|
||||||
|
|
||||||
val aliceVerificationService1 = aliceSession1.cryptoService().verificationService()
|
// Let's try to request any how.
|
||||||
val aliceVerificationService2 = aliceSession2.cryptoService().verificationService()
|
// As it was share previously alice should accept to reshare
|
||||||
|
bobSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
|
||||||
// force keys download
|
|
||||||
commonTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
|
|
||||||
aliceSession1.cryptoService().downloadKeys(listOf(aliceSession1.myUserId), true, it)
|
|
||||||
}
|
|
||||||
commonTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
|
|
||||||
aliceSession2.cryptoService().downloadKeys(listOf(aliceSession2.myUserId), true, it)
|
|
||||||
}
|
|
||||||
|
|
||||||
var session1ShortCode: String? = null
|
|
||||||
var session2ShortCode: String? = null
|
|
||||||
|
|
||||||
aliceVerificationService1.addListener(object : VerificationService.Listener {
|
|
||||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
|
||||||
Log.d("#TEST", "AA: tx incoming?:${tx.isIncoming} state ${tx.state}")
|
|
||||||
if (tx is SasVerificationTransaction) {
|
|
||||||
if (tx.state == VerificationTxState.OnStarted) {
|
|
||||||
(tx as IncomingSasVerificationTransaction).performAccept()
|
|
||||||
}
|
|
||||||
if (tx.state == VerificationTxState.ShortCodeReady) {
|
|
||||||
session1ShortCode = tx.getDecimalCodeRepresentation()
|
|
||||||
Thread.sleep(500)
|
|
||||||
tx.userHasVerifiedShortCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
aliceVerificationService2.addListener(object : VerificationService.Listener {
|
|
||||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
|
||||||
Log.d("#TEST", "BB: tx incoming?:${tx.isIncoming} state ${tx.state}")
|
|
||||||
if (tx is SasVerificationTransaction) {
|
|
||||||
if (tx.state == VerificationTxState.ShortCodeReady) {
|
|
||||||
session2ShortCode = tx.getDecimalCodeRepresentation()
|
|
||||||
Thread.sleep(500)
|
|
||||||
tx.userHasVerifiedShortCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
val txId = "m.testVerif12"
|
|
||||||
aliceVerificationService2.beginKeyVerification(
|
|
||||||
VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId
|
|
||||||
?: "", txId
|
|
||||||
)
|
|
||||||
|
|
||||||
commonTestHelper.waitWithLatch { latch ->
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.deviceId ?: "")?.isVerified == true
|
val outgoing = bobSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
|
||||||
|
val aliceReply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
|
||||||
|
aliceReply != null && aliceReply.result is RequestResult.Success
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assertNotNull(session1ShortCode)
|
|
||||||
Log.d("#TEST", "session1ShortCode: $session1ShortCode")
|
|
||||||
assertNotNull(session2ShortCode)
|
|
||||||
Log.d("#TEST", "session2ShortCode: $session2ShortCode")
|
|
||||||
assertEquals(session1ShortCode, session2ShortCode)
|
|
||||||
|
|
||||||
// SSK and USK private keys should have been shared
|
|
||||||
|
|
||||||
commonTestHelper.waitWithLatch(60_000) { latch ->
|
|
||||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
|
||||||
Log.d("#TEST", "CAN XS :${aliceSession2.cryptoService().crossSigningService().getMyCrossSigningKeys()}")
|
|
||||||
aliceSession2.cryptoService().crossSigningService().canCrossSign()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that key backup key has been shared to
|
|
||||||
commonTestHelper.waitWithLatch(60_000) { latch ->
|
|
||||||
val keysBackupService = aliceSession2.cryptoService().keysBackupService()
|
|
||||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
|
||||||
Log.d("#TEST", "Recovery :${keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
|
|
||||||
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commonTestHelper.signOutAndClose(aliceSession1)
|
|
||||||
commonTestHelper.signOutAndClose(aliceSession2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that our own devices accept to reshare to unverified device if it was shared initialy
|
||||||
|
* if the key was originally shared with him
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
fun test_reShareToUnverifiedIfWasIntendedToBeShared() {
|
||||||
fun test_ImproperKeyShareBug() {
|
val commonTestHelper = CommonTestHelper(context())
|
||||||
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||||
|
|
||||||
commonTestHelper.doSync<Unit> {
|
val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true)
|
||||||
aliceSession.cryptoService().crossSigningService()
|
val aliceSession = testData.firstSession
|
||||||
.initializeCrossSigning(
|
val roomFromAlice = aliceSession.getRoom(testData.roomId)!!
|
||||||
object : UserInteractiveAuthInterceptor {
|
|
||||||
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
|
||||||
promise.resume(
|
|
||||||
UserPasswordAuth(
|
// we wait for alice first session to be aware of that session?
|
||||||
user = aliceSession.myUserId,
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
password = TestConstants.PASSWORD,
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
session = flowResponse.session
|
val newSession = aliceSession.cryptoService().getUserDevices(aliceSession.myUserId)
|
||||||
)
|
.firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
|
||||||
)
|
newSession != null
|
||||||
}
|
}
|
||||||
}, it
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first()
|
||||||
|
val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||||
|
|
||||||
// Create an encrypted room and send a couple of messages
|
// Let's try to request any how.
|
||||||
val roomId = commonTestHelper.runBlockingTest {
|
// As it was share previously alice should accept to reshare
|
||||||
aliceSession.roomService().createRoom(
|
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
|
||||||
CreateRoomParams().apply {
|
|
||||||
visibility = RoomDirectoryVisibility.PRIVATE
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
enableEncryption()
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
}
|
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
|
||||||
)
|
val ownDeviceReply =
|
||||||
|
outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
|
||||||
|
ownDeviceReply != null && ownDeviceReply.result is RequestResult.Success
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val roomAlicePov = aliceSession.getRoom(roomId)
|
}
|
||||||
assertNotNull(roomAlicePov)
|
|
||||||
Thread.sleep(1_000)
|
|
||||||
assertTrue(roomAlicePov?.roomCryptoService()?.isEncrypted() == true)
|
|
||||||
val secondEventId = commonTestHelper.sendTextMessage(roomAlicePov!!, "Message", 3)[1].eventId
|
|
||||||
|
|
||||||
// Create bob session
|
/**
|
||||||
|
* Tests that keys reshared with own verified session are done from the earliest known index
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() {
|
||||||
|
val commonTestHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||||
|
|
||||||
val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true))
|
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||||
commonTestHelper.doSync<Unit> {
|
val aliceSession = testData.firstSession
|
||||||
bobSession.cryptoService().crossSigningService()
|
val bobSession = testData.secondSession!!
|
||||||
.initializeCrossSigning(
|
val roomFromBob = bobSession.getRoom(testData.roomId)!!
|
||||||
object : UserInteractiveAuthInterceptor {
|
|
||||||
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
|
||||||
promise.resume(
|
|
||||||
UserPasswordAuth(
|
|
||||||
user = bobSession.myUserId,
|
|
||||||
password = TestConstants.PASSWORD,
|
|
||||||
session = flowResponse.session
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, it
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let alice invite bob
|
val sentEvents = commonTestHelper.sendTextMessage(roomFromBob, "Hello", 3)
|
||||||
commonTestHelper.runBlockingTest {
|
val sentEventMegolmSession = sentEvents.first().root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||||
roomAlicePov.membershipService().invite(bobSession.myUserId, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
commonTestHelper.runBlockingTest {
|
// Let alice now add a new session
|
||||||
bobSession.roomService().joinRoom(roomAlicePov.roomId, null, emptyList())
|
val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(false))
|
||||||
}
|
aliceNewSession.cryptoService().enableKeyGossiping(false)
|
||||||
|
commonTestHelper.syncSession(aliceNewSession)
|
||||||
|
|
||||||
// we want to discard alice outbound session
|
// we wait bob first session to be aware of that session?
|
||||||
aliceSession.cryptoService().discardOutboundSession(roomAlicePov.roomId)
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
// and now resend a new message to reset index to 0
|
val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId)
|
||||||
commonTestHelper.sendTextMessage(roomAlicePov, "After", 1)
|
.firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
|
||||||
|
newSession != null
|
||||||
val roomRoomBobPov = aliceSession.getRoom(roomId)
|
|
||||||
val beforeJoin = roomRoomBobPov!!.getTimelineEvent(secondEventId)
|
|
||||||
|
|
||||||
var dRes = tryOrNull {
|
|
||||||
commonTestHelper.runBlockingTest {
|
|
||||||
bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(dRes == null)
|
val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first()
|
||||||
|
val newEventId = newEvent.eventId
|
||||||
|
val newEventText = newEvent.getLastMessageContent()!!.body
|
||||||
|
|
||||||
// Try to re-ask the keys
|
// alice should be able to decrypt the new one
|
||||||
|
cryptoTestHelper.ensureCanDecrypt(listOf(newEventId), aliceNewSession, testData.roomId, listOf(newEventText))
|
||||||
|
// but not the first one!
|
||||||
|
cryptoTestHelper.ensureCannotDecrypt(sentEvents.map { it.eventId }, aliceNewSession, testData.roomId)
|
||||||
|
|
||||||
bobSession.cryptoService().reRequestRoomKeyForEvent(beforeJoin!!.root)
|
// All should be using the same session id
|
||||||
|
sentEvents.forEach {
|
||||||
|
assertEquals(sentEventMegolmSession, it.root.content.toModel<EncryptedEventContent>()!!.sessionId)
|
||||||
|
}
|
||||||
|
assertEquals(sentEventMegolmSession, newEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId)
|
||||||
|
|
||||||
Thread.sleep(3_000)
|
// Request a first time, bob should reply with unauthorized and alice should reply with unverified
|
||||||
|
aliceNewSession.cryptoService().enableKeyGossiping(true)
|
||||||
|
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(newEvent.root)
|
||||||
|
|
||||||
// With the bug the first session would have improperly reshare that key :/
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
dRes = tryOrNull {
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
commonTestHelper.runBlockingTest {
|
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
|
||||||
bobSession.cryptoService().decryptEvent(beforeJoin.root, "")
|
val ownDeviceReply = outgoing?.results
|
||||||
|
?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
|
||||||
|
val result = ownDeviceReply?.result
|
||||||
|
Log.v("TEST", "own device result is $result")
|
||||||
|
result != null && result is RequestResult.Failure && result.code == WithHeldCode.UNVERIFIED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel<MessageContent>()?.body}")
|
|
||||||
assert(dRes?.clearEvent == null)
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
|
||||||
|
val bobDeviceReply = outgoing?.results
|
||||||
|
?.firstOrNull { it.userId == bobSession.myUserId && it.fromDevice == bobSession.sessionParams.deviceId }
|
||||||
|
val result = bobDeviceReply?.result
|
||||||
|
Log.v("TEST", "bob device result is $result")
|
||||||
|
result != null && result is RequestResult.Success && result.chainIndex > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// it's a success but still can't decrypt first message
|
||||||
|
cryptoTestHelper.ensureCannotDecrypt(sentEvents.map { it.eventId }, aliceNewSession, testData.roomId)
|
||||||
|
|
||||||
|
// Mark the new session as verified
|
||||||
|
aliceSession.cryptoService()
|
||||||
|
.verificationService()
|
||||||
|
.markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!)
|
||||||
|
|
||||||
|
// Let's now try to request
|
||||||
|
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root)
|
||||||
|
|
||||||
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
// DEBUG LOGS
|
||||||
|
aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest ->
|
||||||
|
Log.v("TEST", "=========================")
|
||||||
|
Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
|
||||||
|
Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
|
||||||
|
Log.v("TEST", "=========================")
|
||||||
|
}
|
||||||
|
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
|
||||||
|
val ownDeviceReply =
|
||||||
|
outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
|
||||||
|
val result = ownDeviceReply?.result
|
||||||
|
result != null && result is RequestResult.Success && result.chainIndex == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// now the new session should be able to decrypt all!
|
||||||
|
cryptoTestHelper.ensureCanDecrypt(
|
||||||
|
sentEvents.map { it.eventId },
|
||||||
|
aliceNewSession,
|
||||||
|
testData.roomId,
|
||||||
|
sentEvents.map { it.getLastMessageContent()!!.body }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Additional test, can we check that bob replied successfully but with a ratcheted key
|
||||||
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
|
||||||
|
val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId }
|
||||||
|
val result = bobReply?.result
|
||||||
|
result != null && result is RequestResult.Success && result.chainIndex == 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commonTestHelper.signOutAndClose(aliceNewSession)
|
||||||
|
commonTestHelper.signOutAndClose(aliceSession)
|
||||||
|
commonTestHelper.signOutAndClose(bobSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that we don't cancel a request to early on first forward if the index is not good enough
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun test_dontCancelToEarly() {
|
||||||
|
val commonTestHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||||
|
|
||||||
|
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||||
|
val aliceSession = testData.firstSession
|
||||||
|
val bobSession = testData.secondSession!!
|
||||||
|
val roomFromBob = bobSession.getRoom(testData.roomId)!!
|
||||||
|
|
||||||
|
val sentEvents = commonTestHelper.sendTextMessage(roomFromBob, "Hello", 3)
|
||||||
|
val sentEventMegolmSession = sentEvents.first().root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||||
|
|
||||||
|
// Let alice now add a new session
|
||||||
|
val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
|
||||||
|
|
||||||
|
// we wait bob first session to be aware of that session?
|
||||||
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId)
|
||||||
|
.firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
|
||||||
|
newSession != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first()
|
||||||
|
val newEventId = newEvent.eventId
|
||||||
|
val newEventText = newEvent.getLastMessageContent()!!.body
|
||||||
|
|
||||||
|
// alice should be able to decrypt the new one
|
||||||
|
cryptoTestHelper.ensureCanDecrypt(listOf(newEventId), aliceNewSession, testData.roomId, listOf(newEventText))
|
||||||
|
// but not the first one!
|
||||||
|
cryptoTestHelper.ensureCannotDecrypt(sentEvents.map { it.eventId }, aliceNewSession, testData.roomId)
|
||||||
|
|
||||||
|
// All should be using the same session id
|
||||||
|
sentEvents.forEach {
|
||||||
|
assertEquals(sentEventMegolmSession, it.root.content.toModel<EncryptedEventContent>()!!.sessionId)
|
||||||
|
}
|
||||||
|
assertEquals(sentEventMegolmSession, newEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId)
|
||||||
|
|
||||||
|
// Mark the new session as verified
|
||||||
|
aliceSession.cryptoService()
|
||||||
|
.verificationService()
|
||||||
|
.markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!)
|
||||||
|
|
||||||
|
// /!\ Stop initial alice session syncing so that it can't reply
|
||||||
|
aliceSession.cryptoService().enableKeyGossiping(false)
|
||||||
|
aliceSession.stopSync()
|
||||||
|
|
||||||
|
// Let's now try to request
|
||||||
|
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root)
|
||||||
|
|
||||||
|
// Should get a reply from bob and not from alice
|
||||||
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
// Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}")
|
||||||
|
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
|
||||||
|
val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId }
|
||||||
|
val result = bobReply?.result
|
||||||
|
result != null && result is RequestResult.Success && result.chainIndex == 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val outgoingReq = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
|
||||||
|
|
||||||
|
assertNull("We should not have a reply from first session", outgoingReq!!.results.firstOrNull { it.fromDevice == aliceSession.sessionParams.deviceId })
|
||||||
|
assertEquals("The request should not be canceled", OutgoingRoomKeyRequestState.SENT, outgoingReq.state)
|
||||||
|
|
||||||
|
// let's wake up alice
|
||||||
|
aliceSession.cryptoService().enableKeyGossiping(true)
|
||||||
|
aliceSession.startSync(true)
|
||||||
|
|
||||||
|
// We should now get a reply from first session
|
||||||
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
|
||||||
|
val ownDeviceReply =
|
||||||
|
outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
|
||||||
|
val result = ownDeviceReply?.result
|
||||||
|
result != null && result is RequestResult.Success && result.chainIndex == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// It should be in sent then cancel
|
||||||
|
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
|
||||||
|
assertEquals("The request should be canceled", OutgoingRoomKeyRequestState.SENT_THEN_CANCELED, outgoing!!.state)
|
||||||
|
|
||||||
|
commonTestHelper.signOutAndClose(aliceNewSession)
|
||||||
|
commonTestHelper.signOutAndClose(aliceSession)
|
||||||
|
commonTestHelper.signOutAndClose(bobSession)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.FixMethodOrder
|
import org.junit.FixMethodOrder
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.MethodSorters
|
import org.junit.runners.MethodSorters
|
||||||
|
@ -29,6 +28,7 @@ import org.matrix.android.sdk.InstrumentedTest
|
||||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.RequestResult
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||||
|
@ -46,12 +46,11 @@ import org.matrix.android.sdk.common.TestConstants
|
||||||
@LargeTest
|
@LargeTest
|
||||||
class WithHeldTests : InstrumentedTest {
|
class WithHeldTests : InstrumentedTest {
|
||||||
|
|
||||||
private val testHelper = CommonTestHelper(context())
|
|
||||||
private val cryptoTestHelper = CryptoTestHelper(testHelper)
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun test_WithHeldUnverifiedReason() {
|
fun test_WithHeldUnverifiedReason() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
// =============================
|
// =============================
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
// =============================
|
// =============================
|
||||||
|
@ -69,7 +68,6 @@ class WithHeldTests : InstrumentedTest {
|
||||||
val roomAlicePOV = aliceSession.getRoom(roomId)!!
|
val roomAlicePOV = aliceSession.getRoom(roomId)!!
|
||||||
|
|
||||||
val bobUnverifiedSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
|
val bobUnverifiedSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
|
||||||
|
|
||||||
// =============================
|
// =============================
|
||||||
// ACT
|
// ACT
|
||||||
// =============================
|
// =============================
|
||||||
|
@ -88,6 +86,7 @@ class WithHeldTests : InstrumentedTest {
|
||||||
|
|
||||||
val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!!
|
val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!!
|
||||||
|
|
||||||
|
val megolmSessionId = eventBobPOV.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||||
// =============================
|
// =============================
|
||||||
// ASSERT
|
// ASSERT
|
||||||
// =============================
|
// =============================
|
||||||
|
@ -103,9 +102,23 @@ class WithHeldTests : InstrumentedTest {
|
||||||
val type = (failure as MXCryptoError.Base).errorType
|
val type = (failure as MXCryptoError.Base).errorType
|
||||||
val technicalMessage = failure.technicalMessage
|
val technicalMessage = failure.technicalMessage
|
||||||
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
|
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
|
||||||
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
|
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNAUTHORISED.value, technicalMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Let's see if the reply we got from bob first session is unverified
|
||||||
|
testHelper.waitWithLatch { latch ->
|
||||||
|
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
|
bobUnverifiedSession.cryptoService().getOutgoingRoomKeyRequests()
|
||||||
|
.firstOrNull { it.sessionId == megolmSessionId }
|
||||||
|
?.results
|
||||||
|
?.firstOrNull { it.fromDevice == bobSession.sessionParams.deviceId }
|
||||||
|
?.result
|
||||||
|
?.let {
|
||||||
|
it as? RequestResult.Failure
|
||||||
|
}
|
||||||
|
?.code == WithHeldCode.UNVERIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
// enable back sending to unverified
|
// enable back sending to unverified
|
||||||
aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false)
|
aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false)
|
||||||
|
|
||||||
|
@ -130,7 +143,7 @@ class WithHeldTests : InstrumentedTest {
|
||||||
val type = (failure as MXCryptoError.Base).errorType
|
val type = (failure as MXCryptoError.Base).errorType
|
||||||
val technicalMessage = failure.technicalMessage
|
val technicalMessage = failure.technicalMessage
|
||||||
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
|
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
|
||||||
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
|
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNAUTHORISED.value, technicalMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
testHelper.signOutAndClose(aliceSession)
|
testHelper.signOutAndClose(aliceSession)
|
||||||
|
@ -139,8 +152,10 @@ class WithHeldTests : InstrumentedTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun test_WithHeldNoOlm() {
|
fun test_WithHeldNoOlm() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||||
val aliceSession = testData.firstSession
|
val aliceSession = testData.firstSession
|
||||||
val bobSession = testData.secondSession!!
|
val bobSession = testData.secondSession!!
|
||||||
|
@ -220,8 +235,10 @@ class WithHeldTests : InstrumentedTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
|
||||||
fun test_WithHeldKeyRequest() {
|
fun test_WithHeldKeyRequest() {
|
||||||
|
val testHelper = CommonTestHelper(context())
|
||||||
|
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||||
|
|
||||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||||
val aliceSession = testData.firstSession
|
val aliceSession = testData.firstSession
|
||||||
val bobSession = testData.secondSession!!
|
val bobSession = testData.secondSession!!
|
||||||
|
@ -267,5 +284,8 @@ class WithHeldTests : InstrumentedTest {
|
||||||
wc?.code == WithHeldCode.UNAUTHORISED
|
wc?.code == WithHeldCode.UNAUTHORISED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testHelper.signOutAndClose(aliceSession)
|
||||||
|
testHelper.signOutAndClose(bobSecondSession)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.crypto.verification.qrcode
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.amshove.kluent.shouldBe
|
import org.amshove.kluent.shouldBe
|
||||||
import org.junit.FixMethodOrder
|
import org.junit.FixMethodOrder
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.MethodSorters
|
import org.junit.runners.MethodSorters
|
||||||
|
@ -40,7 +39,6 @@ import kotlin.coroutines.resume
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@FixMethodOrder(MethodSorters.JVM)
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
@Ignore("This test is flaky ; see issue #5449")
|
|
||||||
class VerificationTest : InstrumentedTest {
|
class VerificationTest : InstrumentedTest {
|
||||||
|
|
||||||
data class ExpectedResult(
|
data class ExpectedResult(
|
||||||
|
|
|
@ -31,5 +31,11 @@ data class MXCryptoConfig constructor(
|
||||||
* If set to false, the request will be forwarded to the application layer; in this
|
* If set to false, the request will be forwarded to the application layer; in this
|
||||||
* case the application can decide to prompt the user.
|
* case the application can decide to prompt the user.
|
||||||
*/
|
*/
|
||||||
val discardRoomKeyRequestsFromUntrustedDevices: Boolean = true
|
val discardRoomKeyRequestsFromUntrustedDevices: Boolean = true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currently megolm keys are requested to the sender device and to all of our devices.
|
||||||
|
* You can limit request only to your sessions by turning this setting to `true`
|
||||||
|
*/
|
||||||
|
val limitRoomKeyRequestsToMyDevices: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningServic
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
|
||||||
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
|
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
|
import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
|
||||||
|
@ -35,8 +36,6 @@ import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
|
import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
import org.matrix.android.sdk.api.session.events.model.Content
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
@ -76,6 +75,15 @@ interface CryptoService {
|
||||||
|
|
||||||
fun setGlobalBlacklistUnverifiedDevices(block: Boolean)
|
fun setGlobalBlacklistUnverifiedDevices(block: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable key gossiping.
|
||||||
|
* Default is true.
|
||||||
|
* If set to false this device won't send key_request nor will accept key forwarded
|
||||||
|
*/
|
||||||
|
fun enableKeyGossiping(enable: Boolean)
|
||||||
|
|
||||||
|
fun isKeyGossipingEnabled(): Boolean
|
||||||
|
|
||||||
fun setRoomUnBlacklistUnverifiedDevices(roomId: String)
|
fun setRoomUnBlacklistUnverifiedDevices(roomId: String)
|
||||||
|
|
||||||
fun getDeviceTrackingStatus(userId: String): Int
|
fun getDeviceTrackingStatus(userId: String): Int
|
||||||
|
@ -94,8 +102,6 @@ interface CryptoService {
|
||||||
|
|
||||||
fun reRequestRoomKeyForEvent(event: Event)
|
fun reRequestRoomKeyForEvent(event: Event)
|
||||||
|
|
||||||
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody)
|
|
||||||
|
|
||||||
fun addRoomKeysRequestListener(listener: GossipingRequestListener)
|
fun addRoomKeysRequestListener(listener: GossipingRequestListener)
|
||||||
|
|
||||||
fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
|
fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
|
||||||
|
@ -142,14 +148,20 @@ interface CryptoService {
|
||||||
fun addNewSessionListener(newSessionListener: NewSessionListener)
|
fun addNewSessionListener(newSessionListener: NewSessionListener)
|
||||||
fun removeSessionListener(listener: NewSessionListener)
|
fun removeSessionListener(listener: NewSessionListener)
|
||||||
|
|
||||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest>
|
||||||
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
|
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>>
|
||||||
|
|
||||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||||
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
|
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
|
||||||
|
|
||||||
fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
|
/**
|
||||||
fun getGossipingEvents(): List<Event>
|
* Can be called by the app layer to accept a request manually
|
||||||
|
* Use carefully as it is prone to social attacks
|
||||||
|
*/
|
||||||
|
suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest)
|
||||||
|
|
||||||
|
fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>>
|
||||||
|
fun getGossipingEvents(): List<AuditTrail>
|
||||||
|
|
||||||
// For testing shared session
|
// For testing shared session
|
||||||
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
|
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -14,14 +14,20 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.matrix.android.sdk.api.session.crypto.model
|
package org.matrix.android.sdk.api.session.crypto
|
||||||
|
|
||||||
enum class OutgoingGossipingRequestState {
|
enum class OutgoingRoomKeyRequestState {
|
||||||
UNSENT,
|
UNSENT,
|
||||||
SENDING,
|
|
||||||
SENT,
|
SENT,
|
||||||
CANCELLING,
|
SENT_THEN_CANCELED,
|
||||||
CANCELLED,
|
CANCELLATION_PENDING,
|
||||||
FAILED_TO_SEND,
|
CANCELLATION_PENDING_AND_WILL_RESEND;
|
||||||
FAILED_TO_CANCEL
|
|
||||||
|
companion object {
|
||||||
|
fun pendingStates() = setOf(
|
||||||
|
UNSENT,
|
||||||
|
CANCELLATION_PENDING_AND_WILL_RESEND,
|
||||||
|
CANCELLATION_PENDING
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -16,9 +16,8 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.api.session.crypto.keyshare
|
package org.matrix.android.sdk.api.session.crypto.keyshare
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRequestCancellation
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
|
import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Room keys events listener
|
* Room keys events listener
|
||||||
|
@ -35,12 +34,12 @@ interface GossipingRequestListener {
|
||||||
* Returns the secret value to be shared
|
* Returns the secret value to be shared
|
||||||
* @return true if is handled
|
* @return true if is handled
|
||||||
*/
|
*/
|
||||||
fun onSecretShareRequest(request: IncomingSecretShareRequest): Boolean
|
fun onSecretShareRequest(request: SecretShareRequest): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A room key request cancellation has been received.
|
* A room key request cancellation has been received.
|
||||||
*
|
*
|
||||||
* @param request the cancellation request
|
* @param request the cancellation request
|
||||||
*/
|
*/
|
||||||
fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation)
|
fun onRequestCancelled(request: IncomingRoomKeyRequest)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
package org.matrix.android.sdk.api.session.crypto.model
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.internal.util.time.Clock
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
|
||||||
import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IncomingRoomKeyRequest class defines the incoming room keys request.
|
* IncomingRoomKeyRequest class defines the incoming room keys request.
|
||||||
|
@ -27,38 +25,25 @@ data class IncomingRoomKeyRequest(
|
||||||
/**
|
/**
|
||||||
* The user id
|
* The user id
|
||||||
*/
|
*/
|
||||||
override val userId: String? = null,
|
val userId: String? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The device id
|
* The device id
|
||||||
*/
|
*/
|
||||||
override val deviceId: String? = null,
|
val deviceId: String? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The request id
|
* The request id
|
||||||
*/
|
*/
|
||||||
override val requestId: String? = null,
|
val requestId: String? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The request body
|
* The request body
|
||||||
*/
|
*/
|
||||||
val requestBody: RoomKeyRequestBody? = null,
|
val requestBody: RoomKeyRequestBody? = null,
|
||||||
|
|
||||||
val state: GossipingRequestState = GossipingRequestState.NONE,
|
val localCreationTimestamp: Long?
|
||||||
|
) {
|
||||||
/**
|
|
||||||
* The runnable to call to accept to share the keys
|
|
||||||
*/
|
|
||||||
@Transient
|
|
||||||
var share: Runnable? = null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The runnable to call to ignore the key share request.
|
|
||||||
*/
|
|
||||||
@Transient
|
|
||||||
var ignore: Runnable? = null,
|
|
||||||
override val localCreationTimestamp: Long?
|
|
||||||
) : IncomingShareRequestCommon {
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Factory
|
* Factory
|
||||||
|
@ -66,18 +51,36 @@ data class IncomingRoomKeyRequest(
|
||||||
* @param event the event
|
* @param event the event
|
||||||
* @param currentTimeMillis the current time in milliseconds
|
* @param currentTimeMillis the current time in milliseconds
|
||||||
*/
|
*/
|
||||||
fun fromEvent(event: Event, currentTimeMillis: Long): IncomingRoomKeyRequest? {
|
fun fromEvent(trail: AuditTrail): IncomingRoomKeyRequest? {
|
||||||
return event.getClearContent()
|
return trail
|
||||||
.toModel<RoomKeyShareRequest>()
|
.takeIf { it.type == TrailType.IncomingKeyRequest }
|
||||||
|
?.let {
|
||||||
|
it.info as? IncomingKeyRequestInfo
|
||||||
|
}
|
||||||
?.let {
|
?.let {
|
||||||
IncomingRoomKeyRequest(
|
IncomingRoomKeyRequest(
|
||||||
userId = event.senderId,
|
userId = it.userId,
|
||||||
deviceId = it.requestingDeviceId,
|
deviceId = it.deviceId,
|
||||||
requestId = it.requestId,
|
requestId = it.requestId,
|
||||||
requestBody = it.body ?: RoomKeyRequestBody(),
|
requestBody = RoomKeyRequestBody(
|
||||||
localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis
|
algorithm = it.alg,
|
||||||
|
roomId = it.roomId,
|
||||||
|
senderKey = it.senderKey,
|
||||||
|
sessionId = it.sessionId
|
||||||
|
),
|
||||||
|
localCreationTimestamp = trail.ageLocalTs
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun fromRestRequest(senderId: String, request: RoomKeyShareRequest, clock: Clock): IncomingRoomKeyRequest? {
|
||||||
|
return IncomingRoomKeyRequest(
|
||||||
|
userId = senderId,
|
||||||
|
deviceId = request.requestingDeviceId,
|
||||||
|
requestId = request.requestId,
|
||||||
|
requestBody = request.body,
|
||||||
|
localCreationTimestamp = clock.epochMillis()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
* A human-readable reason for why the key was not sent. The receiving client should only use this string if it does not understand the code.
|
||||||
*/
|
*/
|
||||||
@Json(name = "reason") val reason: String? = null
|
@Json(name = "reason") val reason: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the device ID of the device sending the m.room_key.withheld message
|
||||||
|
* MSC3735
|
||||||
|
*/
|
||||||
|
@Json(name = "from_device") val fromDevice: String? = null
|
||||||
|
|
||||||
) {
|
) {
|
||||||
val code: WithHeldCode?
|
val code: WithHeldCode?
|
||||||
|
|
|
@ -131,7 +131,7 @@ interface SharedSecretStorageService {
|
||||||
|
|
||||||
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?): IntegrityResult
|
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?): IntegrityResult
|
||||||
|
|
||||||
fun requestSecret(name: String, myOtherDeviceId: String)
|
suspend fun requestSecret(name: String, myOtherDeviceId: String)
|
||||||
|
|
||||||
data class KeyRef(
|
data class KeyRef(
|
||||||
val keyId: String?,
|
val keyId: String?,
|
||||||
|
|
|
@ -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.CryptoService
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||||
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
|
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
|
import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
|
||||||
|
@ -57,15 +59,13 @@ import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
|
import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
|
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
import org.matrix.android.sdk.api.session.crypto.model.TrailType
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
import org.matrix.android.sdk.api.session.events.model.Content
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
|
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||||
|
@ -76,7 +76,6 @@ import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption
|
import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
|
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
|
||||||
|
@ -91,6 +90,7 @@ import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask
|
import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
|
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
|
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
|
||||||
|
import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor
|
||||||
import org.matrix.android.sdk.internal.di.DeviceId
|
import org.matrix.android.sdk.internal.di.DeviceId
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
|
@ -156,9 +156,10 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
|
|
||||||
private val crossSigningService: DefaultCrossSigningService,
|
private val crossSigningService: DefaultCrossSigningService,
|
||||||
//
|
//
|
||||||
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
private val incomingKeyRequestManager: IncomingKeyRequestManager,
|
||||||
|
private val secretShareManager: SecretShareManager,
|
||||||
//
|
//
|
||||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||||
// Actions
|
// Actions
|
||||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||||
private val megolmSessionDataImporter: MegolmSessionDataImporter,
|
private val megolmSessionDataImporter: MegolmSessionDataImporter,
|
||||||
|
@ -178,6 +179,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
private val cryptoCoroutineScope: CoroutineScope,
|
||||||
private val eventDecryptor: EventDecryptor,
|
private val eventDecryptor: EventDecryptor,
|
||||||
|
private val verificationMessageProcessor: VerificationMessageProcessor,
|
||||||
private val liveEventManager: Lazy<StreamEventsManager>
|
private val liveEventManager: Lazy<StreamEventsManager>
|
||||||
) : CryptoService {
|
) : CryptoService {
|
||||||
|
|
||||||
|
@ -192,7 +194,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onLiveEvent(roomId: String, event: Event) {
|
fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean) {
|
||||||
// handle state events
|
// handle state events
|
||||||
if (event.isStateEvent()) {
|
if (event.isStateEvent()) {
|
||||||
when (event.type) {
|
when (event.type) {
|
||||||
|
@ -201,9 +203,18 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
|
EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle verification
|
||||||
|
if (!isInitialSync) {
|
||||||
|
if (event.type != null && verificationMessageProcessor.shouldProcess(event.type)) {
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) {
|
||||||
|
verificationMessageProcessor.process(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val gossipingBuffer = mutableListOf<Event>()
|
// val gossipingBuffer = mutableListOf<Event>()
|
||||||
|
|
||||||
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
|
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
|
||||||
setDeviceNameTask
|
setDeviceNameTask
|
||||||
|
@ -379,27 +390,8 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
// Open the store
|
// Open the store
|
||||||
cryptoStore.open()
|
cryptoStore.open()
|
||||||
|
|
||||||
runCatching {
|
isStarting.set(false)
|
||||||
// if (isInitialSync) {
|
isStarted.set(true)
|
||||||
// // refresh the devices list for each known room members
|
|
||||||
// deviceListManager.invalidateAllDeviceLists()
|
|
||||||
// deviceListManager.refreshOutdatedDeviceLists()
|
|
||||||
// } else {
|
|
||||||
|
|
||||||
// Why would we do that? it will be called at end of syn
|
|
||||||
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
|
||||||
// }
|
|
||||||
}.fold(
|
|
||||||
{
|
|
||||||
isStarting.set(false)
|
|
||||||
isStarted.set(true)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isStarting.set(false)
|
|
||||||
isStarted.set(false)
|
|
||||||
Timber.tag(loggerTag.value).e(it, "Start failed")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -407,7 +399,8 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun close() = runBlocking(coroutineDispatchers.crypto) {
|
fun close() = runBlocking(coroutineDispatchers.crypto) {
|
||||||
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
|
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
|
||||||
incomingGossipingRequestManager.close()
|
incomingKeyRequestManager.close()
|
||||||
|
outgoingKeyRequestManager.close()
|
||||||
olmDevice.release()
|
olmDevice.release()
|
||||||
cryptoStore.close()
|
cryptoStore.close()
|
||||||
}
|
}
|
||||||
|
@ -472,15 +465,28 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
||||||
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
tryOrNull {
|
// Process pending key requests
|
||||||
gossipingBuffer.toList().let {
|
try {
|
||||||
cryptoStore.saveGossipingEvents(it)
|
if (toDevices.isEmpty()) {
|
||||||
|
// this is not blocking
|
||||||
|
outgoingKeyRequestManager.requireProcessAllPendingKeyRequests()
|
||||||
|
} else {
|
||||||
|
Timber.tag(loggerTag.value)
|
||||||
|
.w("Don't process key requests yet as there might be more to_device to catchup")
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
// just for safety but should not throw
|
||||||
|
Timber.tag(loggerTag.value).w("failed to process pending request")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
incomingKeyRequestManager.processIncomingRequests()
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
// just for safety but should not throw
|
||||||
|
Timber.tag(loggerTag.value).w("failed to process incoming room key requests")
|
||||||
}
|
}
|
||||||
gossipingBuffer.clear()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -594,7 +600,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
// (for now at least. Maybe we should alert the user somehow?)
|
// (for now at least. Maybe we should alert the user somehow?)
|
||||||
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
|
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
|
||||||
|
|
||||||
if (existingAlgorithm == algorithm && roomEncryptorsStore.get(roomId) != null) {
|
if (existingAlgorithm == algorithm) {
|
||||||
// ignore
|
// ignore
|
||||||
Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in $roomId")
|
Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in $roomId")
|
||||||
return false
|
return false
|
||||||
|
@ -787,19 +793,25 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
when (event.getClearType()) {
|
when (event.getClearType()) {
|
||||||
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
||||||
gossipingBuffer.add(event)
|
|
||||||
// Keys are imported directly, not waiting for end of sync
|
// Keys are imported directly, not waiting for end of sync
|
||||||
onRoomKeyEvent(event)
|
onRoomKeyEvent(event)
|
||||||
}
|
}
|
||||||
EventType.REQUEST_SECRET,
|
EventType.REQUEST_SECRET -> {
|
||||||
|
secretShareManager.handleSecretRequest(event)
|
||||||
|
}
|
||||||
EventType.ROOM_KEY_REQUEST -> {
|
EventType.ROOM_KEY_REQUEST -> {
|
||||||
// save audit trail
|
event.getClearContent().toModel<RoomKeyShareRequest>()?.let { req ->
|
||||||
gossipingBuffer.add(event)
|
// We'll always get these because we send room key requests to
|
||||||
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
|
// '*' (ie. 'all devices') which includes the sending device,
|
||||||
incomingGossipingRequestManager.onGossipingRequestEvent(event)
|
// so ignore requests from ourself because apart from it being
|
||||||
|
// very silly, it won't work because an Olm session cannot send
|
||||||
|
// messages to itself.
|
||||||
|
if (req.requestingDeviceId != deviceId) { // ignore self requests
|
||||||
|
event.senderId?.let { incomingKeyRequestManager.addNewIncomingRequest(it, req) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
EventType.SEND_SECRET -> {
|
EventType.SEND_SECRET -> {
|
||||||
gossipingBuffer.add(event)
|
|
||||||
onSecretSendReceived(event)
|
onSecretSendReceived(event)
|
||||||
}
|
}
|
||||||
EventType.ROOM_KEY_WITHHELD -> {
|
EventType.ROOM_KEY_WITHHELD -> {
|
||||||
|
@ -837,50 +849,38 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
val withHeldContent = event.getClearContent().toModel<RoomKeyWithHeldContent>() ?: return Unit.also {
|
val withHeldContent = event.getClearContent().toModel<RoomKeyWithHeldContent>() ?: return Unit.also {
|
||||||
Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields")
|
Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields")
|
||||||
}
|
}
|
||||||
Timber.tag(loggerTag.value).i("onKeyWithHeldReceived() received from:${event.senderId}, content <$withHeldContent>")
|
val senderId = event.senderId ?: return Unit.also {
|
||||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm)
|
Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields")
|
||||||
if (alg is IMXWithHeldExtension) {
|
|
||||||
alg.onRoomKeyWithHeldEvent(withHeldContent)
|
|
||||||
} else {
|
|
||||||
Timber.tag(loggerTag.value).e("onKeyWithHeldReceived() from:${event.senderId}: Unable to handle WithHeldContent for ${withHeldContent.algorithm}")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
withHeldContent.sessionId ?: return
|
||||||
|
withHeldContent.algorithm ?: return
|
||||||
|
withHeldContent.roomId ?: return
|
||||||
|
withHeldContent.senderKey ?: return
|
||||||
|
outgoingKeyRequestManager.onRoomKeyWithHeld(
|
||||||
|
sessionId = withHeldContent.sessionId,
|
||||||
|
algorithm = withHeldContent.algorithm,
|
||||||
|
roomId = withHeldContent.roomId,
|
||||||
|
senderKey = withHeldContent.senderKey,
|
||||||
|
fromDevice = withHeldContent.fromDevice,
|
||||||
|
event = Event(
|
||||||
|
type = EventType.ROOM_KEY_WITHHELD,
|
||||||
|
senderId = senderId,
|
||||||
|
content = event.getClearContent()
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSecretSendReceived(event: Event) {
|
private suspend fun onSecretSendReceived(event: Event) {
|
||||||
Timber.tag(loggerTag.value).i("GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}")
|
secretShareManager.onSecretSendReceived(event) { secretName, secretValue ->
|
||||||
if (!event.isEncrypted()) {
|
handleSDKLevelGossip(secretName, secretValue)
|
||||||
// secret send messages must be encrypted
|
|
||||||
Timber.tag(loggerTag.value).e("GOSSIP onSecretSend() :Received unencrypted secret send event")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Was that sent by us?
|
|
||||||
if (event.senderId != userId) {
|
|
||||||
Timber.tag(loggerTag.value).e("GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val secretContent = event.getClearContent().toModel<SecretSendEventContent>() ?: return
|
|
||||||
|
|
||||||
val existingRequest = cryptoStore
|
|
||||||
.getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId }
|
|
||||||
|
|
||||||
if (existingRequest == null) {
|
|
||||||
Timber.tag(loggerTag.value).i("GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) {
|
|
||||||
// TODO Ask to application layer?
|
|
||||||
Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if handled by SDK, otherwise should be sent to application layer
|
* Returns true if handled by SDK, otherwise should be sent to application layer
|
||||||
*/
|
*/
|
||||||
private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean {
|
private fun handleSDKLevelGossip(secretName: String?,
|
||||||
|
secretValue: String): Boolean {
|
||||||
return when (secretName) {
|
return when (secretName) {
|
||||||
MASTER_KEY_SSSS_NAME -> {
|
MASTER_KEY_SSSS_NAME -> {
|
||||||
crossSigningService.onSecretMSKGossip(secretValue)
|
crossSigningService.onSecretMSKGossip(secretValue)
|
||||||
|
@ -1095,6 +1095,12 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
cryptoStore.setGlobalBlacklistUnverifiedDevices(block)
|
cryptoStore.setGlobalBlacklistUnverifiedDevices(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun enableKeyGossiping(enable: Boolean) {
|
||||||
|
cryptoStore.enableKeyGossiping(enable)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isKeyGossipingEnabled() = cryptoStore.isKeyGossipingEnabled()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells whether the client should ever send encrypted messages to unverified devices.
|
* Tells whether the client should ever send encrypted messages to unverified devices.
|
||||||
* The default value is false.
|
* The default value is false.
|
||||||
|
@ -1158,52 +1164,17 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
setRoomBlacklistUnverifiedDevices(roomId, false)
|
setRoomBlacklistUnverifiedDevices(roomId, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Check if this method is still necessary
|
|
||||||
/**
|
|
||||||
* Cancel any earlier room key request
|
|
||||||
*
|
|
||||||
* @param requestBody requestBody
|
|
||||||
*/
|
|
||||||
override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
|
||||||
outgoingGossipingRequestManager.cancelRoomKeyRequest(requestBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Re request the encryption keys required to decrypt an event.
|
* Re request the encryption keys required to decrypt an event.
|
||||||
*
|
*
|
||||||
* @param event the event to decrypt again.
|
* @param event the event to decrypt again.
|
||||||
*/
|
*/
|
||||||
override fun reRequestRoomKeyForEvent(event: Event) {
|
override fun reRequestRoomKeyForEvent(event: Event) {
|
||||||
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
|
outgoingKeyRequestManager.requestKeyForEvent(event, true)
|
||||||
Timber.tag(loggerTag.value).e("reRequestRoomKeyForEvent Failed to re-request key, null content")
|
|
||||||
}
|
|
||||||
|
|
||||||
val requestBody = RoomKeyRequestBody(
|
|
||||||
algorithm = wireContent.algorithm,
|
|
||||||
roomId = event.roomId,
|
|
||||||
senderKey = wireContent.senderKey,
|
|
||||||
sessionId = wireContent.sessionId
|
|
||||||
)
|
|
||||||
|
|
||||||
outgoingGossipingRequestManager.resendRoomKeyRequest(requestBody)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun requestRoomKeyForEvent(event: Event) {
|
override fun requestRoomKeyForEvent(event: Event) {
|
||||||
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
|
outgoingKeyRequestManager.requestKeyForEvent(event, false)
|
||||||
Timber.tag(loggerTag.value).e("requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}")
|
|
||||||
}
|
|
||||||
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
|
||||||
// if (!isStarted()) {
|
|
||||||
// Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init")
|
|
||||||
// internalStart(false)
|
|
||||||
// }
|
|
||||||
roomDecryptorProvider
|
|
||||||
.getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
|
|
||||||
?.requestKeysForEvent(event, false) ?: run {
|
|
||||||
Timber.tag(loggerTag.value).v("requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1212,7 +1183,8 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
* @param listener listener
|
* @param listener listener
|
||||||
*/
|
*/
|
||||||
override fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
|
override fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||||
incomingGossipingRequestManager.addRoomKeysRequestListener(listener)
|
incomingKeyRequestManager.addRoomKeysRequestListener(listener)
|
||||||
|
secretShareManager.addListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1221,42 +1193,10 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
* @param listener listener
|
* @param listener listener
|
||||||
*/
|
*/
|
||||||
override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
|
override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||||
incomingGossipingRequestManager.removeRoomKeysRequestListener(listener)
|
incomingKeyRequestManager.removeRoomKeysRequestListener(listener)
|
||||||
|
secretShareManager.removeListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
// private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
|
|
||||||
// val deviceKey = deviceInfo.identityKey()
|
|
||||||
//
|
|
||||||
// val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
|
|
||||||
// val now = clock.epochMillis()
|
|
||||||
// if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
|
|
||||||
// Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
|
|
||||||
// lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
|
|
||||||
//
|
|
||||||
// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
|
||||||
// ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
|
|
||||||
//
|
|
||||||
// // Now send a blank message on that session so the other side knows about it.
|
|
||||||
// // (The keyshare request is sent in the clear so that won't do)
|
|
||||||
// // We send this first such that, as long as the toDevice messages arrive in the
|
|
||||||
// // same order we sent them, the other end will get this first, set up the new session,
|
|
||||||
// // then get the keyshare request and send the key over this new session (because it
|
|
||||||
// // is the session it has most recently received a message on).
|
|
||||||
// val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY)
|
|
||||||
//
|
|
||||||
// val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
|
||||||
// val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
|
||||||
// sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
|
|
||||||
// Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
|
|
||||||
// val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
|
||||||
// sendToDeviceTask.execute(sendToDeviceParams)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides the list of unknown devices
|
* Provides the list of unknown devices
|
||||||
*
|
*
|
||||||
|
@ -1302,27 +1242,41 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
return "DefaultCryptoService of $userId ($deviceId)"
|
return "DefaultCryptoService of $userId ($deviceId)"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> {
|
override fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest> {
|
||||||
return cryptoStore.getOutgoingRoomKeyRequests()
|
return cryptoStore.getOutgoingRoomKeyRequests()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
|
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>> {
|
||||||
return cryptoStore.getOutgoingRoomKeyRequestsPaged()
|
return cryptoStore.getOutgoingRoomKeyRequestsPaged()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
|
|
||||||
return cryptoStore.getIncomingRoomKeyRequestsPaged()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
|
override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
|
||||||
return cryptoStore.getIncomingRoomKeyRequests()
|
return cryptoStore.getGossipingEvents()
|
||||||
|
.mapNotNull {
|
||||||
|
IncomingRoomKeyRequest.fromEvent(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> {
|
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
|
||||||
|
return cryptoStore.getGossipingEventsTrail(TrailType.IncomingKeyRequest) {
|
||||||
|
IncomingRoomKeyRequest.fromEvent(it)
|
||||||
|
?: IncomingRoomKeyRequest(localCreationTimestamp = 0L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If you registered a `GossipingRequestListener`, you will be notified of key request
|
||||||
|
* that was not accepted by the SDK. You can call back this manually to accept anyhow.
|
||||||
|
*/
|
||||||
|
override suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest) {
|
||||||
|
incomingKeyRequestManager.manuallyAcceptRoomKeyRequest(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>> {
|
||||||
return cryptoStore.getGossipingEventsTrail()
|
return cryptoStore.getGossipingEventsTrail()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getGossipingEvents(): List<Event> {
|
override fun getGossipingEvents(): List<AuditTrail> {
|
||||||
return cryptoStore.getGossipingEvents()
|
return cryptoStore.getGossipingEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1346,8 +1300,8 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
|
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members")
|
Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members")
|
||||||
callback.onFailure(failure)
|
// we probably shouldn't block sending on that (but questionable)
|
||||||
return@launch
|
// but some members won't be able to decrypt
|
||||||
}
|
}
|
||||||
|
|
||||||
val userIds = getRoomUserIds(roomId)
|
val userIds = getRoomUserIds(roomId)
|
||||||
|
|
|
@ -315,10 +315,19 @@ internal class DeviceListManager @Inject constructor(
|
||||||
} else {
|
} else {
|
||||||
Timber.v("## CRYPTO | downloadKeys() : starts")
|
Timber.v("## CRYPTO | downloadKeys() : starts")
|
||||||
val t0 = clock.epochMillis()
|
val t0 = clock.epochMillis()
|
||||||
val result = doKeyDownloadForUsers(downloadUsers)
|
try {
|
||||||
Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms")
|
val result = doKeyDownloadForUsers(downloadUsers)
|
||||||
result.also {
|
Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms")
|
||||||
it.addEntriesFromMap(stored)
|
result.also {
|
||||||
|
it.addEntriesFromMap(stored)
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.w(failure, "## CRYPTO | downloadKeys() : doKeyDownloadForUsers failed after ${clock.epochMillis() - t0} ms")
|
||||||
|
if (forceDownload) {
|
||||||
|
throw failure
|
||||||
|
} else {
|
||||||
|
stored
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.OlmSession
|
||||||
import org.matrix.olm.OlmUtility
|
import org.matrix.olm.OlmUtility
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.URLEncoder
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO)
|
private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO)
|
||||||
|
@ -331,14 +330,6 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
Timber.tag(loggerTag.value).e(e, "## createInboundSession() : removeOneTimeKeys failed")
|
Timber.tag(loggerTag.value).e(e, "## createInboundSession() : removeOneTimeKeys failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).v("## createInboundSession() : ciphertext: $ciphertext")
|
|
||||||
try {
|
|
||||||
val sha256 = olmUtility!!.sha256(URLEncoder.encode(ciphertext, "utf-8"))
|
|
||||||
Timber.tag(loggerTag.value).v("## createInboundSession() :ciphertext: SHA256: $sha256")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag(loggerTag.value).e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext")
|
|
||||||
}
|
|
||||||
|
|
||||||
val olmMessage = OlmMessage()
|
val olmMessage = OlmMessage()
|
||||||
olmMessage.mCipherText = ciphertext
|
olmMessage.mCipherText = ciphertext
|
||||||
olmMessage.mType = messageType.toLong()
|
olmMessage.mType = messageType.toLong()
|
||||||
|
@ -589,6 +580,13 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
|
|
||||||
// Inbound group session
|
// Inbound group session
|
||||||
|
|
||||||
|
sealed interface AddSessionResult {
|
||||||
|
data class Imported(val ratchetIndex: Int) : AddSessionResult
|
||||||
|
abstract class Failure : AddSessionResult
|
||||||
|
object NotImported : Failure()
|
||||||
|
data class NotImportedHigherIndex(val newIndex: Int) : Failure()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an inbound group session to the session store.
|
* Add an inbound group session to the session store.
|
||||||
*
|
*
|
||||||
|
@ -607,7 +605,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
senderKey: String,
|
senderKey: String,
|
||||||
forwardingCurve25519KeyChain: List<String>,
|
forwardingCurve25519KeyChain: List<String>,
|
||||||
keysClaimed: Map<String, String>,
|
keysClaimed: Map<String, String>,
|
||||||
exportFormat: Boolean): Boolean {
|
exportFormat: Boolean): AddSessionResult {
|
||||||
val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
|
val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
|
||||||
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
|
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
|
||||||
val existingSession = existingSessionHolder?.wrapper
|
val existingSession = existingSessionHolder?.wrapper
|
||||||
|
@ -615,7 +613,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
if (existingSession != null) {
|
if (existingSession != null) {
|
||||||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session")
|
Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session")
|
||||||
try {
|
try {
|
||||||
val existingFirstKnown = existingSession.firstKnownIndex ?: return false.also {
|
val existingFirstKnown = existingSession.firstKnownIndex ?: return AddSessionResult.NotImported.also {
|
||||||
// This is quite unexpected, could throw if native was released?
|
// This is quite unexpected, could throw if native was released?
|
||||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
|
Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
|
||||||
candidateSession.olmInboundGroupSession?.releaseSession()
|
candidateSession.olmInboundGroupSession?.releaseSession()
|
||||||
|
@ -626,12 +624,12 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
|
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
|
||||||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
|
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
|
||||||
candidateSession.olmInboundGroupSession?.releaseSession()
|
candidateSession.olmInboundGroupSession?.releaseSession()
|
||||||
return false
|
return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt())
|
||||||
}
|
}
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
|
Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
|
||||||
candidateSession.olmInboundGroupSession?.releaseSession()
|
candidateSession.olmInboundGroupSession?.releaseSession()
|
||||||
return false
|
return AddSessionResult.NotImported
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -641,19 +639,19 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
val candidateOlmInboundSession = candidateSession.olmInboundGroupSession
|
val candidateOlmInboundSession = candidateSession.olmInboundGroupSession
|
||||||
if (null == candidateOlmInboundSession) {
|
if (null == candidateOlmInboundSession) {
|
||||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>")
|
Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>")
|
||||||
return false
|
return AddSessionResult.NotImported
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (candidateOlmInboundSession.sessionIdentifier() != sessionId) {
|
if (candidateOlmInboundSession.sessionIdentifier() != sessionId) {
|
||||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
||||||
candidateOlmInboundSession.releaseSession()
|
candidateOlmInboundSession.releaseSession()
|
||||||
return false
|
return AddSessionResult.NotImported
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
candidateOlmInboundSession.releaseSession()
|
candidateOlmInboundSession.releaseSession()
|
||||||
Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed")
|
Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed")
|
||||||
return false
|
return AddSessionResult.NotImported
|
||||||
}
|
}
|
||||||
|
|
||||||
candidateSession.senderKey = senderKey
|
candidateSession.senderKey = senderKey
|
||||||
|
@ -667,7 +665,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
|
inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return AddSessionResult.Imported(candidateSession.firstKnownIndex?.toInt() ?: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -790,7 +788,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
|
|
||||||
if (timelineSet.contains(messageIndexKey)) {
|
if (timelineSet.contains(messageIndexKey)) {
|
||||||
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
|
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
|
||||||
Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason")
|
Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
package org.matrix.android.sdk.internal.crypto
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
|
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
||||||
|
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
||||||
|
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
internal class RoomEncryptorsStore @Inject constructor() {
|
internal class RoomEncryptorsStore @Inject constructor(
|
||||||
|
private val cryptoStore: IMXCryptoStore,
|
||||||
|
private val megolmEncryptionFactory: MXMegolmEncryptionFactory,
|
||||||
|
private val olmEncryptionFactory: MXOlmEncryptionFactory,
|
||||||
|
) {
|
||||||
|
|
||||||
// MXEncrypting instance for each room.
|
// MXEncrypting instance for each room.
|
||||||
private val roomEncryptors = mutableMapOf<String, IMXEncrypting>()
|
private val roomEncryptors = mutableMapOf<String, IMXEncrypting>()
|
||||||
|
@ -34,7 +43,18 @@ internal class RoomEncryptorsStore @Inject constructor() {
|
||||||
|
|
||||||
fun get(roomId: String): IMXEncrypting? {
|
fun get(roomId: String): IMXEncrypting? {
|
||||||
return synchronized(roomEncryptors) {
|
return synchronized(roomEncryptors) {
|
||||||
roomEncryptors[roomId]
|
val cache = roomEncryptors[roomId]
|
||||||
|
if (cache != null) {
|
||||||
|
return@synchronized cache
|
||||||
|
} else {
|
||||||
|
val alg: IMXEncrypting? = when (cryptoStore.getRoomAlgorithm(roomId)) {
|
||||||
|
MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId)
|
||||||
|
MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
alg?.let { roomEncryptors.put(roomId, it) }
|
||||||
|
return@synchronized alg
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
package org.matrix.android.sdk.internal.crypto.actions
|
||||||
|
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||||
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||||
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
|
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||||
import org.matrix.android.sdk.internal.crypto.RoomDecryptorProvider
|
import org.matrix.android.sdk.internal.crypto.RoomDecryptorProvider
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption
|
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
|
@ -30,12 +31,13 @@ import org.matrix.android.sdk.internal.util.time.Clock
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class MegolmSessionDataImporter @Inject constructor(
|
private val loggerTag = LoggerTag("MegolmSessionDataImporter", LoggerTag.CRYPTO)
|
||||||
private val olmDevice: MXOlmDevice,
|
|
||||||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
internal class MegolmSessionDataImporter @Inject constructor(private val olmDevice: MXOlmDevice,
|
||||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
private val roomDecryptorProvider: RoomDecryptorProvider,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||||
private val clock: Clock,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
|
private val clock: Clock,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,19 +68,23 @@ internal class MegolmSessionDataImporter @Inject constructor(
|
||||||
if (null != decrypting) {
|
if (null != decrypting) {
|
||||||
try {
|
try {
|
||||||
val sessionId = megolmSessionData.sessionId
|
val sessionId = megolmSessionData.sessionId
|
||||||
Timber.v("## importRoomKeys retrieve senderKey " + megolmSessionData.senderKey + " sessionId " + sessionId)
|
Timber.tag(loggerTag.value).v("## importRoomKeys retrieve senderKey ${megolmSessionData.senderKey} sessionId $sessionId")
|
||||||
|
|
||||||
totalNumbersOfImportedKeys++
|
totalNumbersOfImportedKeys++
|
||||||
|
|
||||||
// cancel any outstanding room key requests for this session
|
// cancel any outstanding room key requests for this session
|
||||||
val roomKeyRequestBody = RoomKeyRequestBody(
|
|
||||||
algorithm = megolmSessionData.algorithm,
|
|
||||||
roomId = megolmSessionData.roomId,
|
|
||||||
senderKey = megolmSessionData.senderKey,
|
|
||||||
sessionId = megolmSessionData.sessionId
|
|
||||||
)
|
|
||||||
|
|
||||||
outgoingGossipingRequestManager.cancelRoomKeyRequest(roomKeyRequestBody)
|
Timber.tag(loggerTag.value).d("Imported megolm session $sessionId from backup=$fromBackup in ${megolmSessionData.roomId}")
|
||||||
|
outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(
|
||||||
|
megolmSessionData.sessionId ?: "",
|
||||||
|
megolmSessionData.roomId ?: "",
|
||||||
|
megolmSessionData.senderKey ?: "",
|
||||||
|
tryOrNull {
|
||||||
|
olmInboundGroupSessionWrappers
|
||||||
|
.firstOrNull { it.olmInboundGroupSession?.sessionIdentifier() == megolmSessionData.sessionId }
|
||||||
|
?.firstKnownIndex?.toInt()
|
||||||
|
} ?: 0
|
||||||
|
)
|
||||||
|
|
||||||
// Have another go at decrypting events sent with this session
|
// Have another go at decrypting events sent with this session
|
||||||
when (decrypting) {
|
when (decrypting) {
|
||||||
|
@ -87,7 +93,7 @@ internal class MegolmSessionDataImporter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## importRoomKeys() : onNewSession failed")
|
Timber.tag(loggerTag.value).e(e, "## importRoomKeys() : onNewSession failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +115,7 @@ internal class MegolmSessionDataImporter @Inject constructor(
|
||||||
|
|
||||||
val t1 = clock.epochMillis()
|
val t1 = clock.epochMillis()
|
||||||
|
|
||||||
Timber.v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)")
|
Timber.tag(loggerTag.value).v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)")
|
||||||
|
|
||||||
return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys)
|
return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,6 @@
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms
|
package org.matrix.android.sdk.internal.crypto.algorithms
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||||
|
@ -44,23 +42,4 @@ internal interface IMXDecrypting {
|
||||||
* @param event the key event.
|
* @param event the key event.
|
||||||
*/
|
*/
|
||||||
fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {}
|
fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if we have the keys necessary to respond to a room key request
|
|
||||||
*
|
|
||||||
* @param request keyRequest
|
|
||||||
* @return true if we have the keys and could (theoretically) share
|
|
||||||
*/
|
|
||||||
fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send the response to a room key request.
|
|
||||||
*
|
|
||||||
* @param request keyRequest
|
|
||||||
*/
|
|
||||||
fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {}
|
|
||||||
|
|
||||||
fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue: String) {}
|
|
||||||
|
|
||||||
fun requestKeysForEvent(event: Event, withHeld: Boolean)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||||
|
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent
|
import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
|
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
|
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.session.StreamEventsManager
|
import org.matrix.android.sdk.internal.session.StreamEventsManager
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO)
|
private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO)
|
||||||
|
|
||||||
internal class MXMegolmDecryption(private val userId: String,
|
internal class MXMegolmDecryption(
|
||||||
private val olmDevice: MXOlmDevice,
|
private val olmDevice: MXOlmDevice,
|
||||||
private val deviceListManager: DeviceListManager,
|
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val messageEncrypter: MessageEncrypter,
|
private val liveEventManager: Lazy<StreamEventsManager>
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
) : IMXDecrypting {
|
||||||
private val cryptoStore: IMXCryptoStore,
|
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
|
||||||
private val liveEventManager: Lazy<StreamEventsManager>
|
|
||||||
) : IMXDecrypting, IMXWithHeldExtension {
|
|
||||||
|
|
||||||
var newSessionListener: NewSessionListener? = null
|
var newSessionListener: NewSessionListener? = null
|
||||||
|
|
||||||
|
@ -73,10 +54,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
|
|
||||||
@Throws(MXCryptoError::class)
|
@Throws(MXCryptoError::class)
|
||||||
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||||
// If cross signing is enabled, we don't send request until the keys are trusted
|
return decryptEvent(event, timeline, true)
|
||||||
// There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once
|
|
||||||
val requestOnFail = cryptoStore.getMyCrossSigningInfo()?.isTrusted() == true
|
|
||||||
return decryptEvent(event, timeline, requestOnFail)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(MXCryptoError::class)
|
@Throws(MXCryptoError::class)
|
||||||
|
@ -126,13 +104,14 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
if (throwable is MXCryptoError.OlmError) {
|
if (throwable is MXCryptoError.OlmError) {
|
||||||
// TODO Check the value of .message
|
// TODO Check the value of .message
|
||||||
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
|
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
|
||||||
// addEventToPendingList(event, timeline)
|
// So we know that session, but it's ratcheted and we can't decrypt at that index
|
||||||
// The session might has been partially withheld (and only pass ratcheted)
|
|
||||||
|
if (requestKeysOnFail) {
|
||||||
|
requestKeysForEvent(event)
|
||||||
|
}
|
||||||
|
// Check if partially withheld
|
||||||
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
||||||
if (withHeldInfo != null) {
|
if (withHeldInfo != null) {
|
||||||
if (requestKeysOnFail) {
|
|
||||||
requestKeysForEvent(event, true)
|
|
||||||
}
|
|
||||||
// Encapsulate as withHeld exception
|
// Encapsulate as withHeld exception
|
||||||
throw MXCryptoError.Base(
|
throw MXCryptoError.Base(
|
||||||
MXCryptoError.ErrorType.KEYS_WITHHELD,
|
MXCryptoError.ErrorType.KEYS_WITHHELD,
|
||||||
|
@ -141,10 +120,6 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestKeysOnFail) {
|
|
||||||
requestKeysForEvent(event, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw MXCryptoError.Base(
|
throw MXCryptoError.Base(
|
||||||
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
|
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
|
||||||
"UNKNOWN_MESSAGE_INDEX",
|
"UNKNOWN_MESSAGE_INDEX",
|
||||||
|
@ -162,27 +137,22 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (throwable is MXCryptoError.Base) {
|
if (throwable is MXCryptoError.Base) {
|
||||||
if (
|
if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
||||||
/** if the session is unknown*/
|
// Check if it was withheld by sender to enrich error code
|
||||||
throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
|
|
||||||
) {
|
|
||||||
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
|
||||||
if (withHeldInfo != null) {
|
if (withHeldInfo != null) {
|
||||||
if (requestKeysOnFail) {
|
if (requestKeysOnFail) {
|
||||||
requestKeysForEvent(event, true)
|
requestKeysForEvent(event)
|
||||||
}
|
}
|
||||||
// Encapsulate as withHeld exception
|
// Encapsulate as withHeld exception
|
||||||
throw MXCryptoError.Base(
|
throw MXCryptoError.Base(
|
||||||
MXCryptoError.ErrorType.KEYS_WITHHELD,
|
MXCryptoError.ErrorType.KEYS_WITHHELD,
|
||||||
withHeldInfo.code?.value ?: "",
|
withHeldInfo.code?.value ?: "",
|
||||||
withHeldInfo.reason
|
withHeldInfo.reason)
|
||||||
)
|
}
|
||||||
} else {
|
|
||||||
// This is un-used in Matrix Android SDK2, not sure if needed
|
if (requestKeysOnFail) {
|
||||||
// addEventToPendingList(event, timeline)
|
requestKeysForEvent(event)
|
||||||
if (requestKeysOnFail) {
|
|
||||||
requestKeysForEvent(event, false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,55 +168,10 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
*
|
*
|
||||||
* @param event the event
|
* @param event the event
|
||||||
*/
|
*/
|
||||||
override fun requestKeysForEvent(event: Event, withHeld: Boolean) {
|
private fun requestKeysForEvent(event: Event) {
|
||||||
val sender = event.senderId ?: return
|
outgoingKeyRequestManager.requestKeyForEvent(event, false)
|
||||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
|
||||||
val senderDevice = encryptedEventContent?.deviceId ?: return
|
|
||||||
|
|
||||||
val recipients = if (event.senderId == userId || withHeld) {
|
|
||||||
mapOf(
|
|
||||||
userId to listOf("*")
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// for the case where you share the key with a device that has a broken olm session
|
|
||||||
// The other user might Re-shares a megolm session key with devices if the key has already been
|
|
||||||
// sent to them.
|
|
||||||
mapOf(
|
|
||||||
userId to listOf("*"),
|
|
||||||
sender to listOf(senderDevice)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val requestBody = RoomKeyRequestBody(
|
|
||||||
roomId = event.roomId,
|
|
||||||
algorithm = encryptedEventContent.algorithm,
|
|
||||||
senderKey = encryptedEventContent.senderKey,
|
|
||||||
sessionId = encryptedEventContent.sessionId
|
|
||||||
)
|
|
||||||
|
|
||||||
outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Add an event to the list of those we couldn't decrypt the first time we
|
|
||||||
// * saw them.
|
|
||||||
// *
|
|
||||||
// * @param event the event to try to decrypt later
|
|
||||||
// * @param timelineId the timeline identifier
|
|
||||||
// */
|
|
||||||
// private fun addEventToPendingList(event: Event, timelineId: String) {
|
|
||||||
// val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return
|
|
||||||
// val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}"
|
|
||||||
//
|
|
||||||
// val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() }
|
|
||||||
// val events = timeline.getOrPut(timelineId) { ArrayList() }
|
|
||||||
//
|
|
||||||
// if (event !in events) {
|
|
||||||
// Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}")
|
|
||||||
// events.add(event)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a key event.
|
* Handle a key event.
|
||||||
*
|
*
|
||||||
|
@ -266,6 +191,11 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
||||||
|
if (!cryptoStore.isKeyGossipingEnabled()) {
|
||||||
|
Timber.tag(loggerTag.value)
|
||||||
|
.i("onRoomKeyEvent(), ignore forward adding as per crypto config : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
|
||||||
|
return
|
||||||
|
}
|
||||||
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
|
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
|
||||||
val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>()
|
val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>()
|
||||||
?: return
|
?: return
|
||||||
|
@ -306,7 +236,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
|
Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
|
||||||
val added = olmDevice.addInboundGroupSession(
|
val addSessionResult = olmDevice.addInboundGroupSession(
|
||||||
roomKeyContent.sessionId,
|
roomKeyContent.sessionId,
|
||||||
roomKeyContent.sessionKey,
|
roomKeyContent.sessionKey,
|
||||||
roomKeyContent.roomId,
|
roomKeyContent.roomId,
|
||||||
|
@ -316,18 +246,47 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
exportFormat
|
exportFormat
|
||||||
)
|
)
|
||||||
|
|
||||||
if (added) {
|
when (addSessionResult) {
|
||||||
|
is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex
|
||||||
|
is MXOlmDevice.AddSessionResult.NotImportedHigherIndex -> addSessionResult.newIndex
|
||||||
|
else -> null
|
||||||
|
}?.let { index ->
|
||||||
|
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
||||||
|
val fromDevice = (event.content?.get("sender_key") as? String)?.let { senderDeviceIdentityKey ->
|
||||||
|
cryptoStore.getUserDeviceList(event.senderId ?: "")
|
||||||
|
?.firstOrNull {
|
||||||
|
it.identityKey() == senderDeviceIdentityKey
|
||||||
|
}
|
||||||
|
}?.deviceId
|
||||||
|
|
||||||
|
outgoingKeyRequestManager.onRoomKeyForwarded(
|
||||||
|
sessionId = roomKeyContent.sessionId,
|
||||||
|
algorithm = roomKeyContent.algorithm ?: "",
|
||||||
|
roomId = roomKeyContent.roomId,
|
||||||
|
senderKey = senderKey,
|
||||||
|
fromIndex = index,
|
||||||
|
fromDevice = fromDevice,
|
||||||
|
event = event)
|
||||||
|
|
||||||
|
cryptoStore.saveIncomingForwardKeyAuditTrail(
|
||||||
|
roomId = roomKeyContent.roomId,
|
||||||
|
sessionId = roomKeyContent.sessionId,
|
||||||
|
senderKey = senderKey,
|
||||||
|
algorithm = roomKeyContent.algorithm ?: "",
|
||||||
|
userId = event.senderId ?: "",
|
||||||
|
deviceId = fromDevice ?: "",
|
||||||
|
chainIndex = index.toLong())
|
||||||
|
|
||||||
|
// The index is used to decide if we cancel sent request or if we wait for a better key
|
||||||
|
outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, senderKey, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addSessionResult is MXOlmDevice.AddSessionResult.Imported) {
|
||||||
|
Timber.tag(loggerTag.value)
|
||||||
|
.d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}")
|
||||||
defaultKeysBackupService.maybeBackupKeys()
|
defaultKeysBackupService.maybeBackupKeys()
|
||||||
|
|
||||||
val content = RoomKeyRequestBody(
|
|
||||||
algorithm = roomKeyContent.algorithm,
|
|
||||||
roomId = roomKeyContent.roomId,
|
|
||||||
sessionId = roomKeyContent.sessionId,
|
|
||||||
senderKey = senderKey
|
|
||||||
)
|
|
||||||
|
|
||||||
outgoingGossipingRequestManager.cancelRoomKeyRequest(content)
|
|
||||||
|
|
||||||
onNewSession(roomKeyContent.roomId, senderKey, roomKeyContent.sessionId)
|
onNewSession(roomKeyContent.roomId, senderKey, roomKeyContent.sessionId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -343,77 +302,4 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey")
|
Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey")
|
||||||
newSessionListener?.onNewSession(roomId, senderKey, sessionId)
|
newSessionListener?.onNewSession(roomId, senderKey, sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean {
|
|
||||||
val roomId = request.requestBody?.roomId ?: return false
|
|
||||||
val senderKey = request.requestBody.senderKey ?: return false
|
|
||||||
val sessionId = request.requestBody.sessionId ?: return false
|
|
||||||
return olmDevice.hasInboundSessionKeys(roomId, senderKey, sessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {
|
|
||||||
// sanity checks
|
|
||||||
if (request.requestBody == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val userId = request.userId ?: return
|
|
||||||
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
|
||||||
val body = request.requestBody
|
|
||||||
val sessionHolder = try {
|
|
||||||
olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session for request $body")
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
val export = sessionHolder.mutex.withLock {
|
|
||||||
sessionHolder.wrapper.exportKeys()
|
|
||||||
} ?: return@launch Unit.also {
|
|
||||||
Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session ${body.sessionId}")
|
|
||||||
}
|
|
||||||
|
|
||||||
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
|
|
||||||
.mapCatching {
|
|
||||||
val deviceId = request.deviceId
|
|
||||||
val deviceInfo = cryptoStore.getUserDevice(userId, deviceId ?: "")
|
|
||||||
if (deviceInfo == null) {
|
|
||||||
throw RuntimeException()
|
|
||||||
} else {
|
|
||||||
val devicesByUser = mapOf(userId to listOf(deviceInfo))
|
|
||||||
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
|
||||||
val olmSessionResult = usersDeviceMap.getObject(userId, deviceId)
|
|
||||||
if (olmSessionResult?.sessionId == null) {
|
|
||||||
// no session with this device, probably because there
|
|
||||||
// were no one-time keys.
|
|
||||||
Timber.tag(loggerTag.value).e("no session with this device $deviceId, probably because there were no one-time keys.")
|
|
||||||
return@mapCatching
|
|
||||||
}
|
|
||||||
Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sharing session ${body.sessionId} with device $userId:$deviceId")
|
|
||||||
|
|
||||||
val payloadJson = mapOf(
|
|
||||||
"type" to EventType.FORWARDED_ROOM_KEY,
|
|
||||||
"content" to export
|
|
||||||
)
|
|
||||||
|
|
||||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
|
||||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
|
||||||
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
|
||||||
Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sending ${body.sessionId} to $userId:$deviceId")
|
|
||||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
|
||||||
try {
|
|
||||||
sendToDeviceTask.execute(sendToDeviceParams)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice() : Failed to send ${body.sessionId} to $userId:$deviceId")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) {
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
|
||||||
cryptoStore.addWithHeldMegolmSession(withHeldInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,46 +17,24 @@
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||||
|
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
|
||||||
import org.matrix.android.sdk.internal.session.StreamEventsManager
|
import org.matrix.android.sdk.internal.session.StreamEventsManager
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class MXMegolmDecryptionFactory @Inject constructor(
|
internal class MXMegolmDecryptionFactory @Inject constructor(
|
||||||
@UserId private val userId: String,
|
|
||||||
private val olmDevice: MXOlmDevice,
|
private val olmDevice: MXOlmDevice,
|
||||||
private val deviceListManager: DeviceListManager,
|
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
|
||||||
private val messageEncrypter: MessageEncrypter,
|
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
|
||||||
private val eventsManager: Lazy<StreamEventsManager>
|
private val eventsManager: Lazy<StreamEventsManager>
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun create(): MXMegolmDecryption {
|
fun create(): MXMegolmDecryption {
|
||||||
return MXMegolmDecryption(
|
return MXMegolmDecryption(
|
||||||
userId,
|
|
||||||
olmDevice,
|
olmDevice,
|
||||||
deviceListManager,
|
outgoingKeyRequestManager,
|
||||||
outgoingGossipingRequestManager,
|
|
||||||
messageEncrypter,
|
|
||||||
ensureOlmSessionsForDevicesAction,
|
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
sendToDeviceTask,
|
eventsManager)
|
||||||
coroutineDispatchers,
|
|
||||||
cryptoCoroutineScope,
|
|
||||||
eventsManager
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,6 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.forEach
|
import org.matrix.android.sdk.api.session.crypto.model.forEach
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
import org.matrix.android.sdk.api.session.events.model.Content
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||||
|
@ -285,25 +284,14 @@ internal class MXMegolmEncryption(
|
||||||
// attempted to share with) rather than the contentMap (those we did
|
// attempted to share with) rather than the contentMap (those we did
|
||||||
// share with), because we don't want to try to claim a one-time-key
|
// share with), because we don't want to try to claim a one-time-key
|
||||||
// for dead devices on every message.
|
// for dead devices on every message.
|
||||||
val gossipingEventBuffer = arrayListOf<Event>()
|
for ((_, devicesToShareWith) in devicesByUser) {
|
||||||
for ((userId, devicesToShareWith) in devicesByUser) {
|
|
||||||
for (deviceInfo in devicesToShareWith) {
|
for (deviceInfo in devicesToShareWith) {
|
||||||
session.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex)
|
session.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex)
|
||||||
gossipingEventBuffer.add(
|
// XXX is it needed to add it to the audit trail?
|
||||||
Event(
|
// For now decided that no, we are more interested by forward trail
|
||||||
type = EventType.ROOM_KEY,
|
|
||||||
senderId = myUserId,
|
|
||||||
content = submap.apply {
|
|
||||||
this["session_key"] = ""
|
|
||||||
// we add a fake key for trail
|
|
||||||
this["_dest"] = "$userId|${deviceInfo.deviceId}"
|
|
||||||
}
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cryptoStore.saveGossipingEvents(gossipingEventBuffer)
|
|
||||||
|
|
||||||
if (haveTargets) {
|
if (haveTargets) {
|
||||||
t0 = clock.epochMillis()
|
t0 = clock.epochMillis()
|
||||||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target")
|
Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target")
|
||||||
|
@ -346,7 +334,8 @@ internal class MXMegolmEncryption(
|
||||||
senderKey = senderKey,
|
senderKey = senderKey,
|
||||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
codeString = code.value
|
codeString = code.value,
|
||||||
|
fromDevice = myDeviceId
|
||||||
)
|
)
|
||||||
val params = SendToDeviceTask.Params(
|
val params = SendToDeviceTask.Params(
|
||||||
EventType.ROOM_KEY_WITHHELD,
|
EventType.ROOM_KEY_WITHHELD,
|
||||||
|
|
|
@ -265,8 +265,4 @@ internal class MXOlmDecryption(
|
||||||
|
|
||||||
return res["payload"]
|
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.Optional
|
||||||
import org.matrix.android.sdk.api.util.fromBase64
|
import org.matrix.android.sdk.api.util.fromBase64
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||||
|
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
|
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
|
||||||
|
@ -70,6 +71,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val cryptoCoroutineScope: CoroutineScope,
|
private val cryptoCoroutineScope: CoroutineScope,
|
||||||
private val workManagerProvider: WorkManagerProvider,
|
private val workManagerProvider: WorkManagerProvider,
|
||||||
|
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||||
private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
|
private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
|
||||||
) : CrossSigningService,
|
) : CrossSigningService,
|
||||||
DeviceListManager.UserDevicesUpdateListener {
|
DeviceListManager.UserDevicesUpdateListener {
|
||||||
|
@ -785,7 +787,8 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
// If it's me, recheck trust of all users and devices?
|
// If it's me, recheck trust of all users and devices?
|
||||||
val users = ArrayList<String>()
|
val users = ArrayList<String>()
|
||||||
if (otherUserId == userId && currentTrust != trusted) {
|
if (otherUserId == userId && currentTrust != trusted) {
|
||||||
// reRequestAllPendingRoomKeyRequest()
|
// notify key requester
|
||||||
|
outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted)
|
||||||
cryptoStore.updateUsersTrust {
|
cryptoStore.updateUsersTrust {
|
||||||
users.add(it)
|
users.add(it)
|
||||||
checkUserTrust(it).isVerified()
|
checkUserTrust(it).isVerified()
|
||||||
|
@ -800,19 +803,4 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// private fun reRequestAllPendingRoomKeyRequest() {
|
|
||||||
// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
|
||||||
// Timber.d("## CrossSigning - reRequest pending outgoing room key requests")
|
|
||||||
// cryptoStore.getOutgoingRoomKeyRequests().forEach {
|
|
||||||
// it.requestBody?.let { requestBody ->
|
|
||||||
// if (cryptoStore.getInboundGroupSession(requestBody.sessionId ?: "", requestBody.senderKey ?: "") == null) {
|
|
||||||
// outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody)
|
|
||||||
// } else {
|
|
||||||
// outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,16 +63,11 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBack
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody
|
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
|
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteBackupTask
|
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteBackupTask
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteRoomSessionsDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteSessionsDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask
|
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask
|
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask
|
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask
|
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask
|
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask
|
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask
|
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
|
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||||
|
@ -112,16 +107,11 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
// Tasks
|
// Tasks
|
||||||
private val createKeysBackupVersionTask: CreateKeysBackupVersionTask,
|
private val createKeysBackupVersionTask: CreateKeysBackupVersionTask,
|
||||||
private val deleteBackupTask: DeleteBackupTask,
|
private val deleteBackupTask: DeleteBackupTask,
|
||||||
private val deleteRoomSessionDataTask: DeleteRoomSessionDataTask,
|
|
||||||
private val deleteRoomSessionsDataTask: DeleteRoomSessionsDataTask,
|
|
||||||
private val deleteSessionDataTask: DeleteSessionsDataTask,
|
|
||||||
private val getKeysBackupLastVersionTask: GetKeysBackupLastVersionTask,
|
private val getKeysBackupLastVersionTask: GetKeysBackupLastVersionTask,
|
||||||
private val getKeysBackupVersionTask: GetKeysBackupVersionTask,
|
private val getKeysBackupVersionTask: GetKeysBackupVersionTask,
|
||||||
private val getRoomSessionDataTask: GetRoomSessionDataTask,
|
private val getRoomSessionDataTask: GetRoomSessionDataTask,
|
||||||
private val getRoomSessionsDataTask: GetRoomSessionsDataTask,
|
private val getRoomSessionsDataTask: GetRoomSessionsDataTask,
|
||||||
private val getSessionsDataTask: GetSessionsDataTask,
|
private val getSessionsDataTask: GetSessionsDataTask,
|
||||||
private val storeRoomSessionDataTask: StoreRoomSessionDataTask,
|
|
||||||
private val storeSessionsDataTask: StoreRoomSessionsDataTask,
|
|
||||||
private val storeSessionDataTask: StoreSessionsDataTask,
|
private val storeSessionDataTask: StoreSessionsDataTask,
|
||||||
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
|
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
|
||||||
// Task executor
|
// Task executor
|
||||||
|
@ -168,58 +158,63 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
override fun prepareKeysBackupVersion(password: String?,
|
override fun prepareKeysBackupVersion(password: String?,
|
||||||
progressListener: ProgressListener?,
|
progressListener: ProgressListener?,
|
||||||
callback: MatrixCallback<MegolmBackupCreationInfo>) {
|
callback: MatrixCallback<MegolmBackupCreationInfo>) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||||
runCatching {
|
try {
|
||||||
withContext(coroutineDispatchers.crypto) {
|
val olmPkDecryption = OlmPkDecryption()
|
||||||
val olmPkDecryption = OlmPkDecryption()
|
val signalableMegolmBackupAuthData = if (password != null) {
|
||||||
val signalableMegolmBackupAuthData = if (password != null) {
|
// Generate a private key from the password
|
||||||
// Generate a private key from the password
|
val backgroundProgressListener = if (progressListener == null) {
|
||||||
val backgroundProgressListener = if (progressListener == null) {
|
null
|
||||||
null
|
} else {
|
||||||
} else {
|
object : ProgressListener {
|
||||||
object : ProgressListener {
|
override fun onProgress(progress: Int, total: Int) {
|
||||||
override fun onProgress(progress: Int, total: Int) {
|
uiHandler.post {
|
||||||
uiHandler.post {
|
try {
|
||||||
try {
|
progressListener.onProgress(progress, total)
|
||||||
progressListener.onProgress(progress, total)
|
} catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
Timber.e(e, "prepareKeysBackupVersion: onProgress failure")
|
||||||
Timber.e(e, "prepareKeysBackupVersion: onProgress failure")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener)
|
|
||||||
SignalableMegolmBackupAuthData(
|
|
||||||
publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey),
|
|
||||||
privateKeySalt = generatePrivateKeyResult.salt,
|
|
||||||
privateKeyIterations = generatePrivateKeyResult.iterations
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val publicKey = olmPkDecryption.generateKey()
|
|
||||||
|
|
||||||
SignalableMegolmBackupAuthData(
|
|
||||||
publicKey = publicKey
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary())
|
val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener)
|
||||||
|
SignalableMegolmBackupAuthData(
|
||||||
val signedMegolmBackupAuthData = MegolmBackupAuthData(
|
publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey),
|
||||||
publicKey = signalableMegolmBackupAuthData.publicKey,
|
privateKeySalt = generatePrivateKeyResult.salt,
|
||||||
privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt,
|
privateKeyIterations = generatePrivateKeyResult.iterations
|
||||||
privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations,
|
|
||||||
signatures = objectSigner.signObject(canonicalJson)
|
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
val publicKey = olmPkDecryption.generateKey()
|
||||||
|
|
||||||
MegolmBackupCreationInfo(
|
SignalableMegolmBackupAuthData(
|
||||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP,
|
publicKey = publicKey
|
||||||
authData = signedMegolmBackupAuthData,
|
|
||||||
recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey())
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.foldToCallback(callback)
|
|
||||||
|
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary())
|
||||||
|
|
||||||
|
val signedMegolmBackupAuthData = MegolmBackupAuthData(
|
||||||
|
publicKey = signalableMegolmBackupAuthData.publicKey,
|
||||||
|
privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt,
|
||||||
|
privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations,
|
||||||
|
signatures = objectSigner.signObject(canonicalJson)
|
||||||
|
)
|
||||||
|
|
||||||
|
val creationInfo = MegolmBackupCreationInfo(
|
||||||
|
algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP,
|
||||||
|
authData = signedMegolmBackupAuthData,
|
||||||
|
recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey())
|
||||||
|
)
|
||||||
|
uiHandler.post {
|
||||||
|
callback.onSuccess(creationInfo)
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
uiHandler.post {
|
||||||
|
callback.onFailure(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,41 +262,39 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteBackup(version: String, callback: MatrixCallback<Unit>?) {
|
override fun deleteBackup(version: String, callback: MatrixCallback<Unit>?) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||||
withContext(coroutineDispatchers.crypto) {
|
// If we're currently backing up to this backup... stop.
|
||||||
// If we're currently backing up to this backup... stop.
|
// (We start using it automatically in createKeysBackupVersion so this is symmetrical).
|
||||||
// (We start using it automatically in createKeysBackupVersion so this is symmetrical).
|
if (keysBackupVersion != null && version == keysBackupVersion?.version) {
|
||||||
if (keysBackupVersion != null && version == keysBackupVersion?.version) {
|
resetKeysBackupData()
|
||||||
resetKeysBackupData()
|
keysBackupVersion = null
|
||||||
keysBackupVersion = null
|
keysBackupStateManager.state = KeysBackupState.Unknown
|
||||||
keysBackupStateManager.state = KeysBackupState.Unknown
|
}
|
||||||
}
|
|
||||||
|
|
||||||
deleteBackupTask
|
deleteBackupTask
|
||||||
.configureWith(DeleteBackupTask.Params(version)) {
|
.configureWith(DeleteBackupTask.Params(version)) {
|
||||||
this.callback = object : MatrixCallback<Unit> {
|
this.callback = object : MatrixCallback<Unit> {
|
||||||
private fun eventuallyRestartBackup() {
|
private fun eventuallyRestartBackup() {
|
||||||
// Do not stay in KeysBackupState.Unknown but check what is available on the homeserver
|
// Do not stay in KeysBackupState.Unknown but check what is available on the homeserver
|
||||||
if (state == KeysBackupState.Unknown) {
|
if (state == KeysBackupState.Unknown) {
|
||||||
checkAndStartKeysBackup()
|
checkAndStartKeysBackup()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(data: Unit) {
|
|
||||||
eventuallyRestartBackup()
|
|
||||||
|
|
||||||
uiHandler.post { callback?.onSuccess(Unit) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
eventuallyRestartBackup()
|
|
||||||
|
|
||||||
uiHandler.post { callback?.onFailure(failure) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(data: Unit) {
|
||||||
|
eventuallyRestartBackup()
|
||||||
|
|
||||||
|
uiHandler.post { callback?.onSuccess(Unit) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
eventuallyRestartBackup()
|
||||||
|
|
||||||
|
uiHandler.post { callback?.onFailure(failure) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.executeBy(taskExecutor)
|
}
|
||||||
}
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -480,10 +473,11 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
|
|
||||||
if (authData == null) {
|
if (authData == null) {
|
||||||
Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data")
|
Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data")
|
||||||
|
uiHandler.post {
|
||||||
callback.onFailure(IllegalArgumentException("Missing element"))
|
callback.onFailure(IllegalArgumentException("Missing element"))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||||
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
|
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
|
||||||
// Get current signatures, or create an empty set
|
// Get current signatures, or create an empty set
|
||||||
val myUserSignatures = authData.signatures?.get(userId).orEmpty().toMutableMap()
|
val myUserSignatures = authData.signatures?.get(userId).orEmpty().toMutableMap()
|
||||||
|
@ -536,11 +530,15 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
|
|
||||||
checkAndStartWithKeysBackupVersion(newKeysBackupVersion)
|
checkAndStartWithKeysBackupVersion(newKeysBackupVersion)
|
||||||
|
|
||||||
callback.onSuccess(data)
|
uiHandler.post {
|
||||||
|
callback.onSuccess(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
override fun onFailure(failure: Throwable) {
|
||||||
callback.onFailure(failure)
|
uiHandler.post {
|
||||||
|
callback.onFailure(failure)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -554,15 +552,14 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
callback: MatrixCallback<Unit>) {
|
callback: MatrixCallback<Unit>) {
|
||||||
Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}")
|
Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}")
|
||||||
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||||
val isValid = withContext(coroutineDispatchers.crypto) {
|
val isValid = isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)
|
||||||
isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.")
|
Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.")
|
||||||
|
uiHandler.post {
|
||||||
callback.onFailure(IllegalArgumentException("Invalid recovery key or password"))
|
callback.onFailure(IllegalArgumentException("Invalid recovery key or password"))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
trustKeysBackupVersion(keysBackupVersion, true, callback)
|
trustKeysBackupVersion(keysBackupVersion, true, callback)
|
||||||
}
|
}
|
||||||
|
@ -574,15 +571,14 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
callback: MatrixCallback<Unit>) {
|
callback: MatrixCallback<Unit>) {
|
||||||
Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}")
|
Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}")
|
||||||
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||||
val recoveryKey = withContext(coroutineDispatchers.crypto) {
|
val recoveryKey = recoveryKeyFromPassword(password, keysBackupVersion, null)
|
||||||
recoveryKeyFromPassword(password, keysBackupVersion, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recoveryKey == null) {
|
if (recoveryKey == null) {
|
||||||
Timber.w("trustKeysBackupVersionWithPassphrase: Key backup is missing required data")
|
Timber.w("trustKeysBackupVersionWithPassphrase: Key backup is missing required data")
|
||||||
|
uiHandler.post {
|
||||||
callback.onFailure(IllegalArgumentException("Missing element"))
|
callback.onFailure(IllegalArgumentException("Missing element"))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check trust using the recovery key
|
// Check trust using the recovery key
|
||||||
trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback)
|
trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback)
|
||||||
|
@ -593,30 +589,28 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
override fun onSecretKeyGossip(secret: String) {
|
override fun onSecretKeyGossip(secret: String) {
|
||||||
Timber.i("## CrossSigning - onSecretKeyGossip")
|
Timber.i("## CrossSigning - onSecretKeyGossip")
|
||||||
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||||
try {
|
try {
|
||||||
when (val keysBackupLastVersionResult = getKeysBackupLastVersionTask.execute(Unit)) {
|
val keysBackupVersion = getKeysBackupLastVersionTask.execute(Unit).toKeysVersionResult()
|
||||||
KeysBackupLastVersionResult.NoKeysBackup -> {
|
?: return@launch Unit.also {
|
||||||
Timber.d("No keys backup found")
|
Timber.d("Failed to get backup last version")
|
||||||
}
|
|
||||||
is KeysBackupLastVersionResult.KeysBackup -> {
|
|
||||||
val keysBackupVersion = keysBackupLastVersionResult.keysVersionResult
|
|
||||||
val recoveryKey = computeRecoveryKey(secret.fromBase64())
|
|
||||||
if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) {
|
|
||||||
awaitCallback<Unit> {
|
|
||||||
trustKeysBackupVersion(keysBackupVersion, true, it)
|
|
||||||
}
|
|
||||||
val importResult = awaitCallback<ImportRoomKeysResult> {
|
|
||||||
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it)
|
|
||||||
}
|
|
||||||
withContext(coroutineDispatchers.crypto) {
|
|
||||||
cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
|
|
||||||
}
|
|
||||||
Timber.i("onSecretKeyGossip: Recovered keys $importResult")
|
|
||||||
} else {
|
|
||||||
Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}")
|
|
||||||
}
|
}
|
||||||
|
val recoveryKey = computeRecoveryKey(secret.fromBase64())
|
||||||
|
if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) {
|
||||||
|
awaitCallback<Unit> {
|
||||||
|
trustKeysBackupVersion(keysBackupVersion, true, it)
|
||||||
}
|
}
|
||||||
|
// we don't want to start immediately downloading all as it can take very long
|
||||||
|
|
||||||
|
// val importResult = awaitCallback<ImportRoomKeysResult> {
|
||||||
|
// restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it)
|
||||||
|
// }
|
||||||
|
withContext(coroutineDispatchers.crypto) {
|
||||||
|
cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
|
||||||
|
}
|
||||||
|
Timber.i("onSecretKeyGossip: saved valid backup key")
|
||||||
|
} else {
|
||||||
|
Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}")
|
||||||
}
|
}
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}")
|
Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}")
|
||||||
|
@ -679,9 +673,9 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
callback: MatrixCallback<ImportRoomKeysResult>) {
|
||||||
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
|
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
|
||||||
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val decryption = withContext(coroutineDispatchers.crypto) {
|
val decryption = withContext(coroutineDispatchers.computation) {
|
||||||
// Check if the recovery is valid before going any further
|
// Check if the recovery is valid before going any further
|
||||||
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
|
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
|
||||||
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
|
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
|
||||||
|
@ -754,7 +748,19 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}.foldToCallback(callback)
|
}.foldToCallback(object : MatrixCallback<ImportRoomKeysResult> {
|
||||||
|
override fun onSuccess(data: ImportRoomKeysResult) {
|
||||||
|
uiHandler.post {
|
||||||
|
callback.onSuccess(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
uiHandler.post {
|
||||||
|
callback.onFailure(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -766,7 +772,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
callback: MatrixCallback<ImportRoomKeysResult>) {
|
||||||
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
|
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
|
||||||
|
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.io) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val progressListener = if (stepProgressListener != null) {
|
val progressListener = if (stepProgressListener != null) {
|
||||||
object : ProgressListener {
|
object : ProgressListener {
|
||||||
|
@ -791,7 +797,19 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, it)
|
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.foldToCallback(callback)
|
}.foldToCallback(object : MatrixCallback<ImportRoomKeysResult> {
|
||||||
|
override fun onSuccess(data: ImportRoomKeysResult) {
|
||||||
|
uiHandler.post {
|
||||||
|
callback.onSuccess(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
uiHandler.post {
|
||||||
|
callback.onFailure(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -817,12 +835,16 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
)
|
)
|
||||||
} else if (roomId != null) {
|
} else if (roomId != null) {
|
||||||
// Get all keys for the room
|
// Get all keys for the room
|
||||||
val data = getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version))
|
val data = withContext(coroutineDispatchers.io) {
|
||||||
|
getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version))
|
||||||
|
}
|
||||||
// Convert to KeysBackupData
|
// Convert to KeysBackupData
|
||||||
KeysBackupData(mutableMapOf(roomId to data))
|
KeysBackupData(mutableMapOf(roomId to data))
|
||||||
} else {
|
} else {
|
||||||
// Get all keys
|
// Get all keys
|
||||||
getSessionsDataTask.execute(GetSessionsDataTask.Params(version))
|
withContext(coroutineDispatchers.io) {
|
||||||
|
getSessionsDataTask.execute(GetSessionsDataTask.Params(version))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec
|
||||||
import org.matrix.android.sdk.api.session.securestorage.SsssPassphrase
|
import org.matrix.android.sdk.api.session.securestorage.SsssPassphrase
|
||||||
import org.matrix.android.sdk.api.util.fromBase64
|
import org.matrix.android.sdk.api.util.fromBase64
|
||||||
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.generatePrivateKeyWithPassword
|
import org.matrix.android.sdk.internal.crypto.keysbackup.generatePrivateKeyWithPassword
|
||||||
import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256
|
import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256
|
||||||
import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption
|
import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption
|
||||||
|
@ -57,7 +57,7 @@ import kotlin.experimental.and
|
||||||
internal class DefaultSharedSecretStorageService @Inject constructor(
|
internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
private val accountDataService: SessionAccountDataService,
|
private val accountDataService: SessionAccountDataService,
|
||||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
private val secretShareManager: SecretShareManager,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val cryptoCoroutineScope: CoroutineScope
|
private val cryptoCoroutineScope: CoroutineScope
|
||||||
) : SharedSecretStorageService {
|
) : SharedSecretStorageService {
|
||||||
|
@ -380,10 +380,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||||
return IntegrityResult.Success(keyInfo.content.passphrase != null)
|
return IntegrityResult.Success(keyInfo.content.passphrase != null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun requestSecret(name: String, myOtherDeviceId: String) {
|
override suspend fun requestSecret(name: String, myOtherDeviceId: String) {
|
||||||
outgoingGossipingRequestManager.sendSecretShareRequest(
|
secretShareManager.requestSecretTo(myOtherDeviceId, name)
|
||||||
name,
|
|
||||||
mapOf(userId to listOf(myOtherDeviceId))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,23 +19,22 @@ package org.matrix.android.sdk.internal.crypto.store
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.PagedList
|
||||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.TrailType
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
|
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
|
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
|
||||||
|
@ -82,6 +81,15 @@ internal interface IMXCryptoStore {
|
||||||
*/
|
*/
|
||||||
fun setGlobalBlacklistUnverifiedDevices(block: Boolean)
|
fun setGlobalBlacklistUnverifiedDevices(block: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable key gossiping.
|
||||||
|
* Default is true.
|
||||||
|
* If set to false this device won't send key_request nor will accept key forwarded
|
||||||
|
*/
|
||||||
|
fun enableKeyGossiping(enable: Boolean)
|
||||||
|
|
||||||
|
fun isKeyGossipingEnabled(): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides the rooms ids list in which the messages are not encrypted for the unverified devices.
|
* Provides the rooms ids list in which the messages are not encrypted for the unverified devices.
|
||||||
*
|
*
|
||||||
|
@ -125,18 +133,6 @@ internal interface IMXCryptoStore {
|
||||||
*/
|
*/
|
||||||
fun getDeviceTrackingStatuses(): Map<String, Int>
|
fun getDeviceTrackingStatuses(): Map<String, Int>
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the pending IncomingRoomKeyRequest requests
|
|
||||||
*/
|
|
||||||
fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
|
||||||
|
|
||||||
fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon>
|
|
||||||
|
|
||||||
fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?)
|
|
||||||
|
|
||||||
fun storeIncomingGossipingRequests(requests: List<IncomingShareRequestCommon>)
|
|
||||||
// fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicate if the store contains data for the passed account.
|
* Indicate if the store contains data for the passed account.
|
||||||
*
|
*
|
||||||
|
@ -377,7 +373,9 @@ internal interface IMXCryptoStore {
|
||||||
* @param requestBody the request body
|
* @param requestBody the request body
|
||||||
* @return an OutgoingRoomKeyRequest instance or null
|
* @return an OutgoingRoomKeyRequest instance or null
|
||||||
*/
|
*/
|
||||||
fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest?
|
fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest?
|
||||||
|
fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest?
|
||||||
|
fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List<OutgoingKeyRequest>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look for an existing outgoing room key request, and if none is found, add a new one.
|
* Look for an existing outgoing room key request, and if none is found, add a new one.
|
||||||
|
@ -385,39 +383,59 @@ internal interface IMXCryptoStore {
|
||||||
* @param request the request
|
* @param request the request
|
||||||
* @return either the same instance as passed in, or the existing one.
|
* @return either the same instance as passed in, or the existing one.
|
||||||
*/
|
*/
|
||||||
fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>): OutgoingRoomKeyRequest?
|
fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>, fromIndex: Int): OutgoingKeyRequest
|
||||||
|
fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState)
|
||||||
|
fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int)
|
||||||
|
fun updateOutgoingRoomKeyReply(
|
||||||
|
roomId: String,
|
||||||
|
sessionId: String,
|
||||||
|
algorithm: String,
|
||||||
|
senderKey: String,
|
||||||
|
fromDevice: String?,
|
||||||
|
event: Event)
|
||||||
|
|
||||||
fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest?
|
fun deleteOutgoingRoomKeyRequest(requestId: String)
|
||||||
|
fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState)
|
||||||
|
|
||||||
fun saveGossipingEvent(event: Event) = saveGossipingEvents(listOf(event))
|
fun saveIncomingKeyRequestAuditTrail(
|
||||||
|
requestId: String,
|
||||||
|
roomId: String,
|
||||||
|
sessionId: String,
|
||||||
|
senderKey: String,
|
||||||
|
algorithm: String,
|
||||||
|
fromUser: String,
|
||||||
|
fromDevice: String
|
||||||
|
)
|
||||||
|
|
||||||
fun saveGossipingEvents(events: List<Event>)
|
fun saveWithheldAuditTrail(
|
||||||
|
roomId: String,
|
||||||
|
sessionId: String,
|
||||||
|
senderKey: String,
|
||||||
|
algorithm: String,
|
||||||
|
code: WithHeldCode,
|
||||||
|
userId: String,
|
||||||
|
deviceId: String
|
||||||
|
)
|
||||||
|
|
||||||
fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) {
|
fun saveForwardKeyAuditTrail(
|
||||||
updateGossipingRequestState(
|
roomId: String,
|
||||||
requestUserId = request.userId,
|
sessionId: String,
|
||||||
requestDeviceId = request.deviceId,
|
senderKey: String,
|
||||||
requestId = request.requestId,
|
algorithm: String,
|
||||||
state = state
|
userId: String,
|
||||||
)
|
deviceId: String,
|
||||||
}
|
chainIndex: Long?
|
||||||
|
)
|
||||||
|
|
||||||
fun updateGossipingRequestState(requestUserId: String?,
|
fun saveIncomingForwardKeyAuditTrail(
|
||||||
requestDeviceId: String?,
|
roomId: String,
|
||||||
requestId: String?,
|
sessionId: String,
|
||||||
state: GossipingRequestState)
|
senderKey: String,
|
||||||
|
algorithm: String,
|
||||||
/**
|
userId: String,
|
||||||
* Search an IncomingRoomKeyRequest
|
deviceId: String,
|
||||||
*
|
chainIndex: Long?
|
||||||
* @param userId the user id
|
)
|
||||||
* @param deviceId the device id
|
|
||||||
* @param requestId the request id
|
|
||||||
* @return an IncomingRoomKeyRequest if it exists, else null
|
|
||||||
*/
|
|
||||||
fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest?
|
|
||||||
|
|
||||||
fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState)
|
|
||||||
|
|
||||||
fun addNewSessionListener(listener: NewSessionListener)
|
fun addNewSessionListener(listener: NewSessionListener)
|
||||||
|
|
||||||
|
@ -477,17 +495,15 @@ internal interface IMXCryptoStore {
|
||||||
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
|
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
|
||||||
// Dev tools
|
// Dev tools
|
||||||
|
|
||||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest>
|
||||||
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
|
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>>
|
||||||
fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest>
|
fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>>
|
||||||
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
|
fun <T> getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData<PagedList<T>>
|
||||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
fun getGossipingEvents(): List<AuditTrail>
|
||||||
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
|
|
||||||
fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
|
|
||||||
fun getGossipingEvents(): List<Event>
|
|
||||||
|
|
||||||
fun setDeviceKeysUploaded(uploaded: Boolean)
|
fun setDeviceKeysUploaded(uploaded: Boolean)
|
||||||
fun areDeviceKeysUploaded(): Boolean
|
fun areDeviceKeysUploaded(): Boolean
|
||||||
fun tidyUpDataBase()
|
fun tidyUpDataBase()
|
||||||
fun logDbUsageInfo()
|
fun logDbUsageInfo()
|
||||||
|
fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest>
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,37 +24,40 @@ import com.zhuinden.monarchy.Monarchy
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import io.realm.Sort
|
import io.realm.Sort
|
||||||
|
import io.realm.kotlin.createObject
|
||||||
import io.realm.kotlin.where
|
import io.realm.kotlin.where
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
|
import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
import org.matrix.android.sdk.api.session.crypto.model.IncomingKeyRequestInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.TrailType
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.api.util.toOptional
|
import org.matrix.android.sdk.api.util.toOptional
|
||||||
import org.matrix.android.sdk.internal.crypto.GossipRequestType
|
|
||||||
import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
|
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
|
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
|
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntityFields
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailMapper
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
|
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper
|
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper
|
||||||
|
@ -63,10 +66,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields
|
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields
|
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
||||||
|
@ -74,8 +73,8 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSess
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields
|
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
|
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntityFields
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
|
||||||
|
@ -89,7 +88,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.query.get
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.query.getById
|
import org.matrix.android.sdk.internal.crypto.store.db.query.getById
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate
|
import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate
|
||||||
import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper
|
import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper
|
||||||
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
|
||||||
import org.matrix.android.sdk.internal.database.tools.RealmDebugTools
|
import org.matrix.android.sdk.internal.database.tools.RealmDebugTools
|
||||||
import org.matrix.android.sdk.internal.di.CryptoDatabase
|
import org.matrix.android.sdk.internal.di.CryptoDatabase
|
||||||
import org.matrix.android.sdk.internal.di.DeviceId
|
import org.matrix.android.sdk.internal.di.DeviceId
|
||||||
|
@ -106,6 +104,8 @@ import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("RealmCryptoStore", LoggerTag.CRYPTO)
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
internal class RealmCryptoStore @Inject constructor(
|
internal class RealmCryptoStore @Inject constructor(
|
||||||
@CryptoDatabase private val realmConfiguration: RealmConfiguration,
|
@CryptoDatabase private val realmConfiguration: RealmConfiguration,
|
||||||
|
@ -273,12 +273,13 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? {
|
override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? {
|
||||||
return doWithRealm(realmConfiguration) {
|
return doWithRealm(realmConfiguration) { realm ->
|
||||||
it.where<DeviceInfoEntity>()
|
realm.where<DeviceInfoEntity>()
|
||||||
.equalTo(DeviceInfoEntityFields.IDENTITY_KEY, identityKey)
|
.contains(DeviceInfoEntityFields.KEYS_MAP_JSON, identityKey)
|
||||||
.findFirst()
|
.findAll()
|
||||||
?.let { deviceInfo ->
|
.mapNotNull { CryptoMapper.mapToModel(it) }
|
||||||
CryptoMapper.mapToModel(deviceInfo)
|
.firstOrNull {
|
||||||
|
it.identityKey() == identityKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -743,14 +744,23 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
if (sessionIdentifier != null) {
|
if (sessionIdentifier != null) {
|
||||||
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey)
|
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey)
|
||||||
|
|
||||||
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
|
val existing = realm.where<OlmInboundGroupSessionEntity>()
|
||||||
primaryKey = key
|
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
|
||||||
sessionId = sessionIdentifier
|
.findFirst()
|
||||||
senderKey = session.senderKey
|
|
||||||
putInboundGroupSession(session)
|
|
||||||
}
|
|
||||||
|
|
||||||
realm.insertOrUpdate(realmOlmInboundGroupSession)
|
if (existing != null) {
|
||||||
|
// we want to keep the existing backup status
|
||||||
|
existing.putInboundGroupSession(session)
|
||||||
|
} else {
|
||||||
|
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
|
||||||
|
primaryKey = key
|
||||||
|
sessionId = sessionIdentifier
|
||||||
|
senderKey = session.senderKey
|
||||||
|
putInboundGroupSession(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
realm.insertOrUpdate(realmOlmInboundGroupSession)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -879,18 +889,33 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
doRealmTransaction(realmConfiguration) {
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
|
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
|
||||||
try {
|
try {
|
||||||
|
val sessionIdentifier = olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier()
|
||||||
val key = OlmInboundGroupSessionEntity.createPrimaryKey(
|
val key = OlmInboundGroupSessionEntity.createPrimaryKey(
|
||||||
olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier(),
|
sessionIdentifier,
|
||||||
olmInboundGroupSessionWrapper.senderKey
|
olmInboundGroupSessionWrapper.senderKey
|
||||||
)
|
)
|
||||||
|
|
||||||
it.where<OlmInboundGroupSessionEntity>()
|
val existing = realm.where<OlmInboundGroupSessionEntity>()
|
||||||
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
|
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
?.backedUp = true
|
|
||||||
|
if (existing != null) {
|
||||||
|
existing.backedUp = true
|
||||||
|
} else {
|
||||||
|
// ... might be in cache but not yet persisted, create a record to persist backedup state
|
||||||
|
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
|
||||||
|
primaryKey = key
|
||||||
|
sessionId = sessionIdentifier
|
||||||
|
senderKey = olmInboundGroupSessionWrapper.senderKey
|
||||||
|
putInboundGroupSession(olmInboundGroupSessionWrapper)
|
||||||
|
backedUp = true
|
||||||
|
}
|
||||||
|
|
||||||
|
realm.insertOrUpdate(realmOlmInboundGroupSession)
|
||||||
|
}
|
||||||
} catch (e: OlmException) {
|
} catch (e: OlmException) {
|
||||||
Timber.e(e, "OlmException")
|
Timber.e(e, "OlmException")
|
||||||
}
|
}
|
||||||
|
@ -929,6 +954,18 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun enableKeyGossiping(enable: Boolean) {
|
||||||
|
doRealmTransaction(realmConfiguration) {
|
||||||
|
it.where<CryptoMetadataEntity>().findFirst()?.globalEnableKeyGossiping = enable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isKeyGossipingEnabled(): Boolean {
|
||||||
|
return doWithRealm(realmConfiguration) {
|
||||||
|
it.where<CryptoMetadataEntity>().findFirst()?.globalEnableKeyGossiping
|
||||||
|
} ?: true
|
||||||
|
}
|
||||||
|
|
||||||
override fun getGlobalBlacklistUnverifiedDevices(): Boolean {
|
override fun getGlobalBlacklistUnverifiedDevices(): Boolean {
|
||||||
return doWithRealm(realmConfiguration) {
|
return doWithRealm(realmConfiguration) {
|
||||||
it.where<CryptoMetadataEntity>().findFirst()?.globalBlacklistUnverifiedDevices
|
it.where<CryptoMetadataEntity>().findFirst()?.globalBlacklistUnverifiedDevices
|
||||||
|
@ -1010,12 +1047,13 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
?: defaultValue
|
?: defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? {
|
override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest? {
|
||||||
return monarchy.fetchAllCopiedSync { realm ->
|
return monarchy.fetchAllCopiedSync { realm ->
|
||||||
realm.where<OutgoingGossipingRequestEntity>()
|
realm.where<OutgoingKeyRequestEntity>()
|
||||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId)
|
||||||
}.mapNotNull {
|
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId)
|
||||||
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
}.map {
|
||||||
|
it.toOutgoingKeyRequest()
|
||||||
}.firstOrNull {
|
}.firstOrNull {
|
||||||
it.requestBody?.algorithm == requestBody.algorithm &&
|
it.requestBody?.algorithm == requestBody.algorithm &&
|
||||||
it.requestBody?.roomId == requestBody.roomId &&
|
it.requestBody?.roomId == requestBody.roomId &&
|
||||||
|
@ -1024,41 +1062,37 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? {
|
override fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest? {
|
||||||
return monarchy.fetchAllCopiedSync { realm ->
|
return monarchy.fetchAllCopiedSync { realm ->
|
||||||
realm.where<OutgoingGossipingRequestEntity>()
|
realm.where<OutgoingKeyRequestEntity>()
|
||||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
|
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
|
||||||
.equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName)
|
}.map {
|
||||||
}.mapNotNull {
|
it.toOutgoingKeyRequest()
|
||||||
it.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
|
||||||
}.firstOrNull()
|
}.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
|
override fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List<OutgoingKeyRequest> {
|
||||||
|
// TODO this annoying we have to load all
|
||||||
return monarchy.fetchAllCopiedSync { realm ->
|
return monarchy.fetchAllCopiedSync { realm ->
|
||||||
realm.where<IncomingGossipingRequestEntity>()
|
realm.where<OutgoingKeyRequestEntity>()
|
||||||
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId)
|
||||||
}.mapNotNull {
|
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId)
|
||||||
it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
|
}.map {
|
||||||
|
it.toOutgoingKeyRequest()
|
||||||
|
}.filter {
|
||||||
|
it.requestBody?.algorithm == algorithm &&
|
||||||
|
it.requestBody?.senderKey == senderKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
|
override fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>> {
|
||||||
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||||
realm.where<IncomingGossipingRequestEntity>()
|
realm.where<AuditTrailEntity>().sort(AuditTrailEntityFields.AGE_LOCAL_TS, Sort.DESCENDING)
|
||||||
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
|
||||||
.sort(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Sort.DESCENDING)
|
|
||||||
}
|
}
|
||||||
val dataSourceFactory = realmDataSourceFactory.map {
|
val dataSourceFactory = realmDataSourceFactory.map {
|
||||||
it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
|
AuditTrailMapper.map(it)
|
||||||
?: IncomingRoomKeyRequest(
|
// mm we can't map not null...
|
||||||
requestBody = null,
|
?: createUnknownTrail()
|
||||||
deviceId = "",
|
|
||||||
userId = "",
|
|
||||||
requestId = "",
|
|
||||||
state = GossipingRequestState.NONE,
|
|
||||||
localCreationTimestamp = 0
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return monarchy.findAllPagedWithChanges(
|
return monarchy.findAllPagedWithChanges(
|
||||||
realmDataSourceFactory,
|
realmDataSourceFactory,
|
||||||
|
@ -1073,12 +1107,33 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> {
|
private fun createUnknownTrail() = AuditTrail(
|
||||||
|
clock.epochMillis(),
|
||||||
|
TrailType.Unknown,
|
||||||
|
IncomingKeyRequestInfo(
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun <T> getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData<PagedList<T>> {
|
||||||
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||||
realm.where<GossipingEventEntity>().sort(GossipingEventEntityFields.AGE_LOCAL_TS, Sort.DESCENDING)
|
realm.where<AuditTrailEntity>()
|
||||||
|
.equalTo(AuditTrailEntityFields.TYPE, type.name)
|
||||||
|
.sort(AuditTrailEntityFields.AGE_LOCAL_TS, Sort.DESCENDING)
|
||||||
}
|
}
|
||||||
val dataSourceFactory = realmDataSourceFactory.map { it.toModel() }
|
val dataSourceFactory = realmDataSourceFactory.map { entity ->
|
||||||
val trail = monarchy.findAllPagedWithChanges(
|
(AuditTrailMapper.map(entity)
|
||||||
|
// mm we can't map not null...
|
||||||
|
?: createUnknownTrail()
|
||||||
|
).let { mapper.invoke(it) }
|
||||||
|
}
|
||||||
|
return monarchy.findAllPagedWithChanges(
|
||||||
realmDataSourceFactory,
|
realmDataSourceFactory,
|
||||||
LivePagedListBuilder(
|
LivePagedListBuilder(
|
||||||
dataSourceFactory,
|
dataSourceFactory,
|
||||||
|
@ -1089,28 +1144,36 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return trail
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getGossipingEvents(): List<Event> {
|
override fun getGossipingEvents(): List<AuditTrail> {
|
||||||
return monarchy.fetchAllCopiedSync { realm ->
|
return monarchy.fetchAllCopiedSync { realm ->
|
||||||
realm.where<GossipingEventEntity>()
|
realm.where<AuditTrailEntity>()
|
||||||
}.map {
|
}.mapNotNull {
|
||||||
it.toModel()
|
AuditTrailMapper.map(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>): OutgoingRoomKeyRequest? {
|
override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody,
|
||||||
|
recipients: Map<String, List<String>>,
|
||||||
|
fromIndex: Int): OutgoingKeyRequest {
|
||||||
// Insert the request and return the one passed in parameter
|
// Insert the request and return the one passed in parameter
|
||||||
var request: OutgoingRoomKeyRequest? = null
|
lateinit var request: OutgoingKeyRequest
|
||||||
doRealmTransaction(realmConfiguration) { realm ->
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
|
|
||||||
val existing = realm.where<OutgoingGossipingRequestEntity>()
|
val existing = realm.where<OutgoingKeyRequestEntity>()
|
||||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId)
|
||||||
|
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId)
|
||||||
.findAll()
|
.findAll()
|
||||||
.mapNotNull {
|
.map {
|
||||||
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
it.toOutgoingKeyRequest()
|
||||||
}.firstOrNull {
|
}.also {
|
||||||
|
if (it.size > 1) {
|
||||||
|
// there should be one or zero but not more, worth warning
|
||||||
|
Timber.tag(loggerTag.value).w("There should not be more than one active key request per session")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.firstOrNull {
|
||||||
it.requestBody?.algorithm == requestBody.algorithm &&
|
it.requestBody?.algorithm == requestBody.algorithm &&
|
||||||
it.requestBody?.sessionId == requestBody.sessionId &&
|
it.requestBody?.sessionId == requestBody.sessionId &&
|
||||||
it.requestBody?.senderKey == requestBody.senderKey &&
|
it.requestBody?.senderKey == requestBody.senderKey &&
|
||||||
|
@ -1118,13 +1181,14 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply {
|
request = realm.createObject(OutgoingKeyRequestEntity::class.java).apply {
|
||||||
this.requestId = RequestIdHelper.createUniqueRequestId()
|
this.requestId = RequestIdHelper.createUniqueRequestId()
|
||||||
this.setRecipients(recipients)
|
this.setRecipients(recipients)
|
||||||
this.requestState = OutgoingGossipingRequestState.UNSENT
|
this.requestedIndex = fromIndex
|
||||||
this.type = GossipRequestType.KEY
|
this.requestState = OutgoingRoomKeyRequestState.UNSENT
|
||||||
this.requestedInfoStr = requestBody.toJson()
|
this.setRequestBody(requestBody)
|
||||||
}.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
this.creationTimeStamp = clock.epochMillis()
|
||||||
|
}.toOutgoingKeyRequest()
|
||||||
} else {
|
} else {
|
||||||
request = existing
|
request = existing
|
||||||
}
|
}
|
||||||
|
@ -1132,284 +1196,175 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest? {
|
override fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) {
|
||||||
var request: OutgoingSecretRequest? = null
|
|
||||||
|
|
||||||
// Insert the request and return the one passed in parameter
|
|
||||||
doRealmTransaction(realmConfiguration) { realm ->
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
val existing = realm.where<OutgoingGossipingRequestEntity>()
|
realm.where<OutgoingKeyRequestEntity>()
|
||||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
|
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
|
||||||
.equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName)
|
.findFirst()?.apply {
|
||||||
.findAll()
|
this.requestState = newState
|
||||||
.mapNotNull {
|
if (newState == OutgoingRoomKeyRequestState.UNSENT) {
|
||||||
it.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
// clear the old replies
|
||||||
}.firstOrNull()
|
this.replies.deleteAllFromRealm()
|
||||||
if (existing == null) {
|
|
||||||
request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply {
|
|
||||||
this.type = GossipRequestType.SECRET
|
|
||||||
setRecipients(recipients)
|
|
||||||
this.requestState = OutgoingGossipingRequestState.UNSENT
|
|
||||||
this.requestId = RequestIdHelper.createUniqueRequestId()
|
|
||||||
this.requestedInfoStr = secretName
|
|
||||||
}.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
|
||||||
} else {
|
|
||||||
request = existing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return request
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun saveGossipingEvents(events: List<Event>) {
|
|
||||||
monarchy.writeAsync { realm ->
|
|
||||||
val now = clock.epochMillis()
|
|
||||||
events.forEach { event ->
|
|
||||||
val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
|
|
||||||
val entity = GossipingEventEntity(
|
|
||||||
type = event.type,
|
|
||||||
sender = event.senderId,
|
|
||||||
ageLocalTs = ageLocalTs,
|
|
||||||
content = ContentMapper.map(event.content)
|
|
||||||
).apply {
|
|
||||||
sendState = SendState.SYNCED
|
|
||||||
decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
|
|
||||||
decryptionErrorCode = event.mCryptoError?.name
|
|
||||||
}
|
|
||||||
realm.insertOrUpdate(entity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// override fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest? {
|
|
||||||
// val statesIndex = states.map { it.ordinal }.toTypedArray()
|
|
||||||
// return doRealmQueryAndCopy(realmConfiguration) { realm ->
|
|
||||||
// realm.where<GossipingEventEntity>()
|
|
||||||
// .equalTo(GossipingEventEntityFields.SENDER, credentials.userId)
|
|
||||||
// .findAll()
|
|
||||||
// .filter {entity ->
|
|
||||||
// states.any { it == entity.requestState}
|
|
||||||
// }
|
|
||||||
// }.mapNotNull {
|
|
||||||
// ContentMapper.map(it.content)?.toModel<OutgoingSecretRequest>()
|
|
||||||
// }
|
|
||||||
// ?.toOutgoingRoomKeyRequest()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun getOutgoingSecretShareRequestByState(states: Set<ShareRequestState>): OutgoingSecretRequest? {
|
|
||||||
// val statesIndex = states.map { it.ordinal }.toTypedArray()
|
|
||||||
// return doRealmQueryAndCopy(realmConfiguration) {
|
|
||||||
// it.where<OutgoingSecretRequestEntity>()
|
|
||||||
// .`in`(OutgoingSecretRequestEntityFields.STATE, statesIndex)
|
|
||||||
// .findFirst()
|
|
||||||
// }
|
|
||||||
// ?.toOutgoingSecretRequest()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// override fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) {
|
|
||||||
// doRealmTransaction(realmConfiguration) {
|
|
||||||
// val obj = OutgoingRoomKeyRequestEntity().apply {
|
|
||||||
// requestId = request.requestId
|
|
||||||
// cancellationTxnId = request.cancellationTxnId
|
|
||||||
// state = request.state.ordinal
|
|
||||||
// putRecipients(request.recipients)
|
|
||||||
// putRequestBody(request.requestBody)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// it.insertOrUpdate(obj)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// override fun deleteOutgoingRoomKeyRequest(transactionId: String) {
|
|
||||||
// doRealmTransaction(realmConfiguration) {
|
|
||||||
// it.where<OutgoingRoomKeyRequestEntity>()
|
|
||||||
// .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_ID, transactionId)
|
|
||||||
// .findFirst()
|
|
||||||
// ?.deleteFromRealm()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// override fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) {
|
|
||||||
// if (incomingRoomKeyRequest == null) {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// doRealmTransaction(realmConfiguration) {
|
|
||||||
// // Delete any previous store request with the same parameters
|
|
||||||
// it.where<IncomingRoomKeyRequestEntity>()
|
|
||||||
// .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId)
|
|
||||||
// .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId)
|
|
||||||
// .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId)
|
|
||||||
// .findAll()
|
|
||||||
// .deleteAllFromRealm()
|
|
||||||
//
|
|
||||||
// // Then store it
|
|
||||||
// it.createObject(IncomingRoomKeyRequestEntity::class.java).apply {
|
|
||||||
// userId = incomingRoomKeyRequest.userId
|
|
||||||
// deviceId = incomingRoomKeyRequest.deviceId
|
|
||||||
// requestId = incomingRoomKeyRequest.requestId
|
|
||||||
// putRequestBody(incomingRoomKeyRequest.requestBody)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// override fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingShareRequestCommon) {
|
|
||||||
// doRealmTransaction(realmConfiguration) {
|
|
||||||
// it.where<GossipingEventEntity>()
|
|
||||||
// .equalTo(GossipingEventEntityFields.TYPE, EventType.ROOM_KEY_REQUEST)
|
|
||||||
// .notEqualTo(GossipingEventEntityFields.SENDER, credentials.userId)
|
|
||||||
// .findAll()
|
|
||||||
// .filter {
|
|
||||||
// ContentMapper.map(it.content).toModel<IncomingRoomKeyRequest>()?.let {
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// // .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId)
|
|
||||||
// // .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId)
|
|
||||||
// // .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId)
|
|
||||||
// // .findAll()
|
|
||||||
// // .deleteAllFromRealm()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
override fun updateGossipingRequestState(requestUserId: String?,
|
|
||||||
requestDeviceId: String?,
|
|
||||||
requestId: String?,
|
|
||||||
state: GossipingRequestState) {
|
|
||||||
doRealmTransaction(realmConfiguration) { realm ->
|
|
||||||
realm.where<IncomingGossipingRequestEntity>()
|
|
||||||
.equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, requestUserId)
|
|
||||||
.equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, requestDeviceId)
|
|
||||||
.equalTo(IncomingGossipingRequestEntityFields.REQUEST_ID, requestId)
|
|
||||||
.findAll().forEach {
|
|
||||||
it.requestState = state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) {
|
|
||||||
doRealmTransaction(realmConfiguration) { realm ->
|
|
||||||
realm.where<OutgoingGossipingRequestEntity>()
|
|
||||||
.equalTo(OutgoingGossipingRequestEntityFields.REQUEST_ID, requestId)
|
|
||||||
.findAll().forEach {
|
|
||||||
it.requestState = state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? {
|
|
||||||
return doWithRealm(realmConfiguration) { realm ->
|
|
||||||
realm.where<IncomingGossipingRequestEntity>()
|
|
||||||
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
|
||||||
.equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, deviceId)
|
|
||||||
.equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, userId)
|
|
||||||
.findAll()
|
|
||||||
.mapNotNull { entity ->
|
|
||||||
entity.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
|
|
||||||
}
|
|
||||||
.firstOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
|
|
||||||
return doWithRealm(realmConfiguration) {
|
|
||||||
it.where<IncomingGossipingRequestEntity>()
|
|
||||||
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
|
||||||
.equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name)
|
|
||||||
.findAll()
|
|
||||||
.map { entity ->
|
|
||||||
IncomingRoomKeyRequest(
|
|
||||||
userId = entity.otherUserId,
|
|
||||||
deviceId = entity.otherDeviceId,
|
|
||||||
requestId = entity.requestId,
|
|
||||||
requestBody = entity.getRequestedKeyInfo(),
|
|
||||||
localCreationTimestamp = entity.localCreationTimestamp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon> {
|
|
||||||
return doWithRealm(realmConfiguration) {
|
|
||||||
it.where<IncomingGossipingRequestEntity>()
|
|
||||||
.equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name)
|
|
||||||
.findAll()
|
|
||||||
.mapNotNull { entity ->
|
|
||||||
when (entity.type) {
|
|
||||||
GossipRequestType.KEY -> {
|
|
||||||
IncomingRoomKeyRequest(
|
|
||||||
userId = entity.otherUserId,
|
|
||||||
deviceId = entity.otherDeviceId,
|
|
||||||
requestId = entity.requestId,
|
|
||||||
requestBody = entity.getRequestedKeyInfo(),
|
|
||||||
localCreationTimestamp = entity.localCreationTimestamp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
GossipRequestType.SECRET -> {
|
|
||||||
IncomingSecretShareRequest(
|
|
||||||
userId = entity.otherUserId,
|
|
||||||
deviceId = entity.otherDeviceId,
|
|
||||||
requestId = entity.requestId,
|
|
||||||
secretName = entity.getRequestedSecretName(),
|
|
||||||
localCreationTimestamp = entity.localCreationTimestamp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) {
|
override fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) {
|
||||||
doRealmTransactionAsync(realmConfiguration) { realm ->
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
|
realm.where<OutgoingKeyRequestEntity>()
|
||||||
// After a clear cache, we might have a
|
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
|
||||||
|
.findFirst()?.apply {
|
||||||
realm.createObject(IncomingGossipingRequestEntity::class.java).let {
|
this.requestedIndex = newIndex
|
||||||
it.otherDeviceId = request.deviceId
|
|
||||||
it.otherUserId = request.userId
|
|
||||||
it.requestId = request.requestId ?: ""
|
|
||||||
it.requestState = GossipingRequestState.PENDING
|
|
||||||
it.localCreationTimestamp = ageLocalTS ?: clock.epochMillis()
|
|
||||||
if (request is IncomingSecretShareRequest) {
|
|
||||||
it.type = GossipRequestType.SECRET
|
|
||||||
it.requestedInfoStr = request.secretName
|
|
||||||
} else if (request is IncomingRoomKeyRequest) {
|
|
||||||
it.type = GossipRequestType.KEY
|
|
||||||
it.requestedInfoStr = request.requestBody?.toJson()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun storeIncomingGossipingRequests(requests: List<IncomingShareRequestCommon>) {
|
|
||||||
doRealmTransactionAsync(realmConfiguration) { realm ->
|
|
||||||
requests.forEach { request ->
|
|
||||||
// After a clear cache, we might have a
|
|
||||||
realm.createObject(IncomingGossipingRequestEntity::class.java).let {
|
|
||||||
it.otherDeviceId = request.deviceId
|
|
||||||
it.otherUserId = request.userId
|
|
||||||
it.requestId = request.requestId ?: ""
|
|
||||||
it.requestState = GossipingRequestState.PENDING
|
|
||||||
it.localCreationTimestamp = request.localCreationTimestamp ?: clock.epochMillis()
|
|
||||||
if (request is IncomingSecretShareRequest) {
|
|
||||||
it.type = GossipRequestType.SECRET
|
|
||||||
it.requestedInfoStr = request.secretName
|
|
||||||
} else if (request is IncomingRoomKeyRequest) {
|
|
||||||
it.type = GossipRequestType.KEY
|
|
||||||
it.requestedInfoStr = request.requestBody?.toJson()
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateOutgoingRoomKeyReply(roomId: String,
|
||||||
|
sessionId: String,
|
||||||
|
algorithm: String,
|
||||||
|
senderKey: String,
|
||||||
|
fromDevice: String?,
|
||||||
|
event: Event) {
|
||||||
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
|
realm.where<OutgoingKeyRequestEntity>()
|
||||||
|
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId)
|
||||||
|
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId)
|
||||||
|
.findAll().firstOrNull { entity ->
|
||||||
|
entity.toOutgoingKeyRequest().let {
|
||||||
|
it.requestBody?.senderKey == senderKey &&
|
||||||
|
it.requestBody?.algorithm == algorithm
|
||||||
|
}
|
||||||
|
}?.apply {
|
||||||
|
event.senderId?.let { addReply(it, fromDevice, event) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteOutgoingRoomKeyRequest(requestId: String) {
|
||||||
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
|
realm.where<OutgoingKeyRequestEntity>()
|
||||||
|
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
|
||||||
|
.findFirst()?.deleteOnCascade()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) {
|
||||||
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
|
realm.where<OutgoingKeyRequestEntity>()
|
||||||
|
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, state.name)
|
||||||
|
.findAll()
|
||||||
|
// I delete like this because I want to cascade delete replies?
|
||||||
|
.onEach { it.deleteOnCascade() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveIncomingKeyRequestAuditTrail(
|
||||||
|
requestId: String,
|
||||||
|
roomId: String,
|
||||||
|
sessionId: String,
|
||||||
|
senderKey: String,
|
||||||
|
algorithm: String,
|
||||||
|
fromUser: String,
|
||||||
|
fromDevice: String) {
|
||||||
|
monarchy.writeAsync { realm ->
|
||||||
|
val now = clock.epochMillis()
|
||||||
|
realm.createObject<AuditTrailEntity>().apply {
|
||||||
|
this.ageLocalTs = now
|
||||||
|
this.type = TrailType.IncomingKeyRequest.name
|
||||||
|
val info = IncomingKeyRequestInfo(
|
||||||
|
roomId = roomId,
|
||||||
|
sessionId = sessionId,
|
||||||
|
senderKey = senderKey,
|
||||||
|
alg = algorithm,
|
||||||
|
userId = fromUser,
|
||||||
|
deviceId = fromDevice,
|
||||||
|
requestId = requestId
|
||||||
|
)
|
||||||
|
MoshiProvider.providesMoshi().adapter(IncomingKeyRequestInfo::class.java).toJson(info)?.let {
|
||||||
|
this.contentJson = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// override fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest> {
|
override fun saveWithheldAuditTrail(roomId: String,
|
||||||
// return doRealmQueryAndCopyList(realmConfiguration) {
|
sessionId: String,
|
||||||
// it.where<GossipingEventEntity>()
|
senderKey: String,
|
||||||
// .findAll()
|
algorithm: String,
|
||||||
// }.map {
|
code: WithHeldCode,
|
||||||
// it.toIncomingSecretShareRequest()
|
userId: String,
|
||||||
// }
|
deviceId: String) {
|
||||||
// }
|
monarchy.writeAsync { realm ->
|
||||||
|
val now = clock.epochMillis()
|
||||||
|
realm.createObject<AuditTrailEntity>().apply {
|
||||||
|
this.ageLocalTs = now
|
||||||
|
this.type = TrailType.OutgoingKeyWithheld.name
|
||||||
|
val info = WithheldInfo(
|
||||||
|
roomId = roomId,
|
||||||
|
sessionId = sessionId,
|
||||||
|
senderKey = senderKey,
|
||||||
|
alg = algorithm,
|
||||||
|
code = code,
|
||||||
|
userId = userId,
|
||||||
|
deviceId = deviceId
|
||||||
|
)
|
||||||
|
MoshiProvider.providesMoshi().adapter(WithheldInfo::class.java).toJson(info)?.let {
|
||||||
|
this.contentJson = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveForwardKeyAuditTrail(roomId: String,
|
||||||
|
sessionId: String,
|
||||||
|
senderKey: String,
|
||||||
|
algorithm: String,
|
||||||
|
userId: String,
|
||||||
|
deviceId: String,
|
||||||
|
chainIndex: Long?) {
|
||||||
|
saveForwardKeyTrail(roomId, sessionId, senderKey, algorithm, userId, deviceId, chainIndex, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveIncomingForwardKeyAuditTrail(roomId: String,
|
||||||
|
sessionId: String,
|
||||||
|
senderKey: String,
|
||||||
|
algorithm: String,
|
||||||
|
userId: String,
|
||||||
|
deviceId: String,
|
||||||
|
chainIndex: Long?) {
|
||||||
|
saveForwardKeyTrail(roomId, sessionId, senderKey, algorithm, userId, deviceId, chainIndex, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveForwardKeyTrail(roomId: String,
|
||||||
|
sessionId: String,
|
||||||
|
senderKey: String,
|
||||||
|
algorithm: String,
|
||||||
|
userId: String,
|
||||||
|
deviceId: String,
|
||||||
|
chainIndex: Long?,
|
||||||
|
incoming: Boolean
|
||||||
|
) {
|
||||||
|
monarchy.writeAsync { realm ->
|
||||||
|
val now = clock.epochMillis()
|
||||||
|
realm.createObject<AuditTrailEntity>().apply {
|
||||||
|
this.ageLocalTs = now
|
||||||
|
this.type = if (incoming) TrailType.IncomingKeyForward.name else TrailType.OutgoingKeyForward.name
|
||||||
|
val info = ForwardInfo(
|
||||||
|
roomId = roomId,
|
||||||
|
sessionId = sessionId,
|
||||||
|
senderKey = senderKey,
|
||||||
|
alg = algorithm,
|
||||||
|
userId = userId,
|
||||||
|
deviceId = deviceId,
|
||||||
|
chainIndex = chainIndex
|
||||||
|
)
|
||||||
|
MoshiProvider.providesMoshi().adapter(ForwardInfo::class.java).toJson(info)?.let {
|
||||||
|
this.contentJson = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
* Cross Signing
|
* Cross Signing
|
||||||
|
@ -1513,37 +1468,34 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> {
|
override fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest> {
|
||||||
return monarchy.fetchAllMappedSync({ realm ->
|
return monarchy.fetchAllMappedSync({ realm ->
|
||||||
realm
|
realm
|
||||||
.where(OutgoingGossipingRequestEntity::class.java)
|
.where(OutgoingKeyRequestEntity::class.java)
|
||||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
|
||||||
}, { entity ->
|
}, { entity ->
|
||||||
entity.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
entity.toOutgoingKeyRequest()
|
||||||
})
|
})
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest> {
|
override fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest> {
|
||||||
return monarchy.fetchAllMappedSync({ realm ->
|
return monarchy.fetchAllMappedSync({ realm ->
|
||||||
realm
|
realm
|
||||||
.where(OutgoingGossipingRequestEntity::class.java)
|
.where(OutgoingKeyRequestEntity::class.java)
|
||||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
|
.`in`(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, inStates.map { it.name }.toTypedArray())
|
||||||
}, { entity ->
|
}, { entity ->
|
||||||
entity.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
entity.toOutgoingKeyRequest()
|
||||||
})
|
})
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
|
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>> {
|
||||||
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||||
realm
|
realm
|
||||||
.where(OutgoingGossipingRequestEntity::class.java)
|
.where(OutgoingKeyRequestEntity::class.java)
|
||||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
|
||||||
}
|
}
|
||||||
val dataSourceFactory = realmDataSourceFactory.map {
|
val dataSourceFactory = realmDataSourceFactory.map {
|
||||||
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
it.toOutgoingKeyRequest()
|
||||||
?: OutgoingRoomKeyRequest(requestBody = null, requestId = "?", recipients = emptyMap(), state = OutgoingGossipingRequestState.CANCELLED)
|
|
||||||
}
|
}
|
||||||
val trail = monarchy.findAllPagedWithChanges(
|
val trail = monarchy.findAllPagedWithChanges(
|
||||||
realmDataSourceFactory,
|
realmDataSourceFactory,
|
||||||
|
@ -1722,26 +1674,20 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000
|
val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000
|
||||||
doRealmTransaction(realmConfiguration) { realm ->
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
|
|
||||||
// Only keep one week history
|
// Clean the old ones?
|
||||||
realm.where<IncomingGossipingRequestEntity>()
|
realm.where<OutgoingKeyRequestEntity>()
|
||||||
.lessThan(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, prevWeekTs)
|
.lessThan(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, prevWeekTs)
|
||||||
.findAll()
|
.findAll()
|
||||||
.also { Timber.i("## Crypto Clean up ${it.size} IncomingGossipingRequestEntity") }
|
.also { Timber.i("## Crypto Clean up ${it.size} OutgoingKeyRequestEntity") }
|
||||||
.deleteAllFromRealm()
|
.deleteAllFromRealm()
|
||||||
|
|
||||||
// Clean the cancelled ones?
|
// Only keep one month history
|
||||||
realm.where<OutgoingGossipingRequestEntity>()
|
|
||||||
.equalTo(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, OutgoingGossipingRequestState.CANCELLED.name)
|
|
||||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
|
||||||
.findAll()
|
|
||||||
.also { Timber.i("## Crypto Clean up ${it.size} OutgoingGossipingRequestEntity") }
|
|
||||||
.deleteAllFromRealm()
|
|
||||||
|
|
||||||
// Only keep one week history
|
val prevMonthTs = clock.epochMillis() - 4 * 7 * 24 * 60 * 60 * 1_000L
|
||||||
realm.where<GossipingEventEntity>()
|
realm.where<AuditTrailEntity>()
|
||||||
.lessThan(GossipingEventEntityFields.AGE_LOCAL_TS, prevWeekTs)
|
.lessThan(AuditTrailEntityFields.AGE_LOCAL_TS, prevMonthTs)
|
||||||
.findAll()
|
.findAll()
|
||||||
.also { Timber.i("## Crypto Clean up ${it.size} GossipingEventEntityFields") }
|
.also { Timber.i("## Crypto Clean up ${it.size} AuditTrailEntity") }
|
||||||
.deleteAllFromRealm()
|
.deleteAllFromRealm()
|
||||||
|
|
||||||
// Can we do something for WithHeldSessionEntity?
|
// Can we do something for WithHeldSessionEntity?
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo013
|
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo013
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014
|
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015
|
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
import org.matrix.android.sdk.internal.util.time.Clock
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -50,7 +51,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(
|
||||||
// 0, 1, 2: legacy Riot-Android
|
// 0, 1, 2: legacy Riot-Android
|
||||||
// 3: migrate to RiotX schema
|
// 3: migrate to RiotX schema
|
||||||
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
|
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
|
||||||
val schemaVersion = 15L
|
val schemaVersion = 16L
|
||||||
|
|
||||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||||
Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion")
|
Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion")
|
||||||
|
@ -70,5 +71,6 @@ internal class RealmCryptoStoreMigration @Inject constructor(
|
||||||
if (oldVersion < 13) MigrateCryptoTo013(realm).perform()
|
if (oldVersion < 13) MigrateCryptoTo013(realm).perform()
|
||||||
if (oldVersion < 14) MigrateCryptoTo014(realm).perform()
|
if (oldVersion < 14) MigrateCryptoTo014(realm).perform()
|
||||||
if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
|
if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
|
||||||
|
if (oldVersion < 16) MigrateCryptoTo016(realm).perform()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,19 +17,19 @@
|
||||||
package org.matrix.android.sdk.internal.crypto.store.db
|
package org.matrix.android.sdk.internal.crypto.store.db
|
||||||
|
|
||||||
import io.realm.annotations.RealmModule
|
import io.realm.annotations.RealmModule
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntity
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.db.model.KeyRequestReplyEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
|
||||||
|
@ -51,9 +51,9 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEnti
|
||||||
KeyInfoEntity::class,
|
KeyInfoEntity::class,
|
||||||
CrossSigningInfoEntity::class,
|
CrossSigningInfoEntity::class,
|
||||||
TrustLevelEntity::class,
|
TrustLevelEntity::class,
|
||||||
GossipingEventEntity::class,
|
AuditTrailEntity::class,
|
||||||
IncomingGossipingRequestEntity::class,
|
OutgoingKeyRequestEntity::class,
|
||||||
OutgoingGossipingRequestEntity::class,
|
KeyRequestReplyEntity::class,
|
||||||
MyDeviceLastSeenInfoEntity::class,
|
MyDeviceLastSeenInfoEntity::class,
|
||||||
WithHeldSessionEntity::class,
|
WithHeldSessionEntity::class,
|
||||||
SharedSessionEntity::class,
|
SharedSessionEntity::class,
|
||||||
|
|
|
@ -17,9 +17,6 @@
|
||||||
package org.matrix.android.sdk.internal.crypto.store.db.migration
|
package org.matrix.android.sdk.internal.crypto.store.db.migration
|
||||||
|
|
||||||
import io.realm.DynamicRealm
|
import io.realm.DynamicRealm
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
|
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
|
|
||||||
import org.matrix.android.sdk.internal.util.database.RealmMigrator
|
import org.matrix.android.sdk.internal.util.database.RealmMigrator
|
||||||
|
|
||||||
internal class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) {
|
internal class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) {
|
||||||
|
@ -29,38 +26,37 @@ internal class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5)
|
||||||
realm.schema.remove("IncomingRoomKeyRequestEntity")
|
realm.schema.remove("IncomingRoomKeyRequestEntity")
|
||||||
|
|
||||||
// Not need to migrate existing request, just start fresh?
|
// Not need to migrate existing request, just start fresh?
|
||||||
|
|
||||||
realm.schema.create("GossipingEventEntity")
|
realm.schema.create("GossipingEventEntity")
|
||||||
.addField(GossipingEventEntityFields.TYPE, String::class.java)
|
.addField("type", String::class.java)
|
||||||
.addIndex(GossipingEventEntityFields.TYPE)
|
.addIndex("type")
|
||||||
.addField(GossipingEventEntityFields.CONTENT, String::class.java)
|
.addField("content", String::class.java)
|
||||||
.addField(GossipingEventEntityFields.SENDER, String::class.java)
|
.addField("sender", String::class.java)
|
||||||
.addIndex(GossipingEventEntityFields.SENDER)
|
.addIndex("sender")
|
||||||
.addField(GossipingEventEntityFields.DECRYPTION_RESULT_JSON, String::class.java)
|
.addField("decryptionResultJson", String::class.java)
|
||||||
.addField(GossipingEventEntityFields.DECRYPTION_ERROR_CODE, String::class.java)
|
.addField("decryptionErrorCode", String::class.java)
|
||||||
.addField(GossipingEventEntityFields.AGE_LOCAL_TS, Long::class.java)
|
.addField("ageLocalTs", Long::class.java)
|
||||||
.setNullable(GossipingEventEntityFields.AGE_LOCAL_TS, true)
|
.setNullable("ageLocalTs", true)
|
||||||
.addField(GossipingEventEntityFields.SEND_STATE_STR, String::class.java)
|
.addField("sendStateStr", String::class.java)
|
||||||
|
|
||||||
realm.schema.create("IncomingGossipingRequestEntity")
|
realm.schema.create("IncomingGossipingRequestEntity")
|
||||||
.addField(IncomingGossipingRequestEntityFields.REQUEST_ID, String::class.java)
|
.addField("requestId", String::class.java)
|
||||||
.addIndex(IncomingGossipingRequestEntityFields.REQUEST_ID)
|
.addIndex("requestId")
|
||||||
.addField(IncomingGossipingRequestEntityFields.TYPE_STR, String::class.java)
|
.addField("typeStr", String::class.java)
|
||||||
.addIndex(IncomingGossipingRequestEntityFields.TYPE_STR)
|
.addIndex("typeStr")
|
||||||
.addField(IncomingGossipingRequestEntityFields.OTHER_USER_ID, String::class.java)
|
.addField("otherUserId", String::class.java)
|
||||||
.addField(IncomingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java)
|
.addField("requestedInfoStr", String::class.java)
|
||||||
.addField(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, String::class.java)
|
.addField("otherDeviceId", String::class.java)
|
||||||
.addField(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java)
|
.addField("requestStateStr", String::class.java)
|
||||||
.addField(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Long::class.java)
|
.addField("localCreationTimestamp", Long::class.java)
|
||||||
.setNullable(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, true)
|
.setNullable("localCreationTimestamp", true)
|
||||||
|
|
||||||
realm.schema.create("OutgoingGossipingRequestEntity")
|
realm.schema.create("OutgoingGossipingRequestEntity")
|
||||||
.addField(OutgoingGossipingRequestEntityFields.REQUEST_ID, String::class.java)
|
.addField("requestId", String::class.java)
|
||||||
.addIndex(OutgoingGossipingRequestEntityFields.REQUEST_ID)
|
.addIndex("requestId")
|
||||||
.addField(OutgoingGossipingRequestEntityFields.RECIPIENTS_DATA, String::class.java)
|
.addField("recipientsData", String::class.java)
|
||||||
.addField(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, String::class.java)
|
.addField("requestedInfoStr", String::class.java)
|
||||||
.addField(OutgoingGossipingRequestEntityFields.TYPE_STR, String::class.java)
|
.addField("typeStr", String::class.java)
|
||||||
.addIndex(OutgoingGossipingRequestEntityFields.TYPE_STR)
|
.addIndex("typeStr")
|
||||||
.addField(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, String::class.java)
|
.addField("requestStateStr", String::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -14,18 +14,15 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.matrix.android.sdk.api.session.crypto.model
|
package org.matrix.android.sdk.internal.crypto.store.db.model
|
||||||
|
|
||||||
enum class GossipingRequestState {
|
import io.realm.RealmObject
|
||||||
NONE,
|
import io.realm.annotations.Index
|
||||||
PENDING,
|
|
||||||
REJECTED,
|
|
||||||
ACCEPTING,
|
|
||||||
ACCEPTED,
|
|
||||||
FAILED_TO_ACCEPTED,
|
|
||||||
|
|
||||||
// USER_REJECTED,
|
internal open class AuditTrailEntity(
|
||||||
UNABLE_TO_PROCESS,
|
var ageLocalTs: Long? = null,
|
||||||
CANCELLED_BY_REQUESTER,
|
@Index var type: String? = null,
|
||||||
RE_REQUESTED
|
var contentJson: String? = null
|
||||||
|
) : RealmObject() {
|
||||||
|
companion object
|
||||||
}
|
}
|
|
@ -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,
|
var deviceSyncToken: String? = null,
|
||||||
// Settings for blacklisting unverified devices.
|
// Settings for blacklisting unverified devices.
|
||||||
var globalBlacklistUnverifiedDevices: Boolean = false,
|
var globalBlacklistUnverifiedDevices: Boolean = false,
|
||||||
|
// setting to enable or disable key gossiping
|
||||||
|
var globalEnableKeyGossiping: Boolean = true,
|
||||||
// The keys backup version currently used. Null means no backup.
|
// The keys backup version currently used. Null means no backup.
|
||||||
var backupVersion: String? = null,
|
var backupVersion: String? = null,
|
||||||
|
|
||||||
|
|
|
@ -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
|
package org.matrix.android.sdk.internal.crypto.tasks
|
||||||
|
|
||||||
|
import dagger.Lazy
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
|
import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
|
||||||
|
@ -39,7 +40,7 @@ internal interface EncryptEventTask : Task<EncryptEventTask.Params, Event> {
|
||||||
|
|
||||||
internal class DefaultEncryptEventTask @Inject constructor(
|
internal class DefaultEncryptEventTask @Inject constructor(
|
||||||
private val localEchoRepository: LocalEchoRepository,
|
private val localEchoRepository: LocalEchoRepository,
|
||||||
private val cryptoService: CryptoService
|
private val cryptoService: Lazy<CryptoService>
|
||||||
) : EncryptEventTask {
|
) : EncryptEventTask {
|
||||||
override suspend fun execute(params: EncryptEventTask.Params): Event {
|
override suspend fun execute(params: EncryptEventTask.Params): Event {
|
||||||
// don't want to wait for any query
|
// don't want to wait for any query
|
||||||
|
@ -59,7 +60,7 @@ internal class DefaultEncryptEventTask @Inject constructor(
|
||||||
// try {
|
// try {
|
||||||
// let it throws
|
// let it throws
|
||||||
awaitCallback<MXEncryptEventContentResult> {
|
awaitCallback<MXEncryptEventContentResult> {
|
||||||
cryptoService.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
|
cryptoService.get().encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
|
||||||
}.let { result ->
|
}.let { result ->
|
||||||
val modifiedContent = HashMap(result.eventContent)
|
val modifiedContent = HashMap(result.eventContent)
|
||||||
params.keepKeys?.forEach { toKeep ->
|
params.keepKeys?.forEach { toKeep ->
|
||||||
|
@ -80,7 +81,7 @@ internal class DefaultEncryptEventTask @Inject constructor(
|
||||||
).toContent(),
|
).toContent(),
|
||||||
forwardingCurve25519KeyChain = emptyList(),
|
forwardingCurve25519KeyChain = emptyList(),
|
||||||
senderCurve25519Key = result.eventContent["sender_key"] as? String,
|
senderCurve25519Key = result.eventContent["sender_key"] as? String,
|
||||||
claimedEd25519Key = cryptoService.getMyDevice().fingerprint()
|
claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package org.matrix.android.sdk.internal.crypto.tasks
|
package org.matrix.android.sdk.internal.crypto.tasks
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||||
|
@ -46,7 +47,9 @@ internal class DefaultSendEventTask @Inject constructor(
|
||||||
params.event.roomId
|
params.event.roomId
|
||||||
?.takeIf { params.encrypt }
|
?.takeIf { params.encrypt }
|
||||||
?.let { roomId ->
|
?.let { roomId ->
|
||||||
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
|
tryOrNull {
|
||||||
|
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val event = handleEncryption(params)
|
val event = handleEncryption(params)
|
||||||
|
|
|
@ -47,7 +47,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
|
||||||
localEchoRepository.updateSendState(localId, event.roomId, SendState.SENDING)
|
localEchoRepository.updateSendState(localId, event.roomId, SendState.SENDING)
|
||||||
val response = executeRequest(globalErrorReceiver) {
|
val response = executeRequest(globalErrorReceiver) {
|
||||||
roomAPI.send(
|
roomAPI.send(
|
||||||
localId,
|
txId = localId,
|
||||||
roomId = event.roomId ?: "",
|
roomId = event.roomId ?: "",
|
||||||
content = event.content,
|
content = event.content,
|
||||||
eventType = event.type ?: ""
|
eventType = event.type ?: ""
|
||||||
|
|
|
@ -23,8 +23,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerific
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasMode
|
import org.matrix.android.sdk.api.session.crypto.verification.SasMode
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
|
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -35,8 +35,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
|
||||||
override val deviceId: String?,
|
override val deviceId: String?,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
crossSigningService: CrossSigningService,
|
crossSigningService: CrossSigningService,
|
||||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||||
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
secretShareManager: SecretShareManager,
|
||||||
deviceFingerprint: String,
|
deviceFingerprint: String,
|
||||||
transactionId: String,
|
transactionId: String,
|
||||||
otherUserID: String,
|
otherUserID: String,
|
||||||
|
@ -47,8 +47,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
|
||||||
deviceId,
|
deviceId,
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
outgoingGossipingRequestManager,
|
outgoingKeyRequestManager,
|
||||||
incomingGossipingRequestManager,
|
secretShareManager,
|
||||||
deviceFingerprint,
|
deviceFingerprint,
|
||||||
transactionId,
|
transactionId,
|
||||||
otherUserID,
|
otherUserID,
|
||||||
|
|
|
@ -20,8 +20,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
|
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -32,8 +32,8 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
||||||
deviceId: String?,
|
deviceId: String?,
|
||||||
cryptoStore: IMXCryptoStore,
|
cryptoStore: IMXCryptoStore,
|
||||||
crossSigningService: CrossSigningService,
|
crossSigningService: CrossSigningService,
|
||||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||||
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
secretShareManager: SecretShareManager,
|
||||||
deviceFingerprint: String,
|
deviceFingerprint: String,
|
||||||
transactionId: String,
|
transactionId: String,
|
||||||
otherUserId: String,
|
otherUserId: String,
|
||||||
|
@ -44,8 +44,8 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
||||||
deviceId,
|
deviceId,
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
outgoingGossipingRequestManager,
|
outgoingKeyRequestManager,
|
||||||
incomingGossipingRequestManager,
|
secretShareManager,
|
||||||
deviceFingerprint,
|
deviceFingerprint,
|
||||||
transactionId,
|
transactionId,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
|
|
|
@ -59,9 +59,9 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.ValidVerificationDone
|
import org.matrix.android.sdk.api.session.room.model.message.ValidVerificationDone
|
||||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||||
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
|
|
||||||
import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder
|
import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||||
|
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept
|
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel
|
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel
|
||||||
|
@ -95,8 +95,8 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
@DeviceId private val deviceId: String?,
|
@DeviceId private val deviceId: String?,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||||
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
private val secretShareManager: SecretShareManager,
|
||||||
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
||||||
private val deviceListManager: DeviceListManager,
|
private val deviceListManager: DeviceListManager,
|
||||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||||
|
@ -551,8 +551,8 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
deviceId,
|
deviceId,
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
outgoingGossipingRequestManager,
|
outgoingKeyRequestManager,
|
||||||
incomingGossipingRequestManager,
|
secretShareManager,
|
||||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||||
startReq.transactionId,
|
startReq.transactionId,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
|
@ -771,8 +771,15 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val roomId = event.roomId
|
||||||
|
if (roomId == null) {
|
||||||
|
Timber.e("## SAS Verification missing roomId for event")
|
||||||
|
// TODO cancel?
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
handleReadyReceived(event.senderId, readyReq) {
|
handleReadyReceived(event.senderId, readyReq) {
|
||||||
verificationTransportRoomMessageFactory.createTransport(event.roomId!!, it)
|
verificationTransportRoomMessageFactory.createTransport(roomId, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -814,21 +821,15 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
getExistingTransaction(userId, doneReq.transactionId)
|
getExistingTransaction(userId, doneReq.transactionId)
|
||||||
?: getOldTransaction(userId, doneReq.transactionId)
|
?: getOldTransaction(userId, doneReq.transactionId)
|
||||||
?.let { vt ->
|
?.let { vt ->
|
||||||
val otherDeviceId = vt.otherDeviceId
|
val otherDeviceId = vt.otherDeviceId ?: return@let
|
||||||
if (!crossSigningService.canCrossSign()) {
|
if (!crossSigningService.canCrossSign()) {
|
||||||
outgoingGossipingRequestManager.sendSecretShareRequest(
|
cryptoCoroutineScope.launch {
|
||||||
MASTER_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*"))
|
secretShareManager.requestSecretTo(otherDeviceId, MASTER_KEY_SSSS_NAME)
|
||||||
)
|
secretShareManager.requestSecretTo(otherDeviceId, SELF_SIGNING_KEY_SSSS_NAME)
|
||||||
outgoingGossipingRequestManager.sendSecretShareRequest(
|
secretShareManager.requestSecretTo(otherDeviceId, USER_SIGNING_KEY_SSSS_NAME)
|
||||||
SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*"))
|
secretShareManager.requestSecretTo(otherDeviceId, KEYBACKUP_SECRET_SSSS_NAME)
|
||||||
)
|
}
|
||||||
outgoingGossipingRequestManager.sendSecretShareRequest(
|
|
||||||
USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*"))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
outgoingGossipingRequestManager.sendSecretShareRequest(
|
|
||||||
KEYBACKUP_SECRET_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*"))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -922,8 +923,8 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
otherUserId = senderId,
|
otherUserId = senderId,
|
||||||
otherDeviceId = readyReq.fromDevice,
|
otherDeviceId = readyReq.fromDevice,
|
||||||
crossSigningService = crossSigningService,
|
crossSigningService = crossSigningService,
|
||||||
outgoingGossipingRequestManager = outgoingGossipingRequestManager,
|
outgoingKeyRequestManager = outgoingKeyRequestManager,
|
||||||
incomingGossipingRequestManager = incomingGossipingRequestManager,
|
secretShareManager = secretShareManager,
|
||||||
cryptoStore = cryptoStore,
|
cryptoStore = cryptoStore,
|
||||||
qrCodeData = qrCodeData,
|
qrCodeData = qrCodeData,
|
||||||
userId = userId,
|
userId = userId,
|
||||||
|
@ -1124,8 +1125,8 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
deviceId,
|
deviceId,
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
outgoingGossipingRequestManager,
|
outgoingKeyRequestManager,
|
||||||
incomingGossipingRequestManager,
|
secretShareManager,
|
||||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||||
txID,
|
txID,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
|
@ -1188,6 +1189,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
}
|
}
|
||||||
.distinct()
|
.distinct()
|
||||||
|
|
||||||
|
requestsForUser.add(verificationRequest)
|
||||||
transport.sendVerificationRequest(methodValues, validLocalId, otherUserId, roomId, null) { syncedId, info ->
|
transport.sendVerificationRequest(methodValues, validLocalId, otherUserId, roomId, null) { syncedId, info ->
|
||||||
// We need to update with the syncedID
|
// We need to update with the syncedID
|
||||||
updatePendingRequest(
|
updatePendingRequest(
|
||||||
|
@ -1199,7 +1201,6 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
requestsForUser.add(verificationRequest)
|
|
||||||
dispatchRequestAdded(verificationRequest)
|
dispatchRequestAdded(verificationRequest)
|
||||||
|
|
||||||
return verificationRequest
|
return verificationRequest
|
||||||
|
@ -1323,8 +1324,8 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
deviceId,
|
deviceId,
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
outgoingGossipingRequestManager,
|
outgoingKeyRequestManager,
|
||||||
incomingGossipingRequestManager,
|
secretShareManager,
|
||||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||||
transactionId,
|
transactionId,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
|
@ -1465,8 +1466,8 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
otherUserId = otherUserId,
|
otherUserId = otherUserId,
|
||||||
otherDeviceId = otherDeviceId,
|
otherDeviceId = otherDeviceId,
|
||||||
crossSigningService = crossSigningService,
|
crossSigningService = crossSigningService,
|
||||||
outgoingGossipingRequestManager = outgoingGossipingRequestManager,
|
outgoingKeyRequestManager = outgoingKeyRequestManager,
|
||||||
incomingGossipingRequestManager = incomingGossipingRequestManager,
|
secretShareManager = secretShareManager,
|
||||||
cryptoStore = cryptoStore,
|
cryptoStore = cryptoStore,
|
||||||
qrCodeData = qrCodeData,
|
qrCodeData = qrCodeData,
|
||||||
userId = userId,
|
userId = userId,
|
||||||
|
|
|
@ -20,8 +20,8 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningServic
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||||
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
|
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@ -31,8 +31,8 @@ import timber.log.Timber
|
||||||
internal abstract class DefaultVerificationTransaction(
|
internal abstract class DefaultVerificationTransaction(
|
||||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||||
private val crossSigningService: CrossSigningService,
|
private val crossSigningService: CrossSigningService,
|
||||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||||
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
private val secretShareManager: SecretShareManager,
|
||||||
private val userId: String,
|
private val userId: String,
|
||||||
override val transactionId: String,
|
override val transactionId: String,
|
||||||
override val otherUserId: String,
|
override val otherUserId: String,
|
||||||
|
@ -86,7 +86,7 @@ internal abstract class DefaultVerificationTransaction(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (otherUserId == userId) {
|
if (otherUserId == userId) {
|
||||||
incomingGossipingRequestManager.onVerificationCompleteForDevice(otherDeviceId!!)
|
secretShareManager.onVerificationCompleteForDevice(otherDeviceId!!)
|
||||||
|
|
||||||
// If me it's reasonable to sign and upload the device signature
|
// If me it's reasonable to sign and upload the device signature
|
||||||
// Notice that i might not have the private keys, so may not be able to do it
|
// Notice that i might not have the private keys, so may not be able to do it
|
||||||
|
|
|
@ -23,8 +23,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.SasMode
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
|
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import org.matrix.android.sdk.internal.extensions.toUnsignedInt
|
import org.matrix.android.sdk.internal.extensions.toUnsignedInt
|
||||||
|
@ -42,8 +42,8 @@ internal abstract class SASDefaultVerificationTransaction(
|
||||||
open val deviceId: String?,
|
open val deviceId: String?,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
crossSigningService: CrossSigningService,
|
crossSigningService: CrossSigningService,
|
||||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||||
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
secretShareManager: SecretShareManager,
|
||||||
private val deviceFingerprint: String,
|
private val deviceFingerprint: String,
|
||||||
transactionId: String,
|
transactionId: String,
|
||||||
otherUserId: String,
|
otherUserId: String,
|
||||||
|
@ -52,8 +52,8 @@ internal abstract class SASDefaultVerificationTransaction(
|
||||||
) : DefaultVerificationTransaction(
|
) : DefaultVerificationTransaction(
|
||||||
setDeviceVerificationAction,
|
setDeviceVerificationAction,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
outgoingGossipingRequestManager,
|
outgoingKeyRequestManager,
|
||||||
incomingGossipingRequestManager,
|
secretShareManager,
|
||||||
userId,
|
userId,
|
||||||
transactionId,
|
transactionId,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
|
|
|
@ -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
|
package org.matrix.android.sdk.internal.crypto.verification
|
||||||
|
|
||||||
import io.realm.Realm
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.LocalEcho
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
|
||||||
|
@ -29,22 +25,18 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent
|
||||||
import org.matrix.android.sdk.internal.crypto.EventDecryptor
|
|
||||||
import org.matrix.android.sdk.internal.database.model.EventInsertType
|
|
||||||
import org.matrix.android.sdk.internal.di.DeviceId
|
import org.matrix.android.sdk.internal.di.DeviceId
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
import org.matrix.android.sdk.internal.util.time.Clock
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class VerificationMessageProcessor @Inject constructor(
|
internal class VerificationMessageProcessor @Inject constructor(
|
||||||
private val eventDecryptor: EventDecryptor,
|
|
||||||
private val clock: Clock,
|
|
||||||
private val verificationService: DefaultVerificationService,
|
private val verificationService: DefaultVerificationService,
|
||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
@DeviceId private val deviceId: String?
|
@DeviceId private val deviceId: String?,
|
||||||
) : EventInsertLiveProcessor {
|
private val clock: Clock,
|
||||||
|
) {
|
||||||
|
|
||||||
private val transactionsHandledByOtherDevice = ArrayList<String>()
|
private val transactionsHandledByOtherDevice = ArrayList<String>()
|
||||||
|
|
||||||
|
@ -60,40 +52,20 @@ internal class VerificationMessageProcessor @Inject constructor(
|
||||||
EventType.ENCRYPTED
|
EventType.ENCRYPTED
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
|
fun shouldProcess(eventType: String): Boolean {
|
||||||
if (insertType != EventInsertType.INCREMENTAL_SYNC) {
|
return allowedTypes.contains(eventType)
|
||||||
return false
|
|
||||||
}
|
|
||||||
return allowedTypes.contains(eventType) && !LocalEcho.isLocalEchoId(eventId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun process(realm: Realm, event: Event) {
|
suspend fun process(event: Event) {
|
||||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
|
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.getClearType()} from ${event.senderId}")
|
||||||
|
|
||||||
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
|
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
|
||||||
// the message should be ignored by the receiver.
|
// the message should be ignored by the receiver.
|
||||||
|
|
||||||
if (!VerificationService.isValidRequest(event.ageLocalTs ?: event.originServerTs, clock.epochMillis())) return Unit.also {
|
if (event.ageLocalTs != null && !VerificationService.isValidRequest(event.ageLocalTs, clock.epochMillis())) return Unit.also {
|
||||||
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated")
|
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated age:$event.ageLocalTs ms")
|
||||||
}
|
}
|
||||||
|
|
||||||
// decrypt if needed?
|
|
||||||
if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
|
||||||
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
|
||||||
// for now decrypt sync
|
|
||||||
try {
|
|
||||||
val result = eventDecryptor.decryptEvent(event, "")
|
|
||||||
event.mxDecryptionResult = OlmDecryptionResult(
|
|
||||||
payload = result.clearEvent,
|
|
||||||
senderKey = result.senderCurve25519Key,
|
|
||||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
|
||||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
|
||||||
)
|
|
||||||
} catch (e: MXCryptoError) {
|
|
||||||
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
|
|
||||||
verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
|
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
|
||||||
|
|
||||||
// Relates to is not encrypted
|
// Relates to is not encrypted
|
||||||
|
@ -102,7 +74,6 @@ internal class VerificationMessageProcessor @Inject constructor(
|
||||||
if (event.senderId == userId) {
|
if (event.senderId == userId) {
|
||||||
// If it's send from me, we need to keep track of Requests or Start
|
// If it's send from me, we need to keep track of Requests or Start
|
||||||
// done from another device of mine
|
// done from another device of mine
|
||||||
|
|
||||||
if (EventType.MESSAGE == event.getClearType()) {
|
if (EventType.MESSAGE == event.getClearType()) {
|
||||||
val msgType = event.getClearContent().toModel<MessageContent>()?.msgType
|
val msgType = event.getClearContent().toModel<MessageContent>()?.msgType
|
||||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
|
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
|
||||||
|
@ -137,6 +108,8 @@ internal class VerificationMessageProcessor @Inject constructor(
|
||||||
transactionsHandledByOtherDevice.remove(it)
|
transactionsHandledByOtherDevice.remove(it)
|
||||||
verificationService.onRoomRequestHandledByOtherDevice(event)
|
verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||||
}
|
}
|
||||||
|
} else if (EventType.ENCRYPTED == event.getClearType()) {
|
||||||
|
verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}")
|
Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}")
|
||||||
|
|
|
@ -15,14 +15,8 @@
|
||||||
*/
|
*/
|
||||||
package org.matrix.android.sdk.internal.crypto.verification
|
package org.matrix.android.sdk.internal.crypto.verification
|
||||||
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.work.BackoffPolicy
|
|
||||||
import androidx.work.Data
|
|
||||||
import androidx.work.ExistingWorkPolicy
|
|
||||||
import androidx.work.Operation
|
|
||||||
import androidx.work.WorkInfo
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest
|
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest
|
||||||
|
@ -45,27 +39,28 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE
|
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
|
import org.matrix.android.sdk.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
|
||||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
|
||||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||||
|
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
import org.matrix.android.sdk.internal.util.time.Clock
|
||||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
|
||||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
internal class VerificationTransportRoomMessage(
|
internal class VerificationTransportRoomMessage(
|
||||||
private val workManagerProvider: WorkManagerProvider,
|
private val sendVerificationMessageTask: SendVerificationMessageTask,
|
||||||
private val sessionId: String,
|
|
||||||
private val userId: String,
|
private val userId: String,
|
||||||
private val userDeviceId: String?,
|
private val userDeviceId: String?,
|
||||||
private val roomId: String,
|
private val roomId: String,
|
||||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||||
private val tx: DefaultVerificationTransaction?,
|
private val tx: DefaultVerificationTransaction?,
|
||||||
private val coroutineScope: CoroutineScope,
|
cryptoCoroutineScope: CoroutineScope,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
) : VerificationTransport {
|
) : VerificationTransport {
|
||||||
|
|
||||||
|
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||||
|
private val verificationSenderScope = CoroutineScope(cryptoCoroutineScope.coroutineContext + dispatcher)
|
||||||
|
private val sequencer = SemaphoreCoroutineSequencer()
|
||||||
|
|
||||||
override fun <T> sendToOther(type: String,
|
override fun <T> sendToOther(type: String,
|
||||||
verificationInfo: VerificationInfo<T>,
|
verificationInfo: VerificationInfo<T>,
|
||||||
nextState: VerificationTxState,
|
nextState: VerificationTxState,
|
||||||
|
@ -79,70 +74,22 @@ internal class VerificationTransportRoomMessage(
|
||||||
content = verificationInfo.toEventContent()!!
|
content = verificationInfo.toEventContent()!!
|
||||||
)
|
)
|
||||||
|
|
||||||
val workerParams = WorkerParamsFactory.toData(
|
verificationSenderScope.launch {
|
||||||
SendVerificationMessageWorker.Params(
|
sequencer.post {
|
||||||
sessionId = sessionId,
|
try {
|
||||||
eventId = event.eventId ?: ""
|
val params = SendVerificationMessageTask.Params(event)
|
||||||
)
|
sendVerificationMessageTask.executeRetry(params, 5)
|
||||||
)
|
// Do I need to update local echo state to sent?
|
||||||
val enqueueInfo = enqueueSendWork(workerParams)
|
if (onDone != null) {
|
||||||
|
onDone()
|
||||||
// I cannot just listen to the given work request, because when used in a uniqueWork,
|
} else {
|
||||||
// The callback is called while it is still Running ...
|
tx?.state = nextState
|
||||||
|
}
|
||||||
// Futures.addCallback(enqueueInfo.first.result, object : FutureCallback<Operation.State.SUCCESS> {
|
} catch (failure: Throwable) {
|
||||||
// override fun onSuccess(result: Operation.State.SUCCESS?) {
|
tx?.cancel(onErrorReason)
|
||||||
// if (onDone != null) {
|
}
|
||||||
// onDone()
|
|
||||||
// } else {
|
|
||||||
// tx?.state = nextState
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun onFailure(t: Throwable) {
|
|
||||||
// Timber.e("## SAS verification [${tx?.transactionId}] failed to send toDevice in state : ${tx?.state}, reason: ${t.localizedMessage}")
|
|
||||||
// tx?.cancel(onErrorReason)
|
|
||||||
// }
|
|
||||||
// }, listenerExecutor)
|
|
||||||
|
|
||||||
val workLiveData = workManagerProvider.workManager
|
|
||||||
.getWorkInfosForUniqueWorkLiveData(uniqueQueueName())
|
|
||||||
|
|
||||||
val observer = object : Observer<List<WorkInfo>> {
|
|
||||||
override fun onChanged(workInfoList: List<WorkInfo>?) {
|
|
||||||
workInfoList
|
|
||||||
?.firstOrNull { it.id == enqueueInfo.second }
|
|
||||||
?.let { wInfo ->
|
|
||||||
when (wInfo.state) {
|
|
||||||
WorkInfo.State.FAILED -> {
|
|
||||||
tx?.cancel(onErrorReason)
|
|
||||||
workLiveData.removeObserver(this)
|
|
||||||
}
|
|
||||||
WorkInfo.State.SUCCEEDED -> {
|
|
||||||
if (SessionSafeCoroutineWorker.hasFailed(wInfo.outputData)) {
|
|
||||||
Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}")
|
|
||||||
tx?.cancel(onErrorReason)
|
|
||||||
} else {
|
|
||||||
if (onDone != null) {
|
|
||||||
onDone()
|
|
||||||
} else {
|
|
||||||
tx?.state = nextState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
workLiveData.removeObserver(this)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// nop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO listen to DB to get synced info
|
|
||||||
coroutineScope.launch(Dispatchers.Main) {
|
|
||||||
workLiveData.observeForever(observer)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendVerificationRequest(supportedMethods: List<String>,
|
override fun sendVerificationRequest(supportedMethods: List<String>,
|
||||||
|
@ -173,60 +120,24 @@ internal class VerificationTransportRoomMessage(
|
||||||
val content = info.toContent()
|
val content = info.toContent()
|
||||||
|
|
||||||
val event = createEventAndLocalEcho(
|
val event = createEventAndLocalEcho(
|
||||||
localId,
|
localId = localId,
|
||||||
EventType.MESSAGE,
|
type = EventType.MESSAGE,
|
||||||
roomId,
|
roomId = roomId,
|
||||||
content
|
content = content
|
||||||
)
|
)
|
||||||
|
|
||||||
val workerParams = WorkerParamsFactory.toData(
|
verificationSenderScope.launch {
|
||||||
SendVerificationMessageWorker.Params(
|
val params = SendVerificationMessageTask.Params(event)
|
||||||
sessionId = sessionId,
|
sequencer.post {
|
||||||
eventId = event.eventId ?: ""
|
try {
|
||||||
)
|
val eventId = sendVerificationMessageTask.executeRetry(params, 5)
|
||||||
)
|
// Do I need to update local echo state to sent?
|
||||||
|
callback(eventId, validInfo)
|
||||||
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SendVerificationMessageWorker>()
|
} catch (failure: Throwable) {
|
||||||
.setConstraints(WorkManagerProvider.workConstraints)
|
callback(null, null)
|
||||||
.setInputData(workerParams)
|
}
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
workManagerProvider.workManager
|
|
||||||
.beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
|
|
||||||
.enqueue()
|
|
||||||
|
|
||||||
// I cannot just listen to the given work request, because when used in a uniqueWork,
|
|
||||||
// The callback is called while it is still Running ...
|
|
||||||
|
|
||||||
val workLiveData = workManagerProvider.workManager
|
|
||||||
.getWorkInfosForUniqueWorkLiveData("${roomId}_VerificationWork")
|
|
||||||
|
|
||||||
val observer = object : Observer<List<WorkInfo>> {
|
|
||||||
override fun onChanged(workInfoList: List<WorkInfo>?) {
|
|
||||||
workInfoList
|
|
||||||
?.filter { it.state == WorkInfo.State.SUCCEEDED }
|
|
||||||
?.firstOrNull { it.id == workRequest.id }
|
|
||||||
?.let { wInfo ->
|
|
||||||
if (SessionSafeCoroutineWorker.hasFailed(wInfo.outputData)) {
|
|
||||||
callback(null, null)
|
|
||||||
} else {
|
|
||||||
val eventId = wInfo.outputData.getString(localId)
|
|
||||||
if (eventId != null) {
|
|
||||||
callback(eventId, validInfo)
|
|
||||||
} else {
|
|
||||||
callback(null, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
workLiveData.removeObserver(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO listen to DB to get synced info
|
|
||||||
coroutineScope.launch(Dispatchers.Main) {
|
|
||||||
workLiveData.observeForever(observer)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String?, code: CancelCode) {
|
override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String?, code: CancelCode) {
|
||||||
|
@ -236,13 +147,17 @@ internal class VerificationTransportRoomMessage(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
content = MessageVerificationCancelContent.create(transactionId, code).toContent()
|
content = MessageVerificationCancelContent.create(transactionId, code).toContent()
|
||||||
)
|
)
|
||||||
val workerParams = WorkerParamsFactory.toData(
|
|
||||||
SendVerificationMessageWorker.Params(
|
verificationSenderScope.launch {
|
||||||
sessionId = sessionId,
|
sequencer.post {
|
||||||
eventId = event.eventId ?: ""
|
try {
|
||||||
)
|
val params = SendVerificationMessageTask.Params(event)
|
||||||
)
|
sendVerificationMessageTask.executeRetry(params, 5)
|
||||||
enqueueSendWork(workerParams)
|
} catch (failure: Throwable) {
|
||||||
|
Timber.w(failure, "Failed to cancel verification transaction")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun done(transactionId: String,
|
override fun done(transactionId: String,
|
||||||
|
@ -258,47 +173,21 @@ internal class VerificationTransportRoomMessage(
|
||||||
)
|
)
|
||||||
).toContent()
|
).toContent()
|
||||||
)
|
)
|
||||||
val workerParams = WorkerParamsFactory.toData(
|
verificationSenderScope.launch {
|
||||||
SendVerificationMessageWorker.Params(
|
sequencer.post {
|
||||||
sessionId = sessionId,
|
try {
|
||||||
eventId = event.eventId ?: ""
|
val params = SendVerificationMessageTask.Params(event)
|
||||||
)
|
sendVerificationMessageTask.executeRetry(params, 5)
|
||||||
)
|
} catch (failure: Throwable) {
|
||||||
val enqueueInfo = enqueueSendWork(workerParams)
|
Timber.w(failure, "Failed to complete (done) verification")
|
||||||
|
// should we call onDone?
|
||||||
val workLiveData = workManagerProvider.workManager
|
} finally {
|
||||||
.getWorkInfosForUniqueWorkLiveData(uniqueQueueName())
|
onDone?.invoke()
|
||||||
val observer = object : Observer<List<WorkInfo>> {
|
}
|
||||||
override fun onChanged(workInfoList: List<WorkInfo>?) {
|
|
||||||
workInfoList
|
|
||||||
?.filter { it.state == WorkInfo.State.SUCCEEDED }
|
|
||||||
?.firstOrNull { it.id == enqueueInfo.second }
|
|
||||||
?.let { _ ->
|
|
||||||
onDone?.invoke()
|
|
||||||
workLiveData.removeObserver(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO listen to DB to get synced info
|
|
||||||
coroutineScope.launch(Dispatchers.Main) {
|
|
||||||
workLiveData.observeForever(observer)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun enqueueSendWork(workerParams: Data): Pair<Operation, UUID> {
|
|
||||||
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SendVerificationMessageWorker>()
|
|
||||||
.setConstraints(WorkManagerProvider.workConstraints)
|
|
||||||
.setInputData(workerParams)
|
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
|
|
||||||
.build()
|
|
||||||
return workManagerProvider.workManager
|
|
||||||
.beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
|
|
||||||
.enqueue() to workRequest.id
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun uniqueQueueName() = "${roomId}_VerificationWork"
|
|
||||||
|
|
||||||
override fun createAccept(tid: String,
|
override fun createAccept(tid: String,
|
||||||
keyAgreementProtocol: String,
|
keyAgreementProtocol: String,
|
||||||
hash: String,
|
hash: String,
|
||||||
|
|
|
@ -16,39 +16,35 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.verification
|
package org.matrix.android.sdk.internal.crypto.verification
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
|
||||||
import org.matrix.android.sdk.internal.di.DeviceId
|
import org.matrix.android.sdk.internal.di.DeviceId
|
||||||
import org.matrix.android.sdk.internal.di.SessionId
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
|
||||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
import org.matrix.android.sdk.internal.util.time.Clock
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class VerificationTransportRoomMessageFactory @Inject constructor(
|
internal class VerificationTransportRoomMessageFactory @Inject constructor(
|
||||||
private val workManagerProvider: WorkManagerProvider,
|
private val sendVerificationMessageTask: SendVerificationMessageTask,
|
||||||
@SessionId
|
|
||||||
private val sessionId: String,
|
|
||||||
@UserId
|
@UserId
|
||||||
private val userId: String,
|
private val userId: String,
|
||||||
@DeviceId
|
@DeviceId
|
||||||
private val deviceId: String?,
|
private val deviceId: String?,
|
||||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val cryptoCoroutineScope: CoroutineScope,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage {
|
fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage {
|
||||||
return VerificationTransportRoomMessage(
|
return VerificationTransportRoomMessage(
|
||||||
workManagerProvider,
|
sendVerificationMessageTask = sendVerificationMessageTask,
|
||||||
sessionId,
|
userId = userId,
|
||||||
userId,
|
userDeviceId = deviceId,
|
||||||
deviceId,
|
roomId = roomId,
|
||||||
roomId,
|
localEchoEventFactory = localEchoEventFactory,
|
||||||
localEchoEventFactory,
|
tx = tx,
|
||||||
tx,
|
cryptoCoroutineScope = cryptoCoroutineScope,
|
||||||
taskExecutor.executorScope,
|
clock = clock,
|
||||||
clock
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerification
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.util.fromBase64
|
import org.matrix.android.sdk.api.util.fromBase64
|
||||||
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
|
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64Safe
|
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64Safe
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
|
@ -37,8 +37,8 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||||
override val otherUserId: String,
|
override val otherUserId: String,
|
||||||
override var otherDeviceId: String?,
|
override var otherDeviceId: String?,
|
||||||
private val crossSigningService: CrossSigningService,
|
private val crossSigningService: CrossSigningService,
|
||||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||||
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
secretShareManager: SecretShareManager,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
// Not null only if other user is able to scan QR code
|
// Not null only if other user is able to scan QR code
|
||||||
private val qrCodeData: QrCodeData?,
|
private val qrCodeData: QrCodeData?,
|
||||||
|
@ -48,8 +48,8 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||||
) : DefaultVerificationTransaction(
|
) : DefaultVerificationTransaction(
|
||||||
setDeviceVerificationAction,
|
setDeviceVerificationAction,
|
||||||
crossSigningService,
|
crossSigningService,
|
||||||
outgoingGossipingRequestManager,
|
outgoingKeyRequestManager,
|
||||||
incomingGossipingRequestManager,
|
secretShareManager,
|
||||||
userId,
|
userId,
|
||||||
transactionId,
|
transactionId,
|
||||||
otherUserId,
|
otherUserId,
|
||||||
|
|
|
@ -21,12 +21,8 @@ import dagger.Component
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
import org.matrix.android.sdk.api.auth.data.SessionParams
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker
|
|
||||||
import org.matrix.android.sdk.internal.crypto.CryptoModule
|
import org.matrix.android.sdk.internal.crypto.CryptoModule
|
||||||
import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker
|
|
||||||
import org.matrix.android.sdk.internal.crypto.SendGossipWorker
|
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
|
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker
|
|
||||||
import org.matrix.android.sdk.internal.di.MatrixComponent
|
import org.matrix.android.sdk.internal.di.MatrixComponent
|
||||||
import org.matrix.android.sdk.internal.federation.FederationModule
|
import org.matrix.android.sdk.internal.federation.FederationModule
|
||||||
import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
|
import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
|
||||||
|
@ -133,14 +129,6 @@ internal interface SessionComponent {
|
||||||
|
|
||||||
fun inject(worker: AddPusherWorker)
|
fun inject(worker: AddPusherWorker)
|
||||||
|
|
||||||
fun inject(worker: SendVerificationMessageWorker)
|
|
||||||
|
|
||||||
fun inject(worker: SendGossipRequestWorker)
|
|
||||||
|
|
||||||
fun inject(worker: CancelGossipRequestWorker)
|
|
||||||
|
|
||||||
fun inject(worker: SendGossipWorker)
|
|
||||||
|
|
||||||
fun inject(worker: UpdateTrustWorker)
|
fun inject(worker: UpdateTrustWorker)
|
||||||
|
|
||||||
@Component.Factory
|
@Component.Factory
|
||||||
|
|
|
@ -49,7 +49,6 @@ import org.matrix.android.sdk.api.util.md5
|
||||||
import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
|
import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask
|
import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
|
import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor
|
|
||||||
import org.matrix.android.sdk.internal.database.EventInsertLiveObserver
|
import org.matrix.android.sdk.internal.database.EventInsertLiveObserver
|
||||||
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
||||||
import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory
|
import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory
|
||||||
|
@ -318,10 +317,6 @@ internal abstract class SessionModule {
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindRoomCreateEventProcessor(processor: RoomCreateEventProcessor): EventInsertLiveProcessor
|
abstract fun bindRoomCreateEventProcessor(processor: RoomCreateEventProcessor): EventInsertLiveProcessor
|
||||||
|
|
||||||
@Binds
|
|
||||||
@IntoSet
|
|
||||||
abstract fun bindVerificationMessageProcessor(processor: VerificationMessageProcessor): EventInsertLiveProcessor
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindCallEventProcessor(processor: CallEventProcessor): EventInsertLiveProcessor
|
abstract fun bindCallEventProcessor(processor: CallEventProcessor): EventInsertLiveProcessor
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.membership.joining
|
||||||
|
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
|
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||||
import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult
|
import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult
|
||||||
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
|
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
|
||||||
|
@ -53,6 +54,7 @@ internal class DefaultJoinRoomTask @Inject constructor(
|
||||||
private val readMarkersTask: SetReadMarkersTask,
|
private val readMarkersTask: SetReadMarkersTask,
|
||||||
@SessionDatabase
|
@SessionDatabase
|
||||||
private val realmConfiguration: RealmConfiguration,
|
private val realmConfiguration: RealmConfiguration,
|
||||||
|
private val coroutineDispatcher: MatrixCoroutineDispatchers,
|
||||||
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
|
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
|
||||||
private val globalErrorReceiver: GlobalErrorReceiver,
|
private val globalErrorReceiver: GlobalErrorReceiver,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
|
|
|
@ -382,7 +382,10 @@ internal class RoomSyncHandler @Inject constructor(
|
||||||
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
|
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
|
||||||
|
|
||||||
val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
|
val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
|
||||||
for (event in eventList) {
|
for (rawEvent in eventList) {
|
||||||
|
// It's annoying roomId is not there, but lot of code rely on it.
|
||||||
|
// And had to do it now as copy would delete all decryption results..
|
||||||
|
val event = rawEvent.copy(roomId = roomId)
|
||||||
if (event.eventId == null || event.senderId == null || event.type == null) {
|
if (event.eventId == null || event.senderId == null || event.type == null) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -454,7 +457,7 @@ internal class RoomSyncHandler @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Give info to crypto module
|
// Give info to crypto module
|
||||||
cryptoService.onLiveEvent(roomEntity.roomId, event)
|
cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync)
|
||||||
|
|
||||||
// Try to remove local echo
|
// Try to remove local echo
|
||||||
event.unsignedData?.transactionId?.also {
|
event.unsignedData?.transactionId?.also {
|
||||||
|
|
|
@ -22,11 +22,7 @@ import androidx.work.ListenableWorker
|
||||||
import androidx.work.WorkerFactory
|
import androidx.work.WorkerFactory
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import org.matrix.android.sdk.internal.SessionManager
|
import org.matrix.android.sdk.internal.SessionManager
|
||||||
import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker
|
|
||||||
import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker
|
|
||||||
import org.matrix.android.sdk.internal.crypto.SendGossipWorker
|
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
|
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
|
||||||
import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker
|
|
||||||
import org.matrix.android.sdk.internal.di.MatrixScope
|
import org.matrix.android.sdk.internal.di.MatrixScope
|
||||||
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
|
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
|
||||||
import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker
|
import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker
|
||||||
|
@ -56,8 +52,6 @@ internal class MatrixWorkerFactory @Inject constructor(private val sessionManage
|
||||||
CheckFactoryWorker(appContext, workerParameters, true)
|
CheckFactoryWorker(appContext, workerParameters, true)
|
||||||
AddPusherWorker::class.java.name ->
|
AddPusherWorker::class.java.name ->
|
||||||
AddPusherWorker(appContext, workerParameters, sessionManager)
|
AddPusherWorker(appContext, workerParameters, sessionManager)
|
||||||
CancelGossipRequestWorker::class.java.name ->
|
|
||||||
CancelGossipRequestWorker(appContext, workerParameters, sessionManager)
|
|
||||||
GetGroupDataWorker::class.java.name ->
|
GetGroupDataWorker::class.java.name ->
|
||||||
GetGroupDataWorker(appContext, workerParameters, sessionManager)
|
GetGroupDataWorker(appContext, workerParameters, sessionManager)
|
||||||
MultipleEventSendingDispatcherWorker::class.java.name ->
|
MultipleEventSendingDispatcherWorker::class.java.name ->
|
||||||
|
@ -66,12 +60,6 @@ internal class MatrixWorkerFactory @Inject constructor(private val sessionManage
|
||||||
RedactEventWorker(appContext, workerParameters, sessionManager)
|
RedactEventWorker(appContext, workerParameters, sessionManager)
|
||||||
SendEventWorker::class.java.name ->
|
SendEventWorker::class.java.name ->
|
||||||
SendEventWorker(appContext, workerParameters, sessionManager)
|
SendEventWorker(appContext, workerParameters, sessionManager)
|
||||||
SendGossipRequestWorker::class.java.name ->
|
|
||||||
SendGossipRequestWorker(appContext, workerParameters, sessionManager)
|
|
||||||
SendGossipWorker::class.java.name ->
|
|
||||||
SendGossipWorker(appContext, workerParameters, sessionManager)
|
|
||||||
SendVerificationMessageWorker::class.java.name ->
|
|
||||||
SendVerificationMessageWorker(appContext, workerParameters, sessionManager)
|
|
||||||
SyncWorker::class.java.name ->
|
SyncWorker::class.java.name ->
|
||||||
SyncWorker(appContext, workerParameters, sessionManager)
|
SyncWorker(appContext, workerParameters, sessionManager)
|
||||||
UpdateTrustWorker::class.java.name ->
|
UpdateTrustWorker::class.java.name ->
|
||||||
|
|
|
@ -22,16 +22,17 @@ import im.vector.app.core.date.DateFormatKind
|
||||||
import im.vector.app.core.date.VectorDateFormatter
|
import im.vector.app.core.date.VectorDateFormatter
|
||||||
import im.vector.app.features.popup.DefaultVectorAlert
|
import im.vector.app.features.popup.DefaultVectorAlert
|
||||||
import im.vector.app.features.popup.PopupAlertManager
|
import im.vector.app.features.popup.PopupAlertManager
|
||||||
|
import im.vector.app.features.session.coroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
||||||
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
|
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRequestCancellation
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
||||||
|
@ -60,6 +61,9 @@ class KeyRequestHandler @Inject constructor(
|
||||||
|
|
||||||
var session: Session? = null
|
var session: Session? = null
|
||||||
|
|
||||||
|
// This functionality is disabled in element for now. As it could be prone to social attacks
|
||||||
|
var enablePromptingForRequest = false
|
||||||
|
|
||||||
fun start(session: Session) {
|
fun start(session: Session) {
|
||||||
this.session = session
|
this.session = session
|
||||||
session.cryptoService().verificationService().addListener(this)
|
session.cryptoService().verificationService().addListener(this)
|
||||||
|
@ -72,10 +76,9 @@ class KeyRequestHandler @Inject constructor(
|
||||||
session = null
|
session = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSecretShareRequest(request: IncomingSecretShareRequest): Boolean {
|
override fun onSecretShareRequest(request: SecretShareRequest): Boolean {
|
||||||
// By default Element will not prompt if the SDK has decided that the request should not be fulfilled
|
// By default Element will not prompt if the SDK has decided that the request should not be fulfilled
|
||||||
Timber.v("## onSecretShareRequest() : Ignoring $request")
|
Timber.v("## onSecretShareRequest() : Ignoring $request")
|
||||||
request.ignore?.run()
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +88,8 @@ class KeyRequestHandler @Inject constructor(
|
||||||
* @param request the key request.
|
* @param request the key request.
|
||||||
*/
|
*/
|
||||||
override fun onRoomKeyRequest(request: IncomingRoomKeyRequest) {
|
override fun onRoomKeyRequest(request: IncomingRoomKeyRequest) {
|
||||||
|
if (!enablePromptingForRequest) return
|
||||||
|
|
||||||
val userId = request.userId
|
val userId = request.userId
|
||||||
val deviceId = request.deviceId
|
val deviceId = request.deviceId
|
||||||
val requestId = request.requestId
|
val requestId = request.requestId
|
||||||
|
@ -195,15 +200,14 @@ class KeyRequestHandler @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun denyAllRequests(mappingKey: String) {
|
private fun denyAllRequests(mappingKey: String) {
|
||||||
alertsToRequests[mappingKey]?.forEach {
|
|
||||||
it.ignore?.run()
|
|
||||||
}
|
|
||||||
alertsToRequests.remove(mappingKey)
|
alertsToRequests.remove(mappingKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shareAllSessions(mappingKey: String) {
|
private fun shareAllSessions(mappingKey: String) {
|
||||||
alertsToRequests[mappingKey]?.forEach {
|
alertsToRequests[mappingKey]?.forEach {
|
||||||
it.share?.run()
|
session?.coroutineScope?.launch {
|
||||||
|
session?.cryptoService()?.manuallyAcceptRoomKeyRequest(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
alertsToRequests.remove(mappingKey)
|
alertsToRequests.remove(mappingKey)
|
||||||
}
|
}
|
||||||
|
@ -213,7 +217,7 @@ class KeyRequestHandler @Inject constructor(
|
||||||
*
|
*
|
||||||
* @param request the cancellation request.
|
* @param request the cancellation request.
|
||||||
*/
|
*/
|
||||||
override fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) {
|
override fun onRequestCancelled(request: IncomingRoomKeyRequest) {
|
||||||
// see if we can find the request in the queue
|
// see if we can find the request in the queue
|
||||||
val userId = request.userId
|
val userId = request.userId
|
||||||
val deviceId = request.deviceId
|
val deviceId = request.deviceId
|
||||||
|
|
|
@ -20,7 +20,6 @@ import im.vector.app.R
|
||||||
import im.vector.app.core.platform.ViewModelTask
|
import im.vector.app.core.platform.ViewModelTask
|
||||||
import im.vector.app.core.platform.WaitingViewData
|
import im.vector.app.core.platform.WaitingViewData
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
|
||||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||||
|
@ -149,15 +148,8 @@ class BackupToQuadSMigrationTask @Inject constructor(
|
||||||
// save for gossiping
|
// save for gossiping
|
||||||
keysBackupService.saveBackupRecoveryKey(recoveryKey, version.version)
|
keysBackupService.saveBackupRecoveryKey(recoveryKey, version.version)
|
||||||
|
|
||||||
// while we are there let's restore, but do not block
|
// It's not a good idea to download the full backup, it might take very long
|
||||||
session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
|
// and use a lot of resources
|
||||||
version,
|
|
||||||
recoveryKey,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
NoOpMatrixCallback()
|
|
||||||
)
|
|
||||||
|
|
||||||
return Result.Success
|
return Result.Success
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
|
|
|
@ -42,7 +42,6 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||||
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
|
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
|
||||||
|
@ -424,6 +423,11 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tentativeRestoreBackup(res: Map<String, String>?) {
|
private fun tentativeRestoreBackup(res: Map<String, String>?) {
|
||||||
|
// It's not a good idea to download the full backup, it might take very long
|
||||||
|
// and use a lot of resources
|
||||||
|
// Just check that the key is valid and store it, the backup will be used megolm session per
|
||||||
|
// megolm session when an UISI is encountered
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also {
|
val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also {
|
||||||
|
@ -434,17 +438,13 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||||
session.cryptoService().keysBackupService().getCurrentVersion(it)
|
session.cryptoService().keysBackupService().getCurrentVersion(it)
|
||||||
}.toKeysVersionResult() ?: return@launch
|
}.toKeysVersionResult() ?: return@launch
|
||||||
|
|
||||||
awaitCallback<ImportRoomKeysResult> {
|
val recoveryKey = computeRecoveryKey(secret.fromBase64())
|
||||||
session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
|
val isValid = awaitCallback<Boolean> {
|
||||||
version,
|
session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(recoveryKey, it)
|
||||||
computeRecoveryKey(secret.fromBase64()),
|
}
|
||||||
null,
|
if (isValid) {
|
||||||
null,
|
session.cryptoService().keysBackupService().saveBackupRecoveryKey(recoveryKey, version.version)
|
||||||
null,
|
|
||||||
it
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
awaitCallback<Unit> {
|
awaitCallback<Unit> {
|
||||||
session.cryptoService().keysBackupService().trustKeysBackupVersion(version, true, it)
|
session.cryptoService().keysBackupService().trustKeysBackupVersion(version, true, it)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,10 +27,8 @@ import im.vector.app.core.extensions.cleanup
|
||||||
import im.vector.app.core.extensions.configureWith
|
import im.vector.app.core.extensions.configureWith
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.core.resources.ColorProvider
|
import im.vector.app.core.resources.ColorProvider
|
||||||
import im.vector.app.core.utils.createJSonViewerStyleProvider
|
|
||||||
import im.vector.app.databinding.FragmentGenericRecyclerBinding
|
import im.vector.app.databinding.FragmentGenericRecyclerBinding
|
||||||
import org.billcarsonfr.jsonviewer.JSonViewerDialog
|
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class GossipingEventsPaperTrailFragment @Inject constructor(
|
class GossipingEventsPaperTrailFragment @Inject constructor(
|
||||||
|
@ -64,17 +62,17 @@ class GossipingEventsPaperTrailFragment @Inject constructor(
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun didTap(event: Event) {
|
override fun didTap(event: AuditTrail) {
|
||||||
if (event.isEncrypted()) {
|
// if (event.isEncrypted()) {
|
||||||
event.toClearContentStringWithIndent()
|
// event.toClearContentStringWithIndent()
|
||||||
} else {
|
// } else {
|
||||||
event.toContentStringWithIndent()
|
// event.toContentStringWithIndent()
|
||||||
}?.let {
|
// }?.let {
|
||||||
JSonViewerDialog.newInstance(
|
// JSonViewerDialog.newInstance(
|
||||||
it,
|
// it,
|
||||||
-1,
|
// -1,
|
||||||
createJSonViewerStyleProvider(colorProvider)
|
// createJSonViewerStyleProvider(colorProvider)
|
||||||
).show(childFragmentManager, "JSON_VIEWER")
|
// ).show(childFragmentManager, "JSON_VIEWER")
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,10 +32,10 @@ import im.vector.app.core.platform.EmptyAction
|
||||||
import im.vector.app.core.platform.EmptyViewEvents
|
import im.vector.app.core.platform.EmptyViewEvents
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
|
||||||
|
|
||||||
data class GossipingEventsPaperTrailState(
|
data class GossipingEventsPaperTrailState(
|
||||||
val events: Async<PagedList<Event>> = Uninitialized
|
val events: Async<PagedList<AuditTrail>> = Uninitialized
|
||||||
) : MavericksState
|
) : MavericksState
|
||||||
|
|
||||||
class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted initialState: GossipingEventsPaperTrailState,
|
class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted initialState: GossipingEventsPaperTrailState,
|
||||||
|
|
|
@ -17,62 +17,43 @@
|
||||||
package im.vector.app.features.settings.devtools
|
package im.vector.app.features.settings.devtools
|
||||||
|
|
||||||
import im.vector.app.core.resources.DateProvider
|
import im.vector.app.core.resources.DateProvider
|
||||||
import me.gujun.android.span.span
|
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent
|
import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
|
import org.matrix.android.sdk.api.session.crypto.model.TrailType
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
|
import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
|
||||||
import org.threeten.bp.format.DateTimeFormatter
|
import org.threeten.bp.format.DateTimeFormatter
|
||||||
|
|
||||||
class GossipingEventsSerializer {
|
class GossipingEventsSerializer {
|
||||||
private val full24DateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
|
private val full24DateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
|
||||||
|
|
||||||
fun serialize(eventList: List<Event>): String {
|
fun serialize(eventList: List<AuditTrail>): String {
|
||||||
return buildString {
|
return buildString {
|
||||||
eventList.forEach {
|
eventList.forEach { trail ->
|
||||||
val clearType = it.getClearType()
|
val type = trail.type
|
||||||
append("[${getFormattedDate(it.ageLocalTs)}] $clearType from:${it.senderId} - ")
|
val info = trail.info
|
||||||
when (clearType) {
|
append("[${getFormattedDate(trail.ageLocalTs)}] ${type.name} ")
|
||||||
EventType.ROOM_KEY_REQUEST -> {
|
append("sessionId: ${info.sessionId} ")
|
||||||
val content = it.getClearContent().toModel<RoomKeyShareRequest>()
|
when (type) {
|
||||||
append("reqId:${content?.requestId} action:${content?.action} ")
|
TrailType.IncomingKeyRequest -> {
|
||||||
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
|
append("from:${info.userId}|${info.deviceId} - ")
|
||||||
append("sessionId: ${content.body?.sessionId} ")
|
|
||||||
}
|
|
||||||
append("requestedBy: ${content?.requestingDeviceId}")
|
|
||||||
}
|
}
|
||||||
EventType.FORWARDED_ROOM_KEY -> {
|
TrailType.OutgoingKeyForward -> {
|
||||||
val encryptedContent = it.content.toModel<OlmEventContent>()
|
append("to:${info.userId}|${info.deviceId} - ")
|
||||||
val content = it.getClearContent().toModel<ForwardedRoomKeyContent>()
|
(trail.info as? ForwardInfo)?.let {
|
||||||
|
append("chainIndex: ${it.chainIndex} ")
|
||||||
append("sessionId:${content?.sessionId} From Device (sender key):${encryptedContent?.senderKey}")
|
|
||||||
span("\nFrom Device (sender key):") {
|
|
||||||
textStyle = "bold"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EventType.ROOM_KEY -> {
|
TrailType.OutgoingKeyWithheld -> {
|
||||||
val content = it.getClearContent()
|
append("to:${info.userId}|${info.deviceId} - ")
|
||||||
append("sessionId:${content?.get("session_id")} roomId:${content?.get("room_id")} dest:${content?.get("_dest") ?: "me"}")
|
(trail.info as? WithheldInfo)?.let {
|
||||||
}
|
append("code: ${it.code} ")
|
||||||
EventType.SEND_SECRET -> {
|
|
||||||
val content = it.getClearContent().toModel<SecretSendEventContent>()
|
|
||||||
append("requestId:${content?.requestId} From Device:${it.mxDecryptionResult?.payload?.get("sender_device")}")
|
|
||||||
}
|
|
||||||
EventType.REQUEST_SECRET -> {
|
|
||||||
val content = it.getClearContent().toModel<SecretShareRequest>()
|
|
||||||
append("reqId:${content?.requestId} action:${content?.action} ")
|
|
||||||
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
|
|
||||||
append("secretName:${content.secretName} ")
|
|
||||||
}
|
}
|
||||||
append("requestedBy:${content?.requestingDeviceId}")
|
|
||||||
}
|
}
|
||||||
EventType.ENCRYPTED -> {
|
TrailType.IncomingKeyForward -> {
|
||||||
append("Failed to Decrypt")
|
append("from:${info.userId}|${info.deviceId} - ")
|
||||||
|
(trail.info as? ForwardInfo)?.let {
|
||||||
|
append("chainIndex: ${it.chainIndex} ")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
append("??")
|
append("??")
|
||||||
|
|
|
@ -18,7 +18,6 @@ package im.vector.app.features.settings.devtools
|
||||||
|
|
||||||
import com.airbnb.epoxy.EpoxyModel
|
import com.airbnb.epoxy.EpoxyModel
|
||||||
import com.airbnb.epoxy.paging.PagedListEpoxyController
|
import com.airbnb.epoxy.paging.PagedListEpoxyController
|
||||||
import im.vector.app.R
|
|
||||||
import im.vector.app.core.date.DateFormatKind
|
import im.vector.app.core.date.DateFormatKind
|
||||||
import im.vector.app.core.date.VectorDateFormatter
|
import im.vector.app.core.date.VectorDateFormatter
|
||||||
import im.vector.app.core.resources.ColorProvider
|
import im.vector.app.core.resources.ColorProvider
|
||||||
|
@ -26,137 +25,75 @@ import im.vector.app.core.ui.list.GenericItem_
|
||||||
import im.vector.app.core.utils.createUIHandler
|
import im.vector.app.core.utils.createUIHandler
|
||||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent
|
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
|
import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
|
import org.matrix.android.sdk.api.session.crypto.model.TrailType
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
|
import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class GossipingTrailPagedEpoxyController @Inject constructor(
|
class GossipingTrailPagedEpoxyController @Inject constructor(
|
||||||
private val vectorDateFormatter: VectorDateFormatter,
|
private val vectorDateFormatter: VectorDateFormatter,
|
||||||
private val colorProvider: ColorProvider
|
private val colorProvider: ColorProvider
|
||||||
) : PagedListEpoxyController<Event>(
|
) : PagedListEpoxyController<AuditTrail>(
|
||||||
// Important it must match the PageList builder notify Looper
|
// Important it must match the PageList builder notify Looper
|
||||||
modelBuildingHandler = createUIHandler()
|
modelBuildingHandler = createUIHandler()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
interface InteractionListener {
|
interface InteractionListener {
|
||||||
fun didTap(event: Event)
|
fun didTap(event: AuditTrail)
|
||||||
}
|
}
|
||||||
|
|
||||||
var interactionListener: InteractionListener? = null
|
var interactionListener: InteractionListener? = null
|
||||||
|
|
||||||
override fun buildItemModel(currentPosition: Int, item: Event?): EpoxyModel<*> {
|
override fun buildItemModel(currentPosition: Int, item: AuditTrail?): EpoxyModel<*> {
|
||||||
val host = this
|
val host = this
|
||||||
val event = item ?: return GenericItem_().apply { id(currentPosition) }
|
val event = item ?: return GenericItem_().apply { id(currentPosition) }
|
||||||
return GenericItem_().apply {
|
return GenericItem_().apply {
|
||||||
id(event.hashCode())
|
id(event.hashCode())
|
||||||
itemClickAction { host.interactionListener?.didTap(event) }
|
itemClickAction { host.interactionListener?.didTap(event) }
|
||||||
title(
|
title(event.type.name.toEpoxyCharSequence())
|
||||||
if (event.isEncrypted()) {
|
|
||||||
"${event.getClearType()} [encrypted]"
|
|
||||||
} else {
|
|
||||||
event.type
|
|
||||||
}?.toEpoxyCharSequence()
|
|
||||||
)
|
|
||||||
description(
|
description(
|
||||||
span {
|
span {
|
||||||
+host.vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
|
+host.vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
|
||||||
span("\nfrom: ") {
|
span("\n${host.senderFromTo(event.type)}: ") {
|
||||||
textStyle = "bold"
|
textStyle = "bold"
|
||||||
}
|
}
|
||||||
+"${event.senderId}"
|
+"${event.info.userId}|${event.info.deviceId}"
|
||||||
|
span("\nroomId: ") {
|
||||||
|
textStyle = "bold"
|
||||||
|
}
|
||||||
|
+event.info.roomId
|
||||||
|
span("\nsessionId: ") {
|
||||||
|
textStyle = "bold"
|
||||||
|
}
|
||||||
|
+event.info.sessionId
|
||||||
apply {
|
apply {
|
||||||
if (event.getClearType() == EventType.ROOM_KEY_REQUEST) {
|
when (event.type) {
|
||||||
val content = event.getClearContent().toModel<RoomKeyShareRequest>()
|
TrailType.OutgoingKeyForward -> {
|
||||||
span("\nreqId:") {
|
val fInfo = event.info as ForwardInfo
|
||||||
textStyle = "bold"
|
span("\nchainIndex: ") {
|
||||||
}
|
|
||||||
+" ${content?.requestId}"
|
|
||||||
span("\naction:") {
|
|
||||||
textStyle = "bold"
|
|
||||||
}
|
|
||||||
+" ${content?.action}"
|
|
||||||
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
|
|
||||||
span("\nsessionId:") {
|
|
||||||
textStyle = "bold"
|
textStyle = "bold"
|
||||||
}
|
}
|
||||||
+" ${content.body?.sessionId}"
|
+"${fInfo.chainIndex}"
|
||||||
}
|
}
|
||||||
span("\nrequestedBy: ") {
|
TrailType.OutgoingKeyWithheld -> {
|
||||||
textStyle = "bold"
|
val fInfo = event.info as WithheldInfo
|
||||||
}
|
span("\ncode: ") {
|
||||||
+"${content?.requestingDeviceId}"
|
|
||||||
} else if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
|
||||||
val encryptedContent = event.content.toModel<OlmEventContent>()
|
|
||||||
val content = event.getClearContent().toModel<ForwardedRoomKeyContent>()
|
|
||||||
if (event.mxDecryptionResult == null) {
|
|
||||||
span("**Failed to Decrypt** ${event.mCryptoError}") {
|
|
||||||
textColor = host.colorProvider.getColorFromAttribute(R.attr.colorError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span("\nsessionId:") {
|
|
||||||
textStyle = "bold"
|
|
||||||
}
|
|
||||||
+" ${content?.sessionId}"
|
|
||||||
span("\nFrom Device (sender key):") {
|
|
||||||
textStyle = "bold"
|
|
||||||
}
|
|
||||||
+" ${encryptedContent?.senderKey}"
|
|
||||||
} else if (event.getClearType() == EventType.ROOM_KEY) {
|
|
||||||
// it's a bit of a fake event for trail reasons
|
|
||||||
val content = event.getClearContent()
|
|
||||||
span("\nsessionId:") {
|
|
||||||
textStyle = "bold"
|
|
||||||
}
|
|
||||||
+" ${content?.get("session_id")}"
|
|
||||||
span("\nroomId:") {
|
|
||||||
textStyle = "bold"
|
|
||||||
}
|
|
||||||
+" ${content?.get("room_id")}"
|
|
||||||
span("\nTo :") {
|
|
||||||
textStyle = "bold"
|
|
||||||
}
|
|
||||||
+" ${content?.get("_dest") ?: "me"}"
|
|
||||||
} else if (event.getClearType() == EventType.SEND_SECRET) {
|
|
||||||
val content = event.getClearContent().toModel<SecretSendEventContent>()
|
|
||||||
|
|
||||||
span("\nrequestId:") {
|
|
||||||
textStyle = "bold"
|
|
||||||
}
|
|
||||||
+" ${content?.requestId}"
|
|
||||||
span("\nFrom Device:") {
|
|
||||||
textStyle = "bold"
|
|
||||||
}
|
|
||||||
+" ${event.mxDecryptionResult?.payload?.get("sender_device")}"
|
|
||||||
} else if (event.getClearType() == EventType.REQUEST_SECRET) {
|
|
||||||
val content = event.getClearContent().toModel<SecretShareRequest>()
|
|
||||||
span("\nreqId:") {
|
|
||||||
textStyle = "bold"
|
|
||||||
}
|
|
||||||
+" ${content?.requestId}"
|
|
||||||
span("\naction:") {
|
|
||||||
textStyle = "bold"
|
|
||||||
}
|
|
||||||
+" ${content?.action}"
|
|
||||||
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
|
|
||||||
span("\nsecretName:") {
|
|
||||||
textStyle = "bold"
|
textStyle = "bold"
|
||||||
}
|
}
|
||||||
+" ${content.secretName}"
|
+"${fInfo.code}"
|
||||||
}
|
}
|
||||||
span("\nrequestedBy: ") {
|
TrailType.IncomingKeyRequest -> {
|
||||||
textStyle = "bold"
|
// no additional info
|
||||||
}
|
}
|
||||||
+"${content?.requestingDeviceId}"
|
TrailType.IncomingKeyForward -> {
|
||||||
} else if (event.getClearType() == EventType.ENCRYPTED) {
|
val fInfo = event.info as ForwardInfo
|
||||||
span("**Failed to Decrypt** ${event.mCryptoError}") {
|
span("\nchainIndex: ") {
|
||||||
textColor = host.colorProvider.getColorFromAttribute(R.attr.colorError)
|
textStyle = "bold"
|
||||||
|
}
|
||||||
|
+"${fInfo.chainIndex}"
|
||||||
|
}
|
||||||
|
TrailType.Unknown -> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,4 +101,14 @@ class GossipingTrailPagedEpoxyController @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun senderFromTo(type: TrailType): String {
|
||||||
|
return when (type) {
|
||||||
|
TrailType.OutgoingKeyWithheld,
|
||||||
|
TrailType.OutgoingKeyForward -> "to"
|
||||||
|
TrailType.IncomingKeyRequest,
|
||||||
|
TrailType.IncomingKeyForward -> "from"
|
||||||
|
TrailType.Unknown -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ class IncomingKeyRequestPagedController @Inject constructor(
|
||||||
textStyle = "bold"
|
textStyle = "bold"
|
||||||
}
|
}
|
||||||
span("${roomKeyRequest.userId}")
|
span("${roomKeyRequest.userId}")
|
||||||
|
+"\n"
|
||||||
+host.vectorDateFormatter.format(roomKeyRequest.localCreationTimestamp, DateFormatKind.DEFAULT_DATE_AND_TIME)
|
+host.vectorDateFormatter.format(roomKeyRequest.localCreationTimestamp, DateFormatKind.DEFAULT_DATE_AND_TIME)
|
||||||
span("\nsessionId:") {
|
span("\nsessionId:") {
|
||||||
textStyle = "bold"
|
textStyle = "bold"
|
||||||
|
@ -62,10 +63,6 @@ class IncomingKeyRequestPagedController @Inject constructor(
|
||||||
textStyle = "bold"
|
textStyle = "bold"
|
||||||
}
|
}
|
||||||
+"${roomKeyRequest.deviceId}"
|
+"${roomKeyRequest.deviceId}"
|
||||||
span("\nstate: ") {
|
|
||||||
textStyle = "bold"
|
|
||||||
}
|
|
||||||
+roomKeyRequest.state.name
|
|
||||||
}.toEpoxyCharSequence()
|
}.toEpoxyCharSequence()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,12 +32,12 @@ import im.vector.app.core.platform.EmptyViewEvents
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
|
|
||||||
|
|
||||||
data class KeyRequestListViewState(
|
data class KeyRequestListViewState(
|
||||||
val incomingRequests: Async<PagedList<IncomingRoomKeyRequest>> = Uninitialized,
|
val incomingRequests: Async<PagedList<IncomingRoomKeyRequest>> = Uninitialized,
|
||||||
val outgoingRoomKeyRequests: Async<PagedList<OutgoingRoomKeyRequest>> = Uninitialized
|
val outgoingRoomKeyRequests: Async<PagedList<OutgoingKeyRequest>> = Uninitialized
|
||||||
) : MavericksState
|
) : MavericksState
|
||||||
|
|
||||||
class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState: KeyRequestListViewState,
|
class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState: KeyRequestListViewState,
|
||||||
|
|
|
@ -22,10 +22,10 @@ import im.vector.app.core.ui.list.GenericItem_
|
||||||
import im.vector.app.core.utils.createUIHandler
|
import im.vector.app.core.utils.createUIHandler
|
||||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
|
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyController<OutgoingRoomKeyRequest>(
|
class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyController<OutgoingKeyRequest>(
|
||||||
// Important it must match the PageList builder notify Looper
|
// Important it must match the PageList builder notify Looper
|
||||||
modelBuildingHandler = createUIHandler()
|
modelBuildingHandler = createUIHandler()
|
||||||
) {
|
) {
|
||||||
|
@ -36,7 +36,7 @@ class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyCo
|
||||||
|
|
||||||
var interactionListener: InteractionListener? = null
|
var interactionListener: InteractionListener? = null
|
||||||
|
|
||||||
override fun buildItemModel(currentPosition: Int, item: OutgoingRoomKeyRequest?): EpoxyModel<*> {
|
override fun buildItemModel(currentPosition: Int, item: OutgoingKeyRequest?): EpoxyModel<*> {
|
||||||
val roomKeyRequest = item ?: return GenericItem_().apply { id(currentPosition) }
|
val roomKeyRequest = item ?: return GenericItem_().apply { id(currentPosition) }
|
||||||
|
|
||||||
return GenericItem_().apply {
|
return GenericItem_().apply {
|
||||||
|
|
|
@ -66,7 +66,7 @@ class FakeSharedSecretStorageService : SharedSecretStorageService {
|
||||||
|
|
||||||
override fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?) = integrityResult
|
override fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?) = integrityResult
|
||||||
|
|
||||||
override fun requestSecret(name: String, myOtherDeviceId: String) {
|
override suspend fun requestSecret(name: String, myOtherDeviceId: String) {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue