mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-17 11:48:51 +03:00
Refactor key and secret request managers
use megolm backup before sending key request
This commit is contained in:
parent
d694ea16a8
commit
9177cb11d5
61 changed files with 2499 additions and 1852 deletions
|
@ -31,8 +31,14 @@ import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
|||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
||||
|
@ -46,7 +52,11 @@ import org.matrix.android.sdk.api.session.room.model.Membership
|
|||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner
|
||||
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.awaitCallback
|
||||
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
||||
import java.util.UUID
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
|
@ -293,6 +303,59 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize cross-signing, set up megolm backup and save all in 4S
|
||||
*/
|
||||
fun bootstrapSecurity(session: Session) {
|
||||
initializeCrossSigning(session)
|
||||
val ssssService = session.sharedSecretStorageService
|
||||
testHelper.runBlockingTest {
|
||||
val keyInfo = ssssService.generateKey(
|
||||
UUID.randomUUID().toString(),
|
||||
null,
|
||||
"ssss_key",
|
||||
EmptyKeySigner()
|
||||
)
|
||||
ssssService.setDefaultKey(keyInfo.keyId)
|
||||
|
||||
ssssService.storeSecret(
|
||||
MASTER_KEY_SSSS_NAME,
|
||||
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master!!,
|
||||
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
|
||||
)
|
||||
|
||||
ssssService.storeSecret(
|
||||
SELF_SIGNING_KEY_SSSS_NAME,
|
||||
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned!!,
|
||||
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
|
||||
)
|
||||
|
||||
ssssService.storeSecret(
|
||||
USER_SIGNING_KEY_SSSS_NAME,
|
||||
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user!!,
|
||||
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
|
||||
)
|
||||
|
||||
// set up megolm backup
|
||||
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
|
||||
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
|
||||
}
|
||||
val version = awaitCallback<KeysVersion> {
|
||||
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
|
||||
}
|
||||
// Save it for gossiping
|
||||
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
|
||||
|
||||
extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret ->
|
||||
ssssService.storeSecret(
|
||||
KEYBACKUP_SECRET_SSSS_NAME,
|
||||
secret,
|
||||
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) {
|
||||
assertTrue(alice.cryptoService().crossSigningService().canCrossSign())
|
||||
assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
|
||||
|
|
|
@ -35,6 +35,12 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
|
|||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
|
@ -49,8 +55,10 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
|||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
import org.matrix.android.sdk.common.TestMatrixCallback
|
||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
|
@ -113,10 +121,10 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
otherAccounts.forEach { otherSession ->
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
|
||||
timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
|
||||
timeLineEvent != null &&
|
||||
timeLineEvent.isEncrypted() &&
|
||||
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -232,10 +240,10 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||
timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||
timeLineEvent != null &&
|
||||
timeLineEvent.isEncrypted() &&
|
||||
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
// we want more so let's discard the session
|
||||
|
@ -292,13 +300,13 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
|
||||
// Let's now import keys from backup
|
||||
|
||||
newBobSession.cryptoService().keysBackupService().let { keysBackupService ->
|
||||
newBobSession.cryptoService().keysBackupService().let { kbs ->
|
||||
val keyVersionResult = testHelper.doSync<KeysVersionResult?> {
|
||||
keysBackupService.getVersion(version.version, it)
|
||||
kbs.getVersion(version.version, it)
|
||||
}
|
||||
|
||||
val importedResult = testHelper.doSync<ImportRoomKeysResult> {
|
||||
keysBackupService.restoreKeyBackupWithPassword(keyVersionResult!!,
|
||||
kbs.restoreKeyBackupWithPassword(keyVersionResult!!,
|
||||
keyBackupPassword,
|
||||
null,
|
||||
null,
|
||||
|
@ -341,10 +349,10 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||
timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||
timeLineEvent != null &&
|
||||
timeLineEvent.isEncrypted() &&
|
||||
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -360,7 +368,11 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
// check that new bob can't currently decrypt
|
||||
Log.v("#E2E TEST", "check that new bob can't currently decrypt")
|
||||
|
||||
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
|
||||
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
|
||||
// newBobSession.cryptoService().getOutgoingRoomKeyRequests()
|
||||
// .firstOrNull {
|
||||
// it.sessionId ==
|
||||
// }
|
||||
|
||||
// Try to request
|
||||
sentEventIds.forEach { sentEventId ->
|
||||
|
@ -369,12 +381,34 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
}
|
||||
|
||||
// wait a bit
|
||||
testHelper.runBlockingTest {
|
||||
delay(10_000)
|
||||
}
|
||||
// we need to wait a couple of syncs to let sharing occurs
|
||||
// testHelper.waitFewSyncs(newBobSession, 6)
|
||||
|
||||
// Ensure that new bob still can't decrypt (keys must have been withheld)
|
||||
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.KEYS_WITHHELD)
|
||||
sentEventIds.forEach { sentEventId ->
|
||||
val megolmSessionId = newBobSession.getRoom(e2eRoomID)!!
|
||||
.getTimelineEvent(sentEventId)!!
|
||||
.root.content.toModel<EncryptedEventContent>()!!.sessionId
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests()
|
||||
.first {
|
||||
it.sessionId == megolmSessionId &&
|
||||
it.roomId == e2eRoomID
|
||||
}
|
||||
.results.also {
|
||||
Log.w("##TEST", "result list is $it")
|
||||
}
|
||||
.firstOrNull { it.userId == aliceSession.myUserId }
|
||||
?.result
|
||||
aliceReply != null &&
|
||||
aliceReply is RequestResult.Failure &&
|
||||
WithHeldCode.UNAUTHORISED == aliceReply.code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
|
||||
|
||||
// Now mark new bob session as verified
|
||||
|
||||
|
@ -387,11 +421,6 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
|
||||
}
|
||||
|
||||
// wait a bit
|
||||
testHelper.runBlockingTest {
|
||||
delay(10_000)
|
||||
}
|
||||
|
||||
ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
|
@ -422,10 +451,10 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
|
||||
timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
|
||||
timeLineEvent != null &&
|
||||
timeLineEvent.isEncrypted() &&
|
||||
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -450,10 +479,10 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
|
||||
timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
|
||||
timeLineEvent != null &&
|
||||
timeLineEvent.isEncrypted() &&
|
||||
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -498,25 +527,29 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
// now let new session request
|
||||
newBobSession.cryptoService().requestRoomKeyForEvent(firstEventNewBobPov.root)
|
||||
|
||||
// wait a bit
|
||||
testHelper.runBlockingTest {
|
||||
delay(10_000)
|
||||
}
|
||||
// We need to wait for the key request to be sent out and then a reply to be received
|
||||
|
||||
// old session should have shared the key at earliest known index now
|
||||
// we should be able to decrypt both
|
||||
testHelper.runBlockingTest {
|
||||
try {
|
||||
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
||||
} catch (error: MXCryptoError) {
|
||||
fail("Should be able to decrypt first event now $error")
|
||||
}
|
||||
}
|
||||
testHelper.runBlockingTest {
|
||||
try {
|
||||
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
|
||||
} catch (error: MXCryptoError) {
|
||||
fail("Should be able to decrypt event $error")
|
||||
testHelper.waitWithLatch {
|
||||
testHelper.retryPeriodicallyWithLatch(it) {
|
||||
val canDecryptFirst = try {
|
||||
testHelper.runBlockingTest {
|
||||
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
|
||||
}
|
||||
true
|
||||
} catch (error: MXCryptoError) {
|
||||
false
|
||||
}
|
||||
val canDecryptSecond = try {
|
||||
testHelper.runBlockingTest {
|
||||
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
|
||||
}
|
||||
true
|
||||
} catch (error: MXCryptoError) {
|
||||
false
|
||||
}
|
||||
canDecryptFirst && canDecryptSecond
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -527,7 +560,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? {
|
||||
aliceRoomPOV.sendTextMessage(text)
|
||||
var sentEventId: String? = null
|
||||
testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
|
||||
testHelper.waitWithLatch(4 * 60_000L) { latch ->
|
||||
val timeline = aliceRoomPOV.createTimeline(null, TimelineSettings(60))
|
||||
timeline.start()
|
||||
|
||||
|
@ -549,6 +582,147 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
return sentEventId
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that if a better key is forwared (lower index, it is then used)
|
||||
*/
|
||||
@Test
|
||||
fun testSelfInteractiveVerificationAndGossip() {
|
||||
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() &&
|
||||
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(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
|
@ -621,10 +795,10 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
testHelper.waitWithLatch { latch ->
|
||||
sentEventIds.forEach { sentEventId ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||
timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
val timeLineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
|
||||
timeLineEvent != null &&
|
||||
timeLineEvent.isEncrypted() &&
|
||||
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -642,7 +816,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
if (expectedError == null) {
|
||||
Assert.assertNotNull(errorType)
|
||||
} else {
|
||||
assertEquals(expectedError, errorType, "Message expected to be UISI")
|
||||
assertEquals(expectedError, errorType, "Unexpected reason")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,10 +50,7 @@ class PreShareKeysTest : InstrumentedTest {
|
|||
// clear any outbound session
|
||||
aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
|
||||
|
||||
val preShareCount = bobSession.cryptoService().getGossipingEvents().count {
|
||||
it.senderId == aliceSession.myUserId &&
|
||||
it.getClearType() == EventType.ROOM_KEY
|
||||
}
|
||||
val preShareCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
|
||||
|
||||
assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount)
|
||||
Log.d("#Test", "Room Key Received from alice $preShareCount")
|
||||
|
@ -65,23 +62,23 @@ class PreShareKeysTest : InstrumentedTest {
|
|||
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val newGossipCount = bobSession.cryptoService().getGossipingEvents().count {
|
||||
it.senderId == aliceSession.myUserId &&
|
||||
it.getClearType() == EventType.ROOM_KEY
|
||||
}
|
||||
newGossipCount > preShareCount
|
||||
val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
|
||||
newKeysCount > preShareCount
|
||||
}
|
||||
}
|
||||
|
||||
val latest = bobSession.cryptoService().getGossipingEvents().lastOrNull {
|
||||
it.senderId == aliceSession.myUserId &&
|
||||
it.getClearType() == EventType.ROOM_KEY
|
||||
}
|
||||
val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
|
||||
val aliceOutboundSessionInRoom = aliceCryptoStore.getCurrentOutboundGroupSessionForRoom(e2eRoomID)!!.outboundGroupSession.sessionIdentifier()
|
||||
|
||||
val content = latest?.getClearContent().toModel<RoomKeyContent>()
|
||||
assertNotNull("Bob should have received and decrypted a room key event from alice", content)
|
||||
assertEquals("Wrong room", e2eRoomID, content!!.roomId)
|
||||
val megolmSessionId = content.sessionId!!
|
||||
val bobCryptoStore = (bobSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
|
||||
val aliceDeviceBobPov = bobCryptoStore.getUserDevice(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!)!!
|
||||
val bobInboundForAlice = bobCryptoStore.getInboundGroupSession(aliceOutboundSessionInRoom, aliceDeviceBobPov.identityKey()!!)
|
||||
assertNotNull("Bob should have received and decrypted a room key event from alice", bobInboundForAlice)
|
||||
assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId)
|
||||
|
||||
val megolmSessionId = bobInboundForAlice.olmInboundGroupSession!!.sessionIdentifier()
|
||||
|
||||
assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId)
|
||||
|
||||
val sharedIndex = aliceSession.cryptoService().getSharedWithInfo(e2eRoomID, megolmSessionId)
|
||||
.getObject(bobSession.myUserId, bobSession.sessionParams.deviceId)
|
||||
|
|
|
@ -41,7 +41,6 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreation
|
|||
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
|
||||
|
@ -187,9 +186,9 @@ class KeyShareTests : InstrumentedTest {
|
|||
Thread.sleep(6_000)
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
aliceSession2.cryptoService().getOutgoingRoomKeyRequests().let {
|
||||
it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == OutgoingGossipingRequestState.CANCELLED }
|
||||
}
|
||||
// It should have been deleted from store
|
||||
val outgoingRoomKeyRequests = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
||||
outgoingRoomKeyRequests.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,12 +35,12 @@ import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo
|
|||
import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequest
|
||||
import org.matrix.android.sdk.internal.crypto.model.AuditTrail
|
||||
|
||||
interface CryptoService {
|
||||
|
||||
|
@ -94,8 +94,6 @@ interface CryptoService {
|
|||
|
||||
fun reRequestRoomKeyForEvent(event: Event)
|
||||
|
||||
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody)
|
||||
|
||||
fun addRoomKeysRequestListener(listener: GossipingRequestListener)
|
||||
|
||||
fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
|
||||
|
@ -142,14 +140,14 @@ interface CryptoService {
|
|||
fun addNewSessionListener(newSessionListener: NewSessionListener)
|
||||
fun removeSessionListener(listener: NewSessionListener)
|
||||
|
||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
||||
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
|
||||
fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest>
|
||||
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>>
|
||||
|
||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
|
||||
|
||||
fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
|
||||
fun getGossipingEvents(): List<Event>
|
||||
fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>>
|
||||
fun getGossipingEvents(): List<AuditTrail>
|
||||
|
||||
// For testing shared session
|
||||
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
|
||||
|
|
|
@ -16,12 +16,17 @@
|
|||
|
||||
package org.matrix.android.sdk.api.session.crypto.model
|
||||
|
||||
enum class OutgoingGossipingRequestState {
|
||||
enum class OutgoingRoomKeyRequestState {
|
||||
UNSENT,
|
||||
SENDING,
|
||||
SENT,
|
||||
CANCELLING,
|
||||
CANCELLED,
|
||||
FAILED_TO_SEND,
|
||||
FAILED_TO_CANCEL
|
||||
CANCELLATION_PENDING,
|
||||
CANCELLATION_PENDING_AND_WILL_RESEND;
|
||||
|
||||
companion object {
|
||||
fun pendingStates() = setOf(
|
||||
UNSENT,
|
||||
CANCELLATION_PENDING_AND_WILL_RESEND,
|
||||
CANCELLATION_PENDING
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.crypto.model
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest
|
||||
|
||||
/**
|
||||
* Represents an outgoing room key request
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OutgoingRoomKeyRequest(
|
||||
// RequestBody
|
||||
val requestBody: RoomKeyRequestBody?,
|
||||
// list of recipients for the request
|
||||
override val recipients: Map<String, List<String>>,
|
||||
// Unique id for this request. Used for both
|
||||
// an id within the request for later pairing with a cancellation, and for
|
||||
// the transaction id when sending the to_device messages to our local
|
||||
override val requestId: String, // current state of this request
|
||||
override val state: OutgoingGossipingRequestState
|
||||
// transaction id for the cancellation, if any
|
||||
// override var cancellationTxnId: String? = null
|
||||
) : OutgoingGossipingRequest {
|
||||
|
||||
/**
|
||||
* Used only for log.
|
||||
*
|
||||
* @return the room id.
|
||||
*/
|
||||
val roomId: String?
|
||||
get() = requestBody?.roomId
|
||||
|
||||
/**
|
||||
* Used only for log.
|
||||
*
|
||||
* @return the session id
|
||||
*/
|
||||
val sessionId: String?
|
||||
get() = requestBody?.sessionId
|
||||
}
|
|
@ -52,7 +52,13 @@ data class RoomKeyWithHeldContent(
|
|||
/**
|
||||
* A human-readable reason for why the key was not sent. The receiving client should only use this string if it does not understand the code.
|
||||
*/
|
||||
@Json(name = "reason") val reason: String? = null
|
||||
@Json(name = "reason") val reason: String? = null,
|
||||
|
||||
/**
|
||||
* the device ID of the device sending the m.room_key.withheld message
|
||||
* MSC3735
|
||||
*/
|
||||
@Json(name = "from_device") val fromDevice: String? = null
|
||||
|
||||
) {
|
||||
val code: WithHeldCode?
|
||||
|
|
|
@ -131,7 +131,7 @@ interface SharedSecretStorageService {
|
|||
|
||||
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?): IntegrityResult
|
||||
|
||||
fun requestSecret(name: String, myOtherDeviceId: String)
|
||||
suspend fun requestSecret(name: String, myOtherDeviceId: String)
|
||||
|
||||
data class KeyRef(
|
||||
val keyId: String?,
|
||||
|
|
|
@ -1,121 +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.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
|
||||
|
||||
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 = System.currentTimeMillis()
|
||||
})
|
||||
|
||||
params.recipients.forEach { userToDeviceMap ->
|
||||
userToDeviceMap.value.forEach { deviceId ->
|
||||
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLING)
|
||||
sendToDeviceTask.execute(
|
||||
SendToDeviceTask.Params(
|
||||
eventType = EventType.ROOM_KEY_REQUEST,
|
||||
contentMap = contentMap,
|
||||
transactionId = 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)
|
||||
}
|
||||
}
|
|
@ -57,15 +57,14 @@ import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo
|
|||
import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||
|
@ -76,11 +75,11 @@ import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
|
|||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||
import org.matrix.android.sdk.internal.crypto.model.AuditTrail
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXKey.Companion.KEY_SIGNED_CURVE_25519_TYPE
|
||||
import org.matrix.android.sdk.internal.crypto.model.toRest
|
||||
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
||||
|
@ -154,7 +153,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
|
||||
private val crossSigningService: DefaultCrossSigningService,
|
||||
//
|
||||
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||
private val incomingKeyRequestManager: IncomingKeyRequestManager,
|
||||
private val secretShareManager: SecretShareManager,
|
||||
//
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
// Actions
|
||||
|
@ -201,7 +201,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val gossipingBuffer = mutableListOf<Event>()
|
||||
// val gossipingBuffer = mutableListOf<Event>()
|
||||
|
||||
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
|
||||
setDeviceNameTask
|
||||
|
@ -377,27 +377,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
// Open the store
|
||||
cryptoStore.open()
|
||||
|
||||
runCatching {
|
||||
// if (isInitialSync) {
|
||||
// // refresh the devices list for each known room members
|
||||
// deviceListManager.invalidateAllDeviceLists()
|
||||
// deviceListManager.refreshOutdatedDeviceLists()
|
||||
// } else {
|
||||
|
||||
// Why would we do that? it will be called at end of syn
|
||||
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||
// }
|
||||
}.fold(
|
||||
{
|
||||
isStarting.set(false)
|
||||
isStarted.set(true)
|
||||
},
|
||||
{
|
||||
isStarting.set(false)
|
||||
isStarted.set(false)
|
||||
Timber.tag(loggerTag.value).e(it, "Start failed")
|
||||
}
|
||||
)
|
||||
isStarting.set(false)
|
||||
isStarted.set(true)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -405,7 +386,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
*/
|
||||
fun close() = runBlocking(coroutineDispatchers.crypto) {
|
||||
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
|
||||
incomingGossipingRequestManager.close()
|
||||
incomingKeyRequestManager.close()
|
||||
outgoingGossipingRequestManager.close()
|
||||
olmDevice.release()
|
||||
cryptoStore.close()
|
||||
}
|
||||
|
@ -470,15 +452,28 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
}
|
||||
|
||||
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
||||
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||
}
|
||||
}
|
||||
|
||||
tryOrNull {
|
||||
gossipingBuffer.toList().let {
|
||||
cryptoStore.saveGossipingEvents(it)
|
||||
// Process pending key requests
|
||||
try {
|
||||
if (toDevices.isEmpty()) {
|
||||
// this is not blocking
|
||||
outgoingGossipingRequestManager.requireProcessAllPendingKeyRequests()
|
||||
} else {
|
||||
Timber.tag(loggerTag.value)
|
||||
.w("Don't process key requests yet as their 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -592,7 +587,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
// (for now at least. Maybe we should alert the user somehow?)
|
||||
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
|
||||
|
||||
if (existingAlgorithm == algorithm && roomEncryptorsStore.get(roomId) != null) {
|
||||
if (existingAlgorithm == algorithm) {
|
||||
// ignore
|
||||
Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in $roomId")
|
||||
return false
|
||||
|
@ -783,19 +778,26 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
when (event.getClearType()) {
|
||||
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
||||
gossipingBuffer.add(event)
|
||||
// gossipingBuffer.add(event)
|
||||
// Keys are imported directly, not waiting for end of sync
|
||||
onRoomKeyEvent(event)
|
||||
}
|
||||
EventType.REQUEST_SECRET,
|
||||
EventType.REQUEST_SECRET -> {
|
||||
secretShareManager.handleSecretRequest(event)
|
||||
// incomingGossipingRequestManager.onGossipingRequestEvent(event)
|
||||
}
|
||||
EventType.ROOM_KEY_REQUEST -> {
|
||||
Timber.w("VALR: key request ${event.getClearContent()}")
|
||||
// save audit trail
|
||||
gossipingBuffer.add(event)
|
||||
// gossipingBuffer.add(event)
|
||||
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
|
||||
incomingGossipingRequestManager.onGossipingRequestEvent(event)
|
||||
Timber.w("VALR: sender Id is ${event.senderId} full ev $event")
|
||||
event.getClearContent().toModel<RoomKeyShareRequest>()?.let { req ->
|
||||
event.senderId?.let { incomingKeyRequestManager.addNewIncomingRequest(it, req) }
|
||||
}
|
||||
}
|
||||
EventType.SEND_SECRET -> {
|
||||
gossipingBuffer.add(event)
|
||||
// gossipingBuffer.add(event)
|
||||
onSecretSendReceived(event)
|
||||
}
|
||||
EventType.ROOM_KEY_WITHHELD -> {
|
||||
|
@ -833,50 +835,46 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
val withHeldContent = event.getClearContent().toModel<RoomKeyWithHeldContent>() ?: return Unit.also {
|
||||
Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields")
|
||||
}
|
||||
Timber.tag(loggerTag.value).i("onKeyWithHeldReceived() received from:${event.senderId}, content <$withHeldContent>")
|
||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm)
|
||||
if (alg is IMXWithHeldExtension) {
|
||||
alg.onRoomKeyWithHeldEvent(withHeldContent)
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).e("onKeyWithHeldReceived() from:${event.senderId}: Unable to handle WithHeldContent for ${withHeldContent.algorithm}")
|
||||
return
|
||||
val senderId = event.senderId ?: return Unit.also {
|
||||
Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields")
|
||||
}
|
||||
withHeldContent.sessionId ?: return
|
||||
withHeldContent.algorithm ?: return
|
||||
withHeldContent.roomId ?: return
|
||||
withHeldContent.senderKey ?: return
|
||||
outgoingGossipingRequestManager.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()
|
||||
)
|
||||
)
|
||||
// Timber.tag(loggerTag.value).i("onKeyWithHeldReceived() received from:${event.senderId}, content <$withHeldContent>")
|
||||
// val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm)
|
||||
// if (alg is IMXWithHeldExtension) {
|
||||
// alg.onRoomKeyWithHeldEvent(senderId, withHeldContent)
|
||||
// } else {
|
||||
// Timber.tag(loggerTag.value).e("onKeyWithHeldReceived() from:${event.senderId}: Unable to handle WithHeldContent for ${withHeldContent.algorithm}")
|
||||
// return
|
||||
// }
|
||||
}
|
||||
|
||||
private fun onSecretSendReceived(event: Event) {
|
||||
Timber.tag(loggerTag.value).i("GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}")
|
||||
if (!event.isEncrypted()) {
|
||||
// secret send messages must be encrypted
|
||||
Timber.tag(loggerTag.value).e("GOSSIP onSecretSend() :Received unencrypted secret send event")
|
||||
return
|
||||
}
|
||||
|
||||
// Was that sent by us?
|
||||
if (event.senderId != userId) {
|
||||
Timber.tag(loggerTag.value).e("GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}")
|
||||
return
|
||||
}
|
||||
|
||||
val secretContent = event.getClearContent().toModel<SecretSendEventContent>() ?: return
|
||||
|
||||
val existingRequest = cryptoStore
|
||||
.getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId }
|
||||
|
||||
if (existingRequest == null) {
|
||||
Timber.tag(loggerTag.value).i("GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
|
||||
return
|
||||
}
|
||||
|
||||
if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) {
|
||||
// TODO Ask to application layer?
|
||||
Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK")
|
||||
private suspend fun onSecretSendReceived(event: Event) {
|
||||
secretShareManager.onSecretSendReceived(event) { secretName, secretValue ->
|
||||
handleSDKLevelGossip(secretName, secretValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if handled by SDK, otherwise should be sent to application layer
|
||||
*/
|
||||
private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean {
|
||||
private fun handleSDKLevelGossip(secretName: String?,
|
||||
secretValue: String): Boolean {
|
||||
return when (secretName) {
|
||||
MASTER_KEY_SSSS_NAME -> {
|
||||
crossSigningService.onSecretMSKGossip(secretValue)
|
||||
|
@ -1154,26 +1152,32 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
setRoomBlacklistUnverifiedDevices(roomId, false)
|
||||
}
|
||||
|
||||
// TODO Check if this method is still necessary
|
||||
/**
|
||||
* Cancel any earlier room key request
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
*/
|
||||
override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||
outgoingGossipingRequestManager.cancelRoomKeyRequest(requestBody)
|
||||
}
|
||||
|
||||
/**
|
||||
* Re request the encryption keys required to decrypt an event.
|
||||
*
|
||||
* @param event the event to decrypt again.
|
||||
*/
|
||||
override fun reRequestRoomKeyForEvent(event: Event) {
|
||||
val sender = event.senderId ?: return
|
||||
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
|
||||
Timber.tag(loggerTag.value).e("reRequestRoomKeyForEvent Failed to re-request key, null content")
|
||||
}
|
||||
|
||||
val recipients = if (event.senderId == userId) {
|
||||
mapOf(
|
||||
userId to listOf("*")
|
||||
)
|
||||
} else {
|
||||
// for the case where you share the key with a device that has a broken olm session
|
||||
// The other user might Re-shares a megolm session key with devices if the key has already been
|
||||
// sent to them.
|
||||
mapOf(
|
||||
userId to listOf("*"),
|
||||
// TODO 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(wireContent.deviceId ?: "*")
|
||||
)
|
||||
}
|
||||
val requestBody = RoomKeyRequestBody(
|
||||
algorithm = wireContent.algorithm,
|
||||
roomId = event.roomId,
|
||||
|
@ -1181,7 +1185,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
sessionId = wireContent.sessionId
|
||||
)
|
||||
|
||||
outgoingGossipingRequestManager.resendRoomKeyRequest(requestBody)
|
||||
outgoingGossipingRequestManager.postRoomKeyRequest(requestBody, recipients, true)
|
||||
}
|
||||
|
||||
override fun requestRoomKeyForEvent(event: Event) {
|
||||
|
@ -1208,7 +1212,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
* @param listener listener
|
||||
*/
|
||||
override fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||
incomingGossipingRequestManager.addRoomKeysRequestListener(listener)
|
||||
incomingKeyRequestManager.addRoomKeysRequestListener(listener)
|
||||
// TODO same for secret manager
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1217,7 +1222,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
* @param listener listener
|
||||
*/
|
||||
override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||
incomingGossipingRequestManager.removeRoomKeysRequestListener(listener)
|
||||
incomingKeyRequestManager.removeRoomKeysRequestListener(listener)
|
||||
// TODO same for secret manager
|
||||
}
|
||||
|
||||
// private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
|
||||
|
@ -1298,11 +1304,11 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
return "DefaultCryptoService of $userId ($deviceId)"
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> {
|
||||
override fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest> {
|
||||
return cryptoStore.getOutgoingRoomKeyRequests()
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
|
||||
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>> {
|
||||
return cryptoStore.getOutgoingRoomKeyRequestsPaged()
|
||||
}
|
||||
|
||||
|
@ -1314,11 +1320,11 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
return cryptoStore.getIncomingRoomKeyRequests()
|
||||
}
|
||||
|
||||
override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> {
|
||||
override fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>> {
|
||||
return cryptoStore.getGossipingEventsTrail()
|
||||
}
|
||||
|
||||
override fun getGossipingEvents(): List<Event> {
|
||||
override fun getGossipingEvents(): List<AuditTrail> {
|
||||
return cryptoStore.getGossipingEvents()
|
||||
}
|
||||
|
||||
|
@ -1342,8 +1348,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
|
||||
} catch (failure: Throwable) {
|
||||
Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members")
|
||||
callback.onFailure(failure)
|
||||
return@launch
|
||||
// callback.onFailure(failure)
|
||||
// return@launch
|
||||
}
|
||||
|
||||
val userIds = getRoomUserIds(roomId)
|
||||
|
|
|
@ -1,57 +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.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 instanciated
|
||||
val queueSuffixApp = System.currentTimeMillis()
|
||||
|
||||
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,472 +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.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 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] = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean {
|
||||
val verifTimestamp: Long?
|
||||
synchronized(recentlyVerifiedDevices) {
|
||||
verifTimestamp = recentlyVerifiedDevices[deviceId]
|
||||
}
|
||||
if (verifTimestamp == null) return false
|
||||
|
||||
val age = System.currentTimeMillis() - 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 { System.currentTimeMillis() - it }
|
||||
when (roomKeyShare?.action) {
|
||||
GossipingToDeviceObject.ACTION_SHARE_REQUEST -> {
|
||||
if (event.getClearType() == EventType.REQUEST_SECRET) {
|
||||
IncomingSecretShareRequest.fromEvent(event)?.let {
|
||||
if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) {
|
||||
// ignore, it was sent by me as *
|
||||
Timber.v("## 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)?.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)?.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,364 @@
|
|||
/*
|
||||
* 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.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.MXUsersDevicesMap
|
||||
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 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 messageEncrypter: MessageEncrypter,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val sendToDeviceTask: SendToDeviceTask) {
|
||||
|
||||
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 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
|
||||
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(
|
||||
requestingUserId = senderId,
|
||||
requestingDeviceId = deviceId,
|
||||
roomId = roomId,
|
||||
senderKey = senderKey,
|
||||
sessionId = sessionId,
|
||||
action = action
|
||||
)
|
||||
}
|
||||
|
||||
fun addNewIncomingRequest(senderId: String, request: RoomKeyShareRequest) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when (validMegolmRequest.action) {
|
||||
MegolmRequestAction.Request -> {
|
||||
// it's already in buffer, nop keep existing
|
||||
}
|
||||
MegolmRequestAction.Cancel -> {
|
||||
// discard the request in buffer
|
||||
incomingRequestBuffer.remove(existing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.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 {
|
||||
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 {
|
||||
sendWithheldForRequest(request, WithHeldCode.UNAUTHORISED)
|
||||
// TODO if it's our device we could delegate to the app layer to decide?
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendWithheldForRequest(request: ValidMegolmRequestBody, code: WithHeldCode) {
|
||||
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(
|
||||
request.roomId,
|
||||
request.sessionId,
|
||||
request.senderKey,
|
||||
MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
code,
|
||||
request.requestingUserId,
|
||||
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}")
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
// TODO
|
||||
gossipingRequestListeners.add(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
|
||||
synchronized(gossipingRequestListeners) {
|
||||
// TODO
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,7 +42,6 @@ import org.matrix.olm.OlmOutboundGroupSession
|
|||
import org.matrix.olm.OlmSession
|
||||
import org.matrix.olm.OlmUtility
|
||||
import timber.log.Timber
|
||||
import java.net.URLEncoder
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO)
|
||||
|
@ -329,13 +328,13 @@ internal class MXOlmDevice @Inject constructor(
|
|||
Timber.tag(loggerTag.value).e(e, "## createInboundSession() : removeOneTimeKeys failed")
|
||||
}
|
||||
|
||||
Timber.tag(loggerTag.value).v("## createInboundSession() : ciphertext: $ciphertext")
|
||||
try {
|
||||
val sha256 = olmUtility!!.sha256(URLEncoder.encode(ciphertext, "utf-8"))
|
||||
Timber.tag(loggerTag.value).v("## createInboundSession() :ciphertext: SHA256: $sha256")
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(loggerTag.value).e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext")
|
||||
}
|
||||
// Timber.tag(loggerTag.value).v("## createInboundSession() : ciphertext: $ciphertext.")
|
||||
// try {
|
||||
// val sha256 = olmUtility!!.sha256(URLEncoder.encode(ciphertext, "utf-8"))
|
||||
// Timber.tag(loggerTag.value).v("## createInboundSession() :ciphertext: SHA256: $sha256")
|
||||
// } catch (e: Exception) {
|
||||
// Timber.tag(loggerTag.value).e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext")
|
||||
// }
|
||||
|
||||
val olmMessage = OlmMessage()
|
||||
olmMessage.mCipherText = ciphertext
|
||||
|
@ -787,7 +786,7 @@ internal class MXOlmDevice @Inject constructor(
|
|||
|
||||
if (timelineSet.contains(messageIndexKey)) {
|
||||
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
|
||||
Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason")
|
||||
Timber.tag(loggerTag.value).e("## decryptGroupMessage() timelineId=$timeline: $reason")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
|
||||
}
|
||||
|
||||
|
|
|
@ -16,12 +16,10 @@
|
|||
|
||||
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
|
||||
interface OutgoingGossipingRequest {
|
||||
var recipients: Map<String, List<String>>
|
||||
var requestId: String
|
||||
var state: OutgoingRoomKeyRequestState
|
||||
// transaction id for the cancellation, if any
|
||||
// var cancellationTxnId: String?
|
||||
}
|
||||
|
|
|
@ -17,151 +17,327 @@
|
|||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
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.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
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.OutgoingRoomKeyRequestState
|
||||
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.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.crypto.tasks.DefaultSendToDeviceTask
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||
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 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("OutgoingGossipingRequestManager", 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 OutgoingGossipingRequestManager @Inject constructor(
|
||||
@SessionId private val sessionId: String,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val gossipingWorkManager: GossipingWorkManager) {
|
||||
private val sendToDeviceTask: DefaultSendToDeviceTask,
|
||||
private val perSessionBackupQueryRateLimiter: PerSessionBackupQueryRateLimiter) {
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher)
|
||||
private val sequencer = SemaphoreCoroutineSequencer()
|
||||
|
||||
sendOutgoingGossipingRequest(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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<String>()
|
||||
|
||||
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)
|
||||
fun postRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>, force: Boolean = false) {
|
||||
outgoingRequestScope.launch {
|
||||
sequencer.post {
|
||||
internalQueueRequest(requestBody, recipients, force)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel room key requests, if any match the given details
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
* Typically called when we the session as been imported or received meanwhile
|
||||
*/
|
||||
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
|
||||
cancelRoomKeyRequest(requestBody, false)
|
||||
fun postCancelRequestForSessionIfNeeded(sessionId: String, roomId: String, senderKey: String) {
|
||||
outgoingRequestScope.launch {
|
||||
sequencer.post {
|
||||
internalQueueCancelRequest(sessionId, roomId, senderKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onRoomKeyForwarded(sessionId: String,
|
||||
algorithm: String,
|
||||
roomId: String,
|
||||
senderKey: String,
|
||||
fromDevice: String?,
|
||||
event: Event) {
|
||||
Timber.tag(loggerTag.value).v("Key forwarded for $sessionId from ${event.senderId}|$fromDevice")
|
||||
outgoingRequestScope.launch {
|
||||
sequencer.post {
|
||||
cryptoStore.updateOutgoingRoomKeyReply(
|
||||
roomId,
|
||||
sessionId,
|
||||
algorithm,
|
||||
senderKey,
|
||||
fromDevice,
|
||||
event
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
cryptoStore.updateOutgoingRoomKeyReply(
|
||||
roomId,
|
||||
sessionId,
|
||||
algorithm,
|
||||
senderKey,
|
||||
fromDevice,
|
||||
event
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel room key requests, if any match the given details, and resend
|
||||
*
|
||||
* @param requestBody requestBody
|
||||
* Should be called after a sync, ideally if no catchup sync needed (as keys might arrive in those)
|
||||
*/
|
||||
fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
|
||||
cancelRoomKeyRequest(requestBody, true)
|
||||
fun requireProcessAllPendingKeyRequests() {
|
||||
outgoingRequestScope.launch {
|
||||
sequencer.post {
|
||||
internalProcessPendingKeyRequests()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
private fun internalQueueCancelRequest(sessionId: String, roomId: String, senderKey: String) {
|
||||
// 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,
|
||||
keyShareRequest = request as? OutgoingRoomKeyRequest,
|
||||
secretShareRequest = request as? OutgoingSecretRequest,
|
||||
txnId = createUniqueTxnId()
|
||||
senderKey = senderKey
|
||||
)
|
||||
cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.SENDING)
|
||||
val workRequest = gossipingWorkManager.createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
|
||||
gossipingWorkManager.postWork(workRequest)
|
||||
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 -> {
|
||||
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
|
||||
}
|
||||
OutgoingRoomKeyRequestState.SENT -> {
|
||||
// It was already sent, so cancel
|
||||
cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING)
|
||||
}
|
||||
OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> {
|
||||
// It is already marked to be cancelled
|
||||
}
|
||||
OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> {
|
||||
// we just want to cancel now
|
||||
cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
fun close() {
|
||||
try {
|
||||
outgoingRequestScope.cancel("User Terminate")
|
||||
requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.clear()
|
||||
} catch (failure: Throwable) {
|
||||
Timber.tag(loggerTag.value).w("Failed to shutDown request manager")
|
||||
}
|
||||
}
|
||||
|
||||
val workRequest = gossipingWorkManager.createWork<CancelGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
|
||||
gossipingWorkManager.postWork(workRequest)
|
||||
private fun internalQueueRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>, force: Boolean) {
|
||||
Timber.tag(loggerTag.value).d("Queueing key request for ${requestBody.sessionId}")
|
||||
val existing = cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients)
|
||||
when (existing.state) {
|
||||
OutgoingRoomKeyRequestState.UNSENT -> {
|
||||
// nothing it's new or not yet handled
|
||||
}
|
||||
OutgoingRoomKeyRequestState.SENT -> {
|
||||
// it was already requested
|
||||
Timber.tag(loggerTag.value).w("The session ${requestBody.sessionId} is already requested")
|
||||
if (force) {
|
||||
// update to UNSENT
|
||||
Timber.tag(loggerTag.value).w(".. force to request ${requestBody.sessionId}")
|
||||
cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND)
|
||||
} else {
|
||||
requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(existing.requestId)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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 -> {
|
||||
// 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 {
|
||||
val req = cryptoStore.getOutgoingRoomKeyRequest(it)
|
||||
val sessionId = req?.sessionId ?: return@let
|
||||
val roomId = req.roomId ?: return@let
|
||||
// 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)) {
|
||||
// we found the key in backup, 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 can forget about it
|
||||
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
|
||||
// TODO update the audit trail
|
||||
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)) {
|
||||
// we have to create a new unsent one
|
||||
val body = request.requestBody ?: return
|
||||
// this will create a new unsent request that will be process in the following call
|
||||
cryptoStore.getOrAddOutgoingRoomKeyRequest(body, request.recipients)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequestState
|
||||
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 {
|
||||
object Success : 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>>,
|
||||
// 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
|
||||
}
|
|
@ -17,7 +17,7 @@
|
|||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequestState
|
||||
|
||||
/**
|
||||
* Represents an outgoing room key request
|
||||
|
@ -33,7 +33,7 @@ internal class OutgoingSecretRequest(
|
|||
// 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 {
|
||||
override var state: OutgoingRoomKeyRequestState) : OutgoingGossipingRequest {
|
||||
|
||||
// transaction id for the cancellation, if any
|
||||
}
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* 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.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
||||
import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.store.SavedKeyBackupKeyInfo
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
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
|
||||
) {
|
||||
|
||||
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 = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
/**
|
||||
* 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
|
||||
var now = System.currentTimeMillis()
|
||||
|
||||
private 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)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,11 +17,19 @@
|
|||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class RoomEncryptorsStore @Inject constructor() {
|
||||
internal class RoomEncryptorsStore @Inject constructor(
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
// Repository
|
||||
private val megolmEncryptionFactory: MXMegolmEncryptionFactory,
|
||||
private val olmEncryptionFactory: MXOlmEncryptionFactory,
|
||||
) {
|
||||
|
||||
// MXEncrypting instance for each room.
|
||||
private val roomEncryptors = mutableMapOf<String, IMXEncrypting>()
|
||||
|
@ -34,7 +42,18 @@ internal class RoomEncryptorsStore @Inject constructor() {
|
|||
|
||||
fun get(roomId: String): IMXEncrypting? {
|
||||
return synchronized(roomEncryptors) {
|
||||
roomEncryptors[roomId]
|
||||
val cache = roomEncryptors[roomId]
|
||||
if (cache != null) {
|
||||
return@synchronized cache
|
||||
} else {
|
||||
val alg: IMXEncrypting? = when (cryptoStore.getRoomAlgorithm(roomId)) {
|
||||
MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId)
|
||||
MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId)
|
||||
else -> null
|
||||
}
|
||||
alg?.let { roomEncryptors.put(roomId, it) }
|
||||
return@synchronized alg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,274 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package 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.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.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
|
||||
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 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
|
||||
) {
|
||||
|
||||
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>()
|
||||
|
||||
/**
|
||||
* 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] = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 secret name")
|
||||
}
|
||||
|
||||
if (userId != credentials.userId) {
|
||||
// secrets are only shared between our own devices
|
||||
Timber.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.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.e("The secret is unknown $secretName")
|
||||
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.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 = System.currentTimeMillis() - 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,151 +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.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
|
||||
|
||||
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 = System.currentTimeMillis()
|
||||
})
|
||||
|
||||
params.keyShareRequest.recipients.forEach { userToDeviceMap ->
|
||||
userToDeviceMap.value.forEach { deviceId ->
|
||||
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
params.secretShareRequest != null -> {
|
||||
eventType = EventType.REQUEST_SECRET
|
||||
requestId = params.secretShareRequest.requestId
|
||||
val toDeviceContent = SecretShareRequest(
|
||||
requestingDeviceId = credentials.deviceId,
|
||||
requestId = params.secretShareRequest.requestId,
|
||||
action = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
|
||||
secretName = params.secretShareRequest.secretName
|
||||
)
|
||||
|
||||
cryptoStore.saveGossipingEvent(Event(
|
||||
type = eventType,
|
||||
content = toDeviceContent.toContent(),
|
||||
senderId = credentials.userId
|
||||
).also {
|
||||
it.ageLocalTs = System.currentTimeMillis()
|
||||
})
|
||||
|
||||
params.secretShareRequest.recipients.forEach { userToDeviceMap ->
|
||||
userToDeviceMap.value.forEach { deviceId ->
|
||||
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
return 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,168 +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.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
|
||||
|
||||
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 = System.currentTimeMillis()
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.crypto.actions
|
|||
import androidx.annotation.WorkerThread
|
||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||
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.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
||||
|
@ -29,6 +29,8 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
|||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("MegolmSessionDataImporter", LoggerTag.CRYPTO)
|
||||
|
||||
internal class MegolmSessionDataImporter @Inject constructor(private val olmDevice: MXOlmDevice,
|
||||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
|
@ -62,19 +64,18 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi
|
|||
if (null != decrypting) {
|
||||
try {
|
||||
val sessionId = megolmSessionData.sessionId
|
||||
Timber.v("## importRoomKeys retrieve senderKey " + megolmSessionData.senderKey + " sessionId " + sessionId)
|
||||
Timber.tag(loggerTag.value).v("## importRoomKeys retrieve senderKey ${megolmSessionData.senderKey} sessionId $sessionId")
|
||||
|
||||
totalNumbersOfImportedKeys++
|
||||
|
||||
// cancel any outstanding room key requests for this session
|
||||
val roomKeyRequestBody = RoomKeyRequestBody(
|
||||
algorithm = megolmSessionData.algorithm,
|
||||
roomId = megolmSessionData.roomId,
|
||||
senderKey = megolmSessionData.senderKey,
|
||||
sessionId = megolmSessionData.sessionId
|
||||
)
|
||||
|
||||
outgoingGossipingRequestManager.cancelRoomKeyRequest(roomKeyRequestBody)
|
||||
Timber.tag(loggerTag.value).d("Imported megolm session $sessionId from backup=$fromBackup in ${megolmSessionData.roomId}")
|
||||
outgoingGossipingRequestManager.postCancelRequestForSessionIfNeeded(
|
||||
megolmSessionData.sessionId ?: "",
|
||||
megolmSessionData.roomId ?: "",
|
||||
megolmSessionData.senderKey ?: ""
|
||||
)
|
||||
|
||||
// Have another go at decrypting events sent with this session
|
||||
when (decrypting) {
|
||||
|
@ -83,7 +84,7 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi
|
|||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## importRoomKeys() : onNewSession failed")
|
||||
Timber.tag(loggerTag.value).e(e, "## importRoomKeys() : onNewSession failed")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,7 +106,7 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi
|
|||
|
||||
val t1 = System.currentTimeMillis()
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -33,7 +33,6 @@ import org.matrix.android.sdk.api.session.events.model.Event
|
|||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||
|
@ -41,7 +40,6 @@ import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
|||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
|
||||
|
@ -61,7 +59,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val liveEventManager: Lazy<StreamEventsManager>
|
||||
) : IMXDecrypting, IMXWithHeldExtension {
|
||||
) : IMXDecrypting {
|
||||
|
||||
var newSessionListener: NewSessionListener? = null
|
||||
|
||||
|
@ -192,8 +190,10 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
*/
|
||||
override fun requestKeysForEvent(event: Event, withHeld: Boolean) {
|
||||
val sender = event.senderId ?: return
|
||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
||||
val senderDevice = encryptedEventContent?.deviceId ?: return
|
||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
|
||||
Timber.tag(loggerTag.value).e("requestRoomKeyForEvent Failed to re-request key, null content")
|
||||
}
|
||||
val senderDevice = encryptedEventContent.deviceId
|
||||
|
||||
val recipients = if (event.senderId == userId || withHeld) {
|
||||
mapOf(
|
||||
|
@ -205,7 +205,10 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
// sent to them.
|
||||
mapOf(
|
||||
userId to listOf("*"),
|
||||
sender to listOf(senderDevice)
|
||||
|
||||
// TODO 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 ?: "*")
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -216,7 +219,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
sessionId = encryptedEventContent.sessionId
|
||||
)
|
||||
|
||||
outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients)
|
||||
outgoingGossipingRequestManager.postRoomKeyRequest(requestBody, recipients, force = withHeld)
|
||||
}
|
||||
|
||||
// /**
|
||||
|
@ -286,6 +289,16 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
}
|
||||
|
||||
keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key
|
||||
|
||||
val fromDevice = event.getSenderKey()?.let { cryptoStore.deviceWithIdentityKey(it) }?.deviceId
|
||||
|
||||
outgoingGossipingRequestManager.onRoomKeyForwarded(
|
||||
sessionId = roomKeyContent.sessionId,
|
||||
algorithm = roomKeyContent.algorithm ?: "",
|
||||
roomId = roomKeyContent.roomId,
|
||||
senderKey = forwardedRoomKeyContent.senderKey ?: "",
|
||||
fromDevice = fromDevice,
|
||||
event = event)
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
|
||||
if (null == senderKey) {
|
||||
|
@ -307,16 +320,11 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
exportFormat)
|
||||
|
||||
if (added) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}")
|
||||
defaultKeysBackupService.maybeBackupKeys()
|
||||
|
||||
val content = RoomKeyRequestBody(
|
||||
algorithm = roomKeyContent.algorithm,
|
||||
roomId = roomKeyContent.roomId,
|
||||
sessionId = roomKeyContent.sessionId,
|
||||
senderKey = senderKey
|
||||
)
|
||||
|
||||
outgoingGossipingRequestManager.cancelRoomKeyRequest(content)
|
||||
outgoingGossipingRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, senderKey)
|
||||
|
||||
onNewSession(roomKeyContent.roomId, senderKey, roomKeyContent.sessionId)
|
||||
}
|
||||
|
@ -401,9 +409,4 @@ internal class MXMegolmDecryption(private val userId: String,
|
|||
}
|
||||
}
|
||||
|
||||
override fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
cryptoStore.addWithHeldMegolmSession(withHeldInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
|||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.api.session.crypto.model.forEach
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||
|
@ -281,25 +280,14 @@ internal class MXMegolmEncryption(
|
|||
// attempted to share with) rather than the contentMap (those we did
|
||||
// share with), because we don't want to try to claim a one-time-key
|
||||
// for dead devices on every message.
|
||||
val gossipingEventBuffer = arrayListOf<Event>()
|
||||
for ((userId, devicesToShareWith) in devicesByUser) {
|
||||
for (deviceInfo in devicesToShareWith) {
|
||||
session.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex)
|
||||
gossipingEventBuffer.add(
|
||||
Event(
|
||||
type = EventType.ROOM_KEY,
|
||||
senderId = myUserId,
|
||||
content = submap.apply {
|
||||
this["session_key"] = ""
|
||||
// we add a fake key for trail
|
||||
this["_dest"] = "$userId|${deviceInfo.deviceId}"
|
||||
}
|
||||
))
|
||||
// XXX is it needed to add it to the audit trail?
|
||||
// For now decided that no, we are more interested by forward trail
|
||||
}
|
||||
}
|
||||
|
||||
cryptoStore.saveGossipingEvents(gossipingEventBuffer)
|
||||
|
||||
if (haveTargets) {
|
||||
t0 = System.currentTimeMillis()
|
||||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target")
|
||||
|
|
|
@ -808,12 +808,16 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
))
|
||||
} else if (roomId != null) {
|
||||
// Get all keys for the room
|
||||
val data = getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version))
|
||||
val data = withContext(coroutineDispatchers.io) {
|
||||
getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version))
|
||||
}
|
||||
// Convert to KeysBackupData
|
||||
KeysBackupData(mutableMapOf(roomId to data))
|
||||
} else {
|
||||
// Get all keys
|
||||
getSessionsDataTask.execute(GetSessionsDataTask.Params(version))
|
||||
withContext(coroutineDispatchers.io) {
|
||||
getSessionsDataTask.execute(GetSessionsDataTask.Params(version))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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.model
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.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
|
||||
|
||||
@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
|
||||
) : AuditInfo
|
||||
|
||||
data class AuditTrail(
|
||||
val ageLocalTs: Long,
|
||||
val type: TrailType,
|
||||
val info: AuditInfo
|
||||
)
|
|
@ -40,7 +40,7 @@ import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec
|
|||
import org.matrix.android.sdk.api.session.securestorage.SsssPassphrase
|
||||
import org.matrix.android.sdk.api.util.fromBase64
|
||||
import org.matrix.android.sdk.api.util.toBase64NoPadding
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.generatePrivateKeyWithPassword
|
||||
import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256
|
||||
import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption
|
||||
|
@ -57,7 +57,7 @@ import kotlin.experimental.and
|
|||
internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
private val accountDataService: SessionAccountDataService,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val secretShareManager: SecretShareManager,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope
|
||||
) : SharedSecretStorageService {
|
||||
|
@ -378,10 +378,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||
return IntegrityResult.Success(keyInfo.content.passphrase != null)
|
||||
}
|
||||
|
||||
override fun requestSecret(name: String, myOtherDeviceId: String) {
|
||||
outgoingGossipingRequestManager.sendSecretShareRequest(
|
||||
name,
|
||||
mapOf(userId to listOf(myOtherDeviceId))
|
||||
)
|
||||
override suspend fun requestSecret(name: String, myOtherDeviceId: String) {
|
||||
secretShareManager.requestSecretTo(myOtherDeviceId, name)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,14 +28,16 @@ import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
|||
import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
|
||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequestState
|
||||
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.content.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequest
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest
|
||||
import org.matrix.android.sdk.internal.crypto.model.AuditTrail
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
|
||||
|
@ -377,7 +379,9 @@ internal interface IMXCryptoStore {
|
|||
* @param requestBody the request body
|
||||
* @return an OutgoingRoomKeyRequest instance or null
|
||||
*/
|
||||
fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest?
|
||||
fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest?
|
||||
fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest?
|
||||
fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List<OutgoingKeyRequest>
|
||||
|
||||
/**
|
||||
* Look for an existing outgoing room key request, and if none is found, add a new one.
|
||||
|
@ -385,14 +389,53 @@ internal interface IMXCryptoStore {
|
|||
* @param request the request
|
||||
* @return either the same instance as passed in, or the existing one.
|
||||
*/
|
||||
fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>): OutgoingRoomKeyRequest?
|
||||
fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>): OutgoingKeyRequest
|
||||
fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState)
|
||||
fun updateOutgoingRoomKeyReply(
|
||||
roomId: String,
|
||||
sessionId: String,
|
||||
algorithm: String,
|
||||
senderKey: String, fromDevice: String?, event: Event)
|
||||
|
||||
fun deleteOutgoingRoomKeyRequest(requestId: String)
|
||||
|
||||
fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest?
|
||||
|
||||
@Deprecated("TODO")
|
||||
fun saveGossipingEvent(event: Event) = saveGossipingEvents(listOf(event))
|
||||
|
||||
@Deprecated("TODO")
|
||||
fun saveGossipingEvents(events: List<Event>)
|
||||
|
||||
fun saveIncomingKeyRequestAuditTrail(
|
||||
roomId: String,
|
||||
sessionId: String,
|
||||
senderKey: String,
|
||||
algorithm: String,
|
||||
fromUser: String,
|
||||
fromDevice: String
|
||||
)
|
||||
|
||||
fun saveWithheldAuditTrail(
|
||||
roomId: String,
|
||||
sessionId: String,
|
||||
senderKey: String,
|
||||
algorithm: String,
|
||||
code: WithHeldCode,
|
||||
userId: String,
|
||||
deviceId: String
|
||||
)
|
||||
|
||||
fun saveForwardKeyAuditTrail(
|
||||
roomId: String,
|
||||
sessionId: String,
|
||||
senderKey: String,
|
||||
algorithm: String,
|
||||
userId: String,
|
||||
deviceId: String,
|
||||
chainIndex: Long?
|
||||
)
|
||||
|
||||
fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) {
|
||||
updateGossipingRequestState(
|
||||
requestUserId = request.userId,
|
||||
|
@ -417,7 +460,7 @@ internal interface IMXCryptoStore {
|
|||
*/
|
||||
fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest?
|
||||
|
||||
fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState)
|
||||
fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingRoomKeyRequestState)
|
||||
|
||||
fun addNewSessionListener(listener: NewSessionListener)
|
||||
|
||||
|
@ -477,17 +520,18 @@ internal interface IMXCryptoStore {
|
|||
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
|
||||
// Dev tools
|
||||
|
||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
||||
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
|
||||
fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest>
|
||||
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>>
|
||||
fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest>
|
||||
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
|
||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
|
||||
fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
|
||||
fun getGossipingEvents(): List<Event>
|
||||
fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>>
|
||||
fun getGossipingEvents(): List<AuditTrail>
|
||||
|
||||
fun setDeviceKeysUploaded(uploaded: Boolean)
|
||||
fun areDeviceKeysUploaded(): Boolean
|
||||
fun tidyUpDataBase()
|
||||
fun logDbUsageInfo()
|
||||
fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest>
|
||||
}
|
||||
|
|
|
@ -24,9 +24,11 @@ import com.zhuinden.monarchy.Monarchy
|
|||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.Sort
|
||||
import io.realm.kotlin.createObject
|
||||
import io.realm.kotlin.where
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
|
@ -39,22 +41,31 @@ 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.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.OutgoingRoomKeyRequestState
|
||||
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.content.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.internal.crypto.GossipRequestType
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequest
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest
|
||||
import org.matrix.android.sdk.internal.crypto.model.AuditTrail
|
||||
import org.matrix.android.sdk.internal.crypto.model.ForwardInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.IncomingKeyRequestInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.model.TrailType
|
||||
import org.matrix.android.sdk.internal.crypto.model.WithheldInfo
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailMapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper
|
||||
|
@ -74,8 +85,8 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSess
|
|||
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
|
||||
|
@ -105,6 +116,8 @@ import java.util.concurrent.Executors
|
|||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("RealmCryptoStore", LoggerTag.CRYPTO)
|
||||
|
||||
@SessionScope
|
||||
internal class RealmCryptoStore @Inject constructor(
|
||||
@CryptoDatabase private val realmConfiguration: RealmConfiguration,
|
||||
|
@ -1006,12 +1019,11 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
?: defaultValue
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? {
|
||||
override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest? {
|
||||
return monarchy.fetchAllCopiedSync { realm ->
|
||||
realm.where<OutgoingGossipingRequestEntity>()
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
}.mapNotNull {
|
||||
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||
realm.where<OutgoingKeyRequestEntity>()
|
||||
}.map {
|
||||
it.toOutgoingGossipingRequest()
|
||||
}.firstOrNull {
|
||||
it.requestBody?.algorithm == requestBody.algorithm &&
|
||||
it.requestBody?.roomId == requestBody.roomId &&
|
||||
|
@ -1020,16 +1032,40 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? {
|
||||
override fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest? {
|
||||
return monarchy.fetchAllCopiedSync { realm ->
|
||||
realm.where<OutgoingGossipingRequestEntity>()
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName)
|
||||
}.mapNotNull {
|
||||
it.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
||||
realm.where<OutgoingKeyRequestEntity>()
|
||||
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
|
||||
}.map {
|
||||
it.toOutgoingGossipingRequest()
|
||||
}.firstOrNull()
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List<OutgoingKeyRequest> {
|
||||
// TODO this annoying we have to load all
|
||||
return monarchy.fetchAllCopiedSync { realm ->
|
||||
realm.where<OutgoingKeyRequestEntity>()
|
||||
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId)
|
||||
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId)
|
||||
}.map {
|
||||
it.toOutgoingGossipingRequest()
|
||||
}.filter {
|
||||
it.requestBody?.algorithm == algorithm &&
|
||||
it.requestBody?.senderKey == senderKey
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? {
|
||||
// return monarchy.fetchAllCopiedSync { realm ->
|
||||
// realm.where<OutgoingGossipingRequestEntity>()
|
||||
// .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
|
||||
// .equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName)
|
||||
// }.mapNotNull {
|
||||
// it.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
||||
// }.firstOrNull()
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
|
||||
return monarchy.fetchAllCopiedSync { realm ->
|
||||
realm.where<IncomingGossipingRequestEntity>()
|
||||
|
@ -1066,11 +1102,26 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> {
|
||||
override fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>> {
|
||||
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||
realm.where<GossipingEventEntity>().sort(GossipingEventEntityFields.AGE_LOCAL_TS, Sort.DESCENDING)
|
||||
realm.where<AuditTrailEntity>().sort(AuditTrailEntityFields.AGE_LOCAL_TS, Sort.DESCENDING)
|
||||
}
|
||||
val dataSourceFactory = realmDataSourceFactory.map {
|
||||
AuditTrailMapper.map(it)
|
||||
// mm we can't map not null...
|
||||
?: AuditTrail(
|
||||
System.currentTimeMillis(),
|
||||
TrailType.IncomingKeyRequest,
|
||||
IncomingKeyRequestInfo(
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
)
|
||||
}
|
||||
val dataSourceFactory = realmDataSourceFactory.map { it.toModel() }
|
||||
val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory,
|
||||
LivePagedListBuilder(dataSourceFactory,
|
||||
PagedList.Config.Builder()
|
||||
|
@ -1082,25 +1133,32 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
return trail
|
||||
}
|
||||
|
||||
override fun getGossipingEvents(): List<Event> {
|
||||
override fun getGossipingEvents(): List<AuditTrail> {
|
||||
return monarchy.fetchAllCopiedSync { realm ->
|
||||
realm.where<GossipingEventEntity>()
|
||||
}.map {
|
||||
it.toModel()
|
||||
realm.where<AuditTrailEntity>()
|
||||
}.mapNotNull {
|
||||
AuditTrailMapper.map(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>): OutgoingRoomKeyRequest? {
|
||||
override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>): OutgoingKeyRequest {
|
||||
// Insert the request and return the one passed in parameter
|
||||
var request: OutgoingRoomKeyRequest? = null
|
||||
lateinit var request: OutgoingKeyRequest
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
|
||||
val existing = realm.where<OutgoingGossipingRequestEntity>()
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
val existing = realm.where<OutgoingKeyRequestEntity>()
|
||||
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId)
|
||||
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId)
|
||||
.findAll()
|
||||
.mapNotNull {
|
||||
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||
}.firstOrNull {
|
||||
.map {
|
||||
it.toOutgoingGossipingRequest()
|
||||
}.also {
|
||||
if (it.size > 1) {
|
||||
// there should be one or zero but not more, worth warning
|
||||
Timber.tag(loggerTag.value).w("There should not be more than one active key request per session")
|
||||
}
|
||||
}
|
||||
.firstOrNull {
|
||||
it.requestBody?.algorithm == requestBody.algorithm &&
|
||||
it.requestBody?.sessionId == requestBody.sessionId &&
|
||||
it.requestBody?.senderKey == requestBody.senderKey &&
|
||||
|
@ -1108,13 +1166,13 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
|
||||
if (existing == null) {
|
||||
request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply {
|
||||
request = realm.createObject(OutgoingKeyRequestEntity::class.java).apply {
|
||||
this.requestId = RequestIdHelper.createUniqueRequestId()
|
||||
this.setRecipients(recipients)
|
||||
this.requestState = OutgoingGossipingRequestState.UNSENT
|
||||
this.type = GossipRequestType.KEY
|
||||
this.requestedInfoStr = requestBody.toJson()
|
||||
}.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||
this.requestState = OutgoingRoomKeyRequestState.UNSENT
|
||||
this.setRequestBody(requestBody)
|
||||
this.creationTimeStamp = System.currentTimeMillis()
|
||||
}.toOutgoingGossipingRequest()
|
||||
} else {
|
||||
request = existing
|
||||
}
|
||||
|
@ -1122,32 +1180,72 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
return request
|
||||
}
|
||||
|
||||
override fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest? {
|
||||
var request: OutgoingSecretRequest? = null
|
||||
|
||||
// Insert the request and return the one passed in parameter
|
||||
override fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) {
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
val existing = realm.where<OutgoingGossipingRequestEntity>()
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName)
|
||||
.findAll()
|
||||
.mapNotNull {
|
||||
it.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
||||
}.firstOrNull()
|
||||
if (existing == null) {
|
||||
request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply {
|
||||
this.type = GossipRequestType.SECRET
|
||||
setRecipients(recipients)
|
||||
this.requestState = OutgoingGossipingRequestState.UNSENT
|
||||
this.requestId = RequestIdHelper.createUniqueRequestId()
|
||||
this.requestedInfoStr = secretName
|
||||
}.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
||||
} else {
|
||||
request = existing
|
||||
}
|
||||
realm.where<OutgoingKeyRequestEntity>()
|
||||
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
|
||||
.findFirst()?.apply {
|
||||
this.requestState = newState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return request
|
||||
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.toOutgoingGossipingRequest().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 getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest? {
|
||||
return null
|
||||
// var request: OutgoingSecretRequest? = null
|
||||
//
|
||||
// // Insert the request and return the one passed in parameter
|
||||
// doRealmTransaction(realmConfiguration) { realm ->
|
||||
// val existing = realm.where<OutgoingGossipingRequestEntity>()
|
||||
// .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
|
||||
// .equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName)
|
||||
// .findAll()
|
||||
// .mapNotNull {
|
||||
// it.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
||||
// }.firstOrNull()
|
||||
// if (existing == null) {
|
||||
// request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply {
|
||||
// this.type = GossipRequestType.SECRET
|
||||
// setRecipients(recipients)
|
||||
// this.requestState = OutgoingRoomKeyRequestEntity.UNSENT
|
||||
// this.requestId = RequestIdHelper.createUniqueRequestId()
|
||||
// this.requestedInfoStr = secretName
|
||||
// }.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
||||
// } else {
|
||||
// request = existing
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return request
|
||||
}
|
||||
|
||||
override fun saveGossipingEvents(events: List<Event>) {
|
||||
|
@ -1170,6 +1268,88 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun saveIncomingKeyRequestAuditTrail(
|
||||
roomId: String,
|
||||
sessionId: String,
|
||||
senderKey: String,
|
||||
algorithm: String,
|
||||
fromUser: String,
|
||||
fromDevice: String) {
|
||||
monarchy.writeAsync { realm ->
|
||||
val now = System.currentTimeMillis()
|
||||
realm.createObject<AuditTrailEntity>().apply {
|
||||
this.ageLocalTs = now
|
||||
this.type = TrailType.IncomingKeyRequest.name
|
||||
val info = IncomingKeyRequestInfo(
|
||||
roomId,
|
||||
sessionId,
|
||||
senderKey,
|
||||
algorithm,
|
||||
fromUser,
|
||||
fromDevice
|
||||
)
|
||||
MoshiProvider.providesMoshi().adapter(IncomingKeyRequestInfo::class.java).toJson(info)?.let {
|
||||
this.contentJson = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveWithheldAuditTrail(roomId: String,
|
||||
sessionId: String,
|
||||
senderKey: String,
|
||||
algorithm: String,
|
||||
code: WithHeldCode,
|
||||
userId: String,
|
||||
deviceId: String) {
|
||||
monarchy.writeAsync { realm ->
|
||||
val now = System.currentTimeMillis()
|
||||
realm.createObject<AuditTrailEntity>().apply {
|
||||
this.ageLocalTs = now
|
||||
this.type = TrailType.OutgoingKeyWithheld.name
|
||||
val info = WithheldInfo(
|
||||
roomId,
|
||||
sessionId,
|
||||
senderKey,
|
||||
algorithm,
|
||||
code,
|
||||
userId,
|
||||
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?) {
|
||||
monarchy.writeAsync { realm ->
|
||||
val now = System.currentTimeMillis()
|
||||
realm.createObject<AuditTrailEntity>().apply {
|
||||
this.ageLocalTs = now
|
||||
this.type = TrailType.OutgoingKeyForward.name
|
||||
val info = ForwardInfo(
|
||||
roomId,
|
||||
sessionId,
|
||||
senderKey,
|
||||
algorithm,
|
||||
userId,
|
||||
deviceId,
|
||||
chainIndex
|
||||
)
|
||||
MoshiProvider.providesMoshi().adapter(ForwardInfo::class.java).toJson(info)?.let {
|
||||
this.contentJson = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// override fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest? {
|
||||
// val statesIndex = states.map { it.ordinal }.toTypedArray()
|
||||
// return doRealmQueryAndCopy(realmConfiguration) { realm ->
|
||||
|
@ -1276,10 +1456,10 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) {
|
||||
override fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingRoomKeyRequestState) {
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
realm.where<OutgoingGossipingRequestEntity>()
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.REQUEST_ID, requestId)
|
||||
realm.where<OutgoingKeyRequestEntity>()
|
||||
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
|
||||
.findAll().forEach {
|
||||
it.requestState = state
|
||||
}
|
||||
|
@ -1503,37 +1683,46 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> {
|
||||
override fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest> {
|
||||
return monarchy.fetchAllMappedSync({ realm ->
|
||||
realm
|
||||
.where(OutgoingGossipingRequestEntity::class.java)
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
.where(OutgoingKeyRequestEntity::class.java)
|
||||
}, { entity ->
|
||||
entity.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||
entity.toOutgoingGossipingRequest()
|
||||
})
|
||||
.filterNotNull()
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest> {
|
||||
return monarchy.fetchAllMappedSync({ realm ->
|
||||
realm
|
||||
.where(OutgoingKeyRequestEntity::class.java)
|
||||
.`in`(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, inStates.map { it.name }.toTypedArray())
|
||||
}, { entity ->
|
||||
entity.toOutgoingGossipingRequest()
|
||||
})
|
||||
.filterNotNull()
|
||||
}
|
||||
|
||||
override fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest> {
|
||||
return monarchy.fetchAllMappedSync({ realm ->
|
||||
realm
|
||||
.where(OutgoingGossipingRequestEntity::class.java)
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
|
||||
}, { entity ->
|
||||
entity.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
||||
})
|
||||
.filterNotNull()
|
||||
// return monarchy.fetchAllMappedSync({ realm ->
|
||||
// realm
|
||||
// .where(OutgoingGossipingRequestEntity::class.java)
|
||||
// .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
|
||||
// }, { entity ->
|
||||
// entity.toOutgoingGossipingRequest() as? OutgoingSecretRequest
|
||||
// })
|
||||
// .filterNotNull()
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
|
||||
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>> {
|
||||
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
|
||||
realm
|
||||
.where(OutgoingGossipingRequestEntity::class.java)
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
.where(OutgoingKeyRequestEntity::class.java)
|
||||
}
|
||||
val dataSourceFactory = realmDataSourceFactory.map {
|
||||
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||
?: OutgoingRoomKeyRequest(requestBody = null, requestId = "?", recipients = emptyMap(), state = OutgoingGossipingRequestState.CANCELLED)
|
||||
it.toOutgoingGossipingRequest()
|
||||
}
|
||||
val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory,
|
||||
LivePagedListBuilder(dataSourceFactory,
|
||||
|
@ -1716,12 +1905,11 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
.also { Timber.i("## Crypto Clean up ${it.size} IncomingGossipingRequestEntity") }
|
||||
.deleteAllFromRealm()
|
||||
|
||||
// Clean the cancelled ones?
|
||||
realm.where<OutgoingGossipingRequestEntity>()
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, OutgoingGossipingRequestState.CANCELLED.name)
|
||||
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
|
||||
// Clean the old ones?
|
||||
realm.where<OutgoingKeyRequestEntity>()
|
||||
.lessThan(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, prevWeekTs)
|
||||
.findAll()
|
||||
.also { Timber.i("## Crypto Clean up ${it.size} OutgoingGossipingRequestEntity") }
|
||||
.also { Timber.i("## Crypto Clean up ${it.size} OutgoingKeyRequestEntity") }
|
||||
.deleteAllFromRealm()
|
||||
|
||||
// Only keep one week history
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo
|
|||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo013
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -47,7 +48,7 @@ internal class RealmCryptoStoreMigration @Inject constructor() : RealmMigration
|
|||
// 0, 1, 2: legacy Riot-Android
|
||||
// 3: migrate to RiotX schema
|
||||
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
|
||||
val schemaVersion = 15L
|
||||
val schemaVersion = 16L
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion")
|
||||
|
@ -67,5 +68,6 @@ internal class RealmCryptoStoreMigration @Inject constructor() : RealmMigration
|
|||
if (oldVersion < 13) MigrateCryptoTo013(realm).perform()
|
||||
if (oldVersion < 14) MigrateCryptoTo014(realm).perform()
|
||||
if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
|
||||
if (oldVersion < 16) MigrateCryptoTo016(realm).perform()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.matrix.android.sdk.internal.crypto.store.db
|
||||
|
||||
import io.realm.annotations.RealmModule
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity
|
||||
|
@ -24,12 +25,13 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
|
|||
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeyRequestReplyEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
|
||||
|
@ -50,9 +52,9 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEnti
|
|||
KeyInfoEntity::class,
|
||||
CrossSigningInfoEntity::class,
|
||||
TrustLevelEntity::class,
|
||||
GossipingEventEntity::class,
|
||||
IncomingGossipingRequestEntity::class,
|
||||
OutgoingGossipingRequestEntity::class,
|
||||
AuditTrailEntity::class,
|
||||
OutgoingKeyRequestEntity::class,
|
||||
KeyRequestReplyEntity::class,
|
||||
MyDeviceLastSeenInfoEntity::class,
|
||||
WithHeldSessionEntity::class,
|
||||
SharedSessionEntity::class,
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package 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.KeyRequestReplyEntity
|
||||
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
|
||||
|
||||
class MigrateCryptoTo016(realm: DynamicRealm) : RealmMigrator(realm, 15) {
|
||||
|
||||
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
|
||||
|
||||
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.`$`, KeyRequestReplyEntity::class.java)
|
||||
.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.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)
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -14,18 +14,14 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.crypto.model
|
||||
package org.matrix.android.sdk.internal.crypto.store.db.model
|
||||
|
||||
enum class GossipingRequestState {
|
||||
NONE,
|
||||
PENDING,
|
||||
REJECTED,
|
||||
ACCEPTING,
|
||||
ACCEPTED,
|
||||
FAILED_TO_ACCEPTED,
|
||||
import io.realm.RealmObject
|
||||
|
||||
// USER_REJECTED,
|
||||
UNABLE_TO_PROCESS,
|
||||
CANCELLED_BY_REQUESTER,
|
||||
RE_REQUESTED
|
||||
internal open class AuditTrailEntity(
|
||||
var ageLocalTs: Long? = null,
|
||||
var type: String? = null,
|
||||
var contentJson: String? = null
|
||||
) : RealmObject() {
|
||||
companion object
|
||||
}
|
|
@ -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.internal.crypto.store.db.model
|
||||
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.internal.crypto.model.AuditInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.AuditTrail
|
||||
import org.matrix.android.sdk.internal.crypto.model.ForwardInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.IncomingKeyRequestInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.TrailType
|
||||
import org.matrix.android.sdk.internal.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
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
AuditTrail(
|
||||
ageLocalTs = entity.ageLocalTs ?: 0,
|
||||
type = TrailType.Unknown,
|
||||
info = object : AuditInfo {
|
||||
override val roomId: String
|
||||
get() = ""
|
||||
override val sessionId: String
|
||||
get() = ""
|
||||
override val senderKey: String
|
||||
get() = ""
|
||||
override val alg: String
|
||||
get() = ""
|
||||
override val userId: String
|
||||
get() = ""
|
||||
override val deviceId: String
|
||||
get() = ""
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,6 +33,8 @@ import timber.log.Timber
|
|||
* (room key request, or sss secret sharing, as well as cancellations)
|
||||
*
|
||||
*/
|
||||
|
||||
// not used anymore, just here for db migration
|
||||
internal open class GossipingEventEntity(@Index var type: String? = "",
|
||||
var content: String? = null,
|
||||
@Index var sender: String? = null,
|
||||
|
|
|
@ -26,6 +26,7 @@ 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
|
||||
|
||||
// not used anymore, just here for db migration
|
||||
internal open class IncomingGossipingRequestEntity(@Index var requestId: String? = "",
|
||||
@Index var typeStr: String? = null,
|
||||
var otherUserId: String? = null,
|
||||
|
|
|
@ -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,34 +1,26 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
// /*
|
||||
// * 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
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState
|
||||
|
||||
// not used anymore, just here for db migration
|
||||
internal open class OutgoingGossipingRequestEntity(
|
||||
@Index var requestId: String? = null,
|
||||
var recipientsData: String? = null,
|
||||
|
@ -36,69 +28,5 @@ internal open class OutgoingGossipingRequestEntity(
|
|||
@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 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package 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.model.OutgoingRoomKeyRequestState
|
||||
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.crypto.OutgoingKeyRequest
|
||||
import org.matrix.android.sdk.internal.crypto.RequestReply
|
||||
import org.matrix.android.sdk.internal.crypto.RequestResult
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import timber.log.Timber
|
||||
|
||||
internal open class OutgoingKeyRequestEntity(
|
||||
@Index var requestId: String? = 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)
|
||||
)
|
||||
}
|
||||
|
||||
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) {
|
||||
Timber.w("##VALR: add reply $userId / $fromDevice / $event")
|
||||
val newReply = KeyRequestReplyEntity(
|
||||
senderId = userId,
|
||||
fromDevice = fromDevice,
|
||||
eventJson = MoshiProvider.providesMoshi().adapter(Event::class.java).toJson(event)
|
||||
)
|
||||
replies.add(newReply)
|
||||
}
|
||||
|
||||
fun toOutgoingGossipingRequest(): OutgoingKeyRequest {
|
||||
return OutgoingKeyRequest(
|
||||
requestBody = getRequestedKeyInfo(),
|
||||
recipients = getRecipients().orEmpty(),
|
||||
requestId = requestId ?: "",
|
||||
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,
|
||||
entity.fromDevice,
|
||||
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
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun OutgoingKeyRequestEntity.deleteOnCascade() {
|
||||
replies.deleteAllFromRealm()
|
||||
deleteFromRealm()
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package org.matrix.android.sdk.internal.crypto.tasks
|
||||
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||
|
@ -46,7 +47,9 @@ internal class DefaultSendEventTask @Inject constructor(
|
|||
params.event.roomId
|
||||
?.takeIf { params.encrypt }
|
||||
?.let { roomId ->
|
||||
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
|
||||
tryOrNull {
|
||||
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
|
||||
}
|
||||
}
|
||||
|
||||
val event = handleEncryption(params)
|
||||
|
|
|
@ -23,8 +23,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerific
|
|||
import org.matrix.android.sdk.api.session.crypto.verification.SasMode
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import timber.log.Timber
|
||||
|
@ -36,7 +36,7 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
|
|||
private val cryptoStore: IMXCryptoStore,
|
||||
crossSigningService: CrossSigningService,
|
||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||
secretShareManager: SecretShareManager,
|
||||
deviceFingerprint: String,
|
||||
transactionId: String,
|
||||
otherUserID: String,
|
||||
|
@ -48,7 +48,7 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
|
|||
cryptoStore,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager,
|
||||
secretShareManager,
|
||||
deviceFingerprint,
|
||||
transactionId,
|
||||
otherUserID,
|
||||
|
|
|
@ -20,8 +20,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
|||
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import timber.log.Timber
|
||||
|
@ -33,7 +33,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
|||
cryptoStore: IMXCryptoStore,
|
||||
crossSigningService: CrossSigningService,
|
||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||
secretShareManager: SecretShareManager,
|
||||
deviceFingerprint: String,
|
||||
transactionId: String,
|
||||
otherUserId: String,
|
||||
|
@ -45,7 +45,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
|||
cryptoStore,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager,
|
||||
secretShareManager,
|
||||
deviceFingerprint,
|
||||
transactionId,
|
||||
otherUserId,
|
||||
|
|
|
@ -59,9 +59,9 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification
|
|||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.ValidVerificationDone
|
||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel
|
||||
|
@ -95,7 +95,7 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
@DeviceId private val deviceId: String?,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||
private val secretShareManager: SecretShareManager,
|
||||
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||
|
@ -548,7 +548,7 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
cryptoStore,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager,
|
||||
secretShareManager,
|
||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||
startReq.transactionId,
|
||||
otherUserId,
|
||||
|
@ -807,17 +807,15 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
getExistingTransaction(userId, doneReq.transactionId)
|
||||
?: getOldTransaction(userId, doneReq.transactionId)
|
||||
?.let { vt ->
|
||||
val otherDeviceId = vt.otherDeviceId
|
||||
val otherDeviceId = vt.otherDeviceId ?: return@let
|
||||
if (!crossSigningService.canCrossSign()) {
|
||||
outgoingGossipingRequestManager.sendSecretShareRequest(MASTER_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId
|
||||
?: "*")))
|
||||
outgoingGossipingRequestManager.sendSecretShareRequest(SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId
|
||||
?: "*")))
|
||||
outgoingGossipingRequestManager.sendSecretShareRequest(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId
|
||||
?: "*")))
|
||||
cryptoCoroutineScope.launch {
|
||||
secretShareManager.requestSecretTo(otherDeviceId, MASTER_KEY_SSSS_NAME)
|
||||
secretShareManager.requestSecretTo(otherDeviceId, SELF_SIGNING_KEY_SSSS_NAME)
|
||||
secretShareManager.requestSecretTo(otherDeviceId, USER_SIGNING_KEY_SSSS_NAME)
|
||||
secretShareManager.requestSecretTo(otherDeviceId, KEYBACKUP_SECRET_SSSS_NAME)
|
||||
}
|
||||
}
|
||||
outgoingGossipingRequestManager.sendSecretShareRequest(KEYBACKUP_SECRET_SSSS_NAME, mapOf(userId to listOf(otherDeviceId
|
||||
?: "*")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -912,7 +910,7 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
otherDeviceId = readyReq.fromDevice,
|
||||
crossSigningService = crossSigningService,
|
||||
outgoingGossipingRequestManager = outgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager = incomingGossipingRequestManager,
|
||||
secretShareManager = secretShareManager,
|
||||
cryptoStore = cryptoStore,
|
||||
qrCodeData = qrCodeData,
|
||||
userId = userId,
|
||||
|
@ -1111,7 +1109,7 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
cryptoStore,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager,
|
||||
secretShareManager,
|
||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||
txID,
|
||||
otherUserId,
|
||||
|
@ -1303,7 +1301,7 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
cryptoStore,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager,
|
||||
secretShareManager,
|
||||
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
|
||||
transactionId,
|
||||
otherUserId,
|
||||
|
@ -1441,7 +1439,7 @@ internal class DefaultVerificationService @Inject constructor(
|
|||
otherDeviceId = otherDeviceId,
|
||||
crossSigningService = crossSigningService,
|
||||
outgoingGossipingRequestManager = outgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager = incomingGossipingRequestManager,
|
||||
secretShareManager = secretShareManager,
|
||||
cryptoStore = cryptoStore,
|
||||
qrCodeData = qrCodeData,
|
||||
userId = userId,
|
||||
|
|
|
@ -20,8 +20,8 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningServic
|
|||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import timber.log.Timber
|
||||
|
||||
|
@ -32,7 +32,7 @@ internal abstract class DefaultVerificationTransaction(
|
|||
private val setDeviceVerificationAction: SetDeviceVerificationAction,
|
||||
private val crossSigningService: CrossSigningService,
|
||||
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||
private val secretShareManager: SecretShareManager,
|
||||
private val userId: String,
|
||||
override val transactionId: String,
|
||||
override val otherUserId: String,
|
||||
|
@ -86,7 +86,7 @@ internal abstract class DefaultVerificationTransaction(
|
|||
}
|
||||
|
||||
if (otherUserId == userId) {
|
||||
incomingGossipingRequestManager.onVerificationCompleteForDevice(otherDeviceId!!)
|
||||
secretShareManager.onVerificationCompleteForDevice(otherDeviceId!!)
|
||||
|
||||
// If me it's reasonable to sign and upload the device signature
|
||||
// Notice that i might not have the private keys, so may not be able to do it
|
||||
|
|
|
@ -23,8 +23,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.SasMode
|
|||
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.extensions.toUnsignedInt
|
||||
|
@ -43,7 +43,7 @@ internal abstract class SASDefaultVerificationTransaction(
|
|||
private val cryptoStore: IMXCryptoStore,
|
||||
crossSigningService: CrossSigningService,
|
||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||
secretShareManager: SecretShareManager,
|
||||
private val deviceFingerprint: String,
|
||||
transactionId: String,
|
||||
otherUserId: String,
|
||||
|
@ -53,7 +53,7 @@ internal abstract class SASDefaultVerificationTransaction(
|
|||
setDeviceVerificationAction,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager,
|
||||
secretShareManager,
|
||||
userId,
|
||||
transactionId,
|
||||
otherUserId,
|
||||
|
|
|
@ -22,8 +22,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerification
|
|||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.util.fromBase64
|
||||
import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.SecretShareManager
|
||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64Safe
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
|
@ -38,7 +38,7 @@ internal class DefaultQrCodeVerificationTransaction(
|
|||
override var otherDeviceId: String?,
|
||||
private val crossSigningService: CrossSigningService,
|
||||
outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager: IncomingGossipingRequestManager,
|
||||
secretShareManager: SecretShareManager,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
// Not null only if other user is able to scan QR code
|
||||
private val qrCodeData: QrCodeData?,
|
||||
|
@ -49,7 +49,7 @@ internal class DefaultQrCodeVerificationTransaction(
|
|||
setDeviceVerificationAction,
|
||||
crossSigningService,
|
||||
outgoingGossipingRequestManager,
|
||||
incomingGossipingRequestManager,
|
||||
secretShareManager,
|
||||
userId,
|
||||
transactionId,
|
||||
otherUserId,
|
||||
|
|
|
@ -21,10 +21,7 @@ import dagger.Component
|
|||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker
|
||||
import org.matrix.android.sdk.internal.crypto.CryptoModule
|
||||
import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker
|
||||
import org.matrix.android.sdk.internal.crypto.SendGossipWorker
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
|
||||
import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker
|
||||
import org.matrix.android.sdk.internal.di.MatrixComponent
|
||||
|
@ -134,12 +131,6 @@ internal interface SessionComponent {
|
|||
|
||||
fun inject(worker: SendVerificationMessageWorker)
|
||||
|
||||
fun inject(worker: SendGossipRequestWorker)
|
||||
|
||||
fun inject(worker: CancelGossipRequestWorker)
|
||||
|
||||
fun inject(worker: SendGossipWorker)
|
||||
|
||||
fun inject(worker: UpdateTrustWorker)
|
||||
|
||||
@Component.Factory
|
||||
|
|
|
@ -22,9 +22,6 @@ import androidx.work.ListenableWorker
|
|||
import androidx.work.WorkerFactory
|
||||
import androidx.work.WorkerParameters
|
||||
import org.matrix.android.sdk.internal.SessionManager
|
||||
import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker
|
||||
import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker
|
||||
import org.matrix.android.sdk.internal.crypto.SendGossipWorker
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
|
||||
import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker
|
||||
import org.matrix.android.sdk.internal.di.MatrixScope
|
||||
|
@ -56,8 +53,6 @@ internal class MatrixWorkerFactory @Inject constructor(private val sessionManage
|
|||
CheckFactoryWorker(appContext, workerParameters, true)
|
||||
AddPusherWorker::class.java.name ->
|
||||
AddPusherWorker(appContext, workerParameters, sessionManager)
|
||||
CancelGossipRequestWorker::class.java.name ->
|
||||
CancelGossipRequestWorker(appContext, workerParameters, sessionManager)
|
||||
GetGroupDataWorker::class.java.name ->
|
||||
GetGroupDataWorker(appContext, workerParameters, sessionManager)
|
||||
MultipleEventSendingDispatcherWorker::class.java.name ->
|
||||
|
@ -66,10 +61,6 @@ internal class MatrixWorkerFactory @Inject constructor(private val sessionManage
|
|||
RedactEventWorker(appContext, workerParameters, sessionManager)
|
||||
SendEventWorker::class.java.name ->
|
||||
SendEventWorker(appContext, workerParameters, sessionManager)
|
||||
SendGossipRequestWorker::class.java.name ->
|
||||
SendGossipRequestWorker(appContext, workerParameters, sessionManager)
|
||||
SendGossipWorker::class.java.name ->
|
||||
SendGossipWorker(appContext, workerParameters, sessionManager)
|
||||
SendVerificationMessageWorker::class.java.name ->
|
||||
SendVerificationMessageWorker(appContext, workerParameters, sessionManager)
|
||||
SyncWorker::class.java.name ->
|
||||
|
|
|
@ -20,7 +20,6 @@ import im.vector.app.R
|
|||
import im.vector.app.core.platform.ViewModelTask
|
||||
import im.vector.app.core.platform.WaitingViewData
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
||||
import org.matrix.android.sdk.api.listeners.ProgressListener
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
|
@ -143,15 +142,8 @@ class BackupToQuadSMigrationTask @Inject constructor(
|
|||
// save for gossiping
|
||||
keysBackupService.saveBackupRecoveryKey(recoveryKey, version.version)
|
||||
|
||||
// while we are there let's restore, but do not block
|
||||
session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
|
||||
version,
|
||||
recoveryKey,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
NoOpMatrixCallback()
|
||||
)
|
||||
// It's not a good idea to download the full backup, it might take very long
|
||||
// and use a lot of resources
|
||||
|
||||
return Result.Success
|
||||
} catch (failure: Throwable) {
|
||||
|
|
|
@ -42,7 +42,6 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
|
|||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
|
||||
|
@ -58,6 +57,7 @@ import org.matrix.android.sdk.api.util.MatrixItem
|
|||
import org.matrix.android.sdk.api.util.awaitCallback
|
||||
import org.matrix.android.sdk.api.util.fromBase64
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
|
||||
import timber.log.Timber
|
||||
|
||||
data class VerificationBottomSheetViewState(
|
||||
|
@ -422,6 +422,11 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun tentativeRestoreBackup(res: Map<String, String>?) {
|
||||
// It's not a good idea to download the full backup, it might take very long
|
||||
// and use a lot of resources
|
||||
// Just check that the ey is valid and store it, the backup will be used megolm session per
|
||||
// megolm session when an UISI is encountered
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also {
|
||||
|
@ -432,17 +437,13 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
|||
session.cryptoService().keysBackupService().getCurrentVersion(it)
|
||||
}.toKeysVersionResult() ?: return@launch
|
||||
|
||||
awaitCallback<ImportRoomKeysResult> {
|
||||
session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
|
||||
version,
|
||||
computeRecoveryKey(secret.fromBase64()),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
it
|
||||
)
|
||||
val recoveryKey = computeRecoveryKey(secret.fromBase64())
|
||||
val isValid = awaitCallback<Boolean> {
|
||||
session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(recoveryKey, it)
|
||||
}
|
||||
if (isValid) {
|
||||
session.cryptoService().keysBackupService().saveBackupRecoveryKey(recoveryKey, version.version)
|
||||
}
|
||||
|
||||
awaitCallback<Unit> {
|
||||
session.cryptoService().keysBackupService().trustKeysBackupVersion(version, true, it)
|
||||
}
|
||||
|
|
|
@ -27,10 +27,8 @@ import im.vector.app.core.extensions.cleanup
|
|||
import im.vector.app.core.extensions.configureWith
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.utils.createJSonViewerStyleProvider
|
||||
import im.vector.app.databinding.FragmentGenericRecyclerBinding
|
||||
import org.billcarsonfr.jsonviewer.JSonViewerDialog
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.internal.crypto.model.AuditTrail
|
||||
import javax.inject.Inject
|
||||
|
||||
class GossipingEventsPaperTrailFragment @Inject constructor(
|
||||
|
@ -64,17 +62,17 @@ class GossipingEventsPaperTrailFragment @Inject constructor(
|
|||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun didTap(event: Event) {
|
||||
if (event.isEncrypted()) {
|
||||
event.toClearContentStringWithIndent()
|
||||
} else {
|
||||
event.toContentStringWithIndent()
|
||||
}?.let {
|
||||
JSonViewerDialog.newInstance(
|
||||
it,
|
||||
-1,
|
||||
createJSonViewerStyleProvider(colorProvider)
|
||||
).show(childFragmentManager, "JSON_VIEWER")
|
||||
}
|
||||
override fun didTap(event: AuditTrail) {
|
||||
// if (event.isEncrypted()) {
|
||||
// event.toClearContentStringWithIndent()
|
||||
// } else {
|
||||
// event.toContentStringWithIndent()
|
||||
// }?.let {
|
||||
// JSonViewerDialog.newInstance(
|
||||
// it,
|
||||
// -1,
|
||||
// createJSonViewerStyleProvider(colorProvider)
|
||||
// ).show(childFragmentManager, "JSON_VIEWER")
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,10 +32,10 @@ import im.vector.app.core.platform.EmptyAction
|
|||
import im.vector.app.core.platform.EmptyViewEvents
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.internal.crypto.model.AuditTrail
|
||||
|
||||
data class GossipingEventsPaperTrailState(
|
||||
val events: Async<PagedList<Event>> = Uninitialized
|
||||
val events: Async<PagedList<AuditTrail>> = Uninitialized
|
||||
) : MavericksState
|
||||
|
||||
class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted initialState: GossipingEventsPaperTrailState,
|
||||
|
|
|
@ -17,62 +17,37 @@
|
|||
package im.vector.app.features.settings.devtools
|
||||
|
||||
import im.vector.app.core.resources.DateProvider
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent
|
||||
import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.internal.crypto.model.AuditTrail
|
||||
import org.matrix.android.sdk.internal.crypto.model.ForwardInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.TrailType
|
||||
import org.matrix.android.sdk.internal.crypto.model.WithheldInfo
|
||||
import org.threeten.bp.format.DateTimeFormatter
|
||||
|
||||
class GossipingEventsSerializer {
|
||||
private val full24DateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
|
||||
|
||||
fun serialize(eventList: List<Event>): String {
|
||||
fun serialize(eventList: List<AuditTrail>): String {
|
||||
return buildString {
|
||||
eventList.forEach {
|
||||
val clearType = it.getClearType()
|
||||
append("[${getFormattedDate(it.ageLocalTs)}] $clearType from:${it.senderId} - ")
|
||||
when (clearType) {
|
||||
EventType.ROOM_KEY_REQUEST -> {
|
||||
val content = it.getClearContent().toModel<RoomKeyShareRequest>()
|
||||
append("reqId:${content?.requestId} action:${content?.action} ")
|
||||
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
|
||||
append("sessionId: ${content.body?.sessionId} ")
|
||||
}
|
||||
append("requestedBy: ${content?.requestingDeviceId}")
|
||||
eventList.forEach { trail ->
|
||||
val type = trail.type
|
||||
val info = trail.info
|
||||
append("[${getFormattedDate(trail.ageLocalTs)}] ${type.name} ")
|
||||
append("sessionId: ${info.sessionId} ")
|
||||
when (type) {
|
||||
TrailType.IncomingKeyRequest -> {
|
||||
append("from:${info.userId}|${info.deviceId} - ")
|
||||
}
|
||||
EventType.FORWARDED_ROOM_KEY -> {
|
||||
val encryptedContent = it.content.toModel<OlmEventContent>()
|
||||
val content = it.getClearContent().toModel<ForwardedRoomKeyContent>()
|
||||
|
||||
append("sessionId:${content?.sessionId} From Device (sender key):${encryptedContent?.senderKey}")
|
||||
span("\nFrom Device (sender key):") {
|
||||
textStyle = "bold"
|
||||
TrailType.OutgoingKeyForward -> {
|
||||
append("to:${info.userId}|${info.deviceId} - ")
|
||||
(trail.info as? ForwardInfo)?.let {
|
||||
append("chainIndex: ${it.chainIndex} ")
|
||||
}
|
||||
}
|
||||
EventType.ROOM_KEY -> {
|
||||
val content = it.getClearContent()
|
||||
append("sessionId:${content?.get("session_id")} roomId:${content?.get("room_id")} dest:${content?.get("_dest") ?: "me"}")
|
||||
}
|
||||
EventType.SEND_SECRET -> {
|
||||
val content = it.getClearContent().toModel<SecretSendEventContent>()
|
||||
append("requestId:${content?.requestId} From Device:${it.mxDecryptionResult?.payload?.get("sender_device")}")
|
||||
}
|
||||
EventType.REQUEST_SECRET -> {
|
||||
val content = it.getClearContent().toModel<SecretShareRequest>()
|
||||
append("reqId:${content?.requestId} action:${content?.action} ")
|
||||
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
|
||||
append("secretName:${content.secretName} ")
|
||||
TrailType.OutgoingKeyWithheld -> {
|
||||
append("to:${info.userId}|${info.deviceId} - ")
|
||||
(trail.info as? WithheldInfo)?.let {
|
||||
append("code: ${it.code} ")
|
||||
}
|
||||
append("requestedBy:${content?.requestingDeviceId}")
|
||||
}
|
||||
EventType.ENCRYPTED -> {
|
||||
append("Failed to Decrypt")
|
||||
}
|
||||
else -> {
|
||||
append("??")
|
||||
|
|
|
@ -18,7 +18,6 @@ package im.vector.app.features.settings.devtools
|
|||
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.paging.PagedListEpoxyController
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.date.DateFormatKind
|
||||
import im.vector.app.core.date.VectorDateFormatter
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
|
@ -26,137 +25,68 @@ import im.vector.app.core.ui.list.GenericItem_
|
|||
import im.vector.app.core.utils.createUIHandler
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent
|
||||
import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
|
||||
import org.matrix.android.sdk.internal.crypto.model.AuditTrail
|
||||
import org.matrix.android.sdk.internal.crypto.model.ForwardInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.TrailType
|
||||
import org.matrix.android.sdk.internal.crypto.model.WithheldInfo
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
class GossipingTrailPagedEpoxyController @Inject constructor(
|
||||
private val vectorDateFormatter: VectorDateFormatter,
|
||||
private val colorProvider: ColorProvider
|
||||
) : PagedListEpoxyController<Event>(
|
||||
) : PagedListEpoxyController<AuditTrail>(
|
||||
// Important it must match the PageList builder notify Looper
|
||||
modelBuildingHandler = createUIHandler()
|
||||
) {
|
||||
|
||||
interface InteractionListener {
|
||||
fun didTap(event: Event)
|
||||
fun didTap(event: AuditTrail)
|
||||
}
|
||||
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
override fun buildItemModel(currentPosition: Int, item: Event?): EpoxyModel<*> {
|
||||
override fun buildItemModel(currentPosition: Int, item: AuditTrail?): EpoxyModel<*> {
|
||||
val host = this
|
||||
val event = item ?: return GenericItem_().apply { id(currentPosition) }
|
||||
return GenericItem_().apply {
|
||||
id(event.hashCode())
|
||||
itemClickAction { host.interactionListener?.didTap(event) }
|
||||
title(
|
||||
if (event.isEncrypted()) {
|
||||
"${event.getClearType()} [encrypted]"
|
||||
} else {
|
||||
event.type
|
||||
}?.toEpoxyCharSequence()
|
||||
)
|
||||
title(event.type.name.toEpoxyCharSequence())
|
||||
description(
|
||||
span {
|
||||
+host.vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
|
||||
span("\nfrom: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${event.senderId}"
|
||||
+"${event.info.userId}|${event.info.deviceId}"
|
||||
span("\nroomId: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+event.info.roomId
|
||||
span("\nsessionId: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+event.info.sessionId
|
||||
apply {
|
||||
if (event.getClearType() == EventType.ROOM_KEY_REQUEST) {
|
||||
val content = event.getClearContent().toModel<RoomKeyShareRequest>()
|
||||
span("\nreqId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.requestId}"
|
||||
span("\naction:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.action}"
|
||||
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
|
||||
span("\nsessionId:") {
|
||||
when (event.type) {
|
||||
TrailType.OutgoingKeyForward -> {
|
||||
val fInfo = event.info as ForwardInfo
|
||||
span("\nchainIndex: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content.body?.sessionId}"
|
||||
+"${fInfo.chainIndex}"
|
||||
}
|
||||
span("\nrequestedBy: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${content?.requestingDeviceId}"
|
||||
} else if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
||||
val encryptedContent = event.content.toModel<OlmEventContent>()
|
||||
val content = event.getClearContent().toModel<ForwardedRoomKeyContent>()
|
||||
if (event.mxDecryptionResult == null) {
|
||||
span("**Failed to Decrypt** ${event.mCryptoError}") {
|
||||
textColor = host.colorProvider.getColorFromAttribute(R.attr.colorError)
|
||||
}
|
||||
}
|
||||
span("\nsessionId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.sessionId}"
|
||||
span("\nFrom Device (sender key):") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${encryptedContent?.senderKey}"
|
||||
} else if (event.getClearType() == EventType.ROOM_KEY) {
|
||||
// it's a bit of a fake event for trail reasons
|
||||
val content = event.getClearContent()
|
||||
span("\nsessionId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.get("session_id")}"
|
||||
span("\nroomId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.get("room_id")}"
|
||||
span("\nTo :") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.get("_dest") ?: "me"}"
|
||||
} else if (event.getClearType() == EventType.SEND_SECRET) {
|
||||
val content = event.getClearContent().toModel<SecretSendEventContent>()
|
||||
|
||||
span("\nrequestId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.requestId}"
|
||||
span("\nFrom Device:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${event.mxDecryptionResult?.payload?.get("sender_device")}"
|
||||
} else if (event.getClearType() == EventType.REQUEST_SECRET) {
|
||||
val content = event.getClearContent().toModel<SecretShareRequest>()
|
||||
span("\nreqId:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.requestId}"
|
||||
span("\naction:") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content?.action}"
|
||||
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
|
||||
span("\nsecretName:") {
|
||||
TrailType.OutgoingKeyWithheld -> {
|
||||
val fInfo = event.info as WithheldInfo
|
||||
span("\ncode: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+" ${content.secretName}"
|
||||
+"${fInfo.code}"
|
||||
}
|
||||
span("\nrequestedBy: ") {
|
||||
textStyle = "bold"
|
||||
}
|
||||
+"${content?.requestingDeviceId}"
|
||||
} else if (event.getClearType() == EventType.ENCRYPTED) {
|
||||
span("**Failed to Decrypt** ${event.mCryptoError}") {
|
||||
textColor = host.colorProvider.getColorFromAttribute(R.attr.colorError)
|
||||
TrailType.IncomingKeyRequest -> {
|
||||
// no additional info
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,11 +33,11 @@ import im.vector.app.core.platform.VectorViewModel
|
|||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequest
|
||||
|
||||
data class KeyRequestListViewState(
|
||||
val incomingRequests: Async<PagedList<IncomingRoomKeyRequest>> = Uninitialized,
|
||||
val outgoingRoomKeyRequests: Async<PagedList<OutgoingRoomKeyRequest>> = Uninitialized
|
||||
val outgoingRoomKeyRequests: Async<PagedList<OutgoingKeyRequest>> = Uninitialized
|
||||
) : MavericksState
|
||||
|
||||
class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState: KeyRequestListViewState,
|
||||
|
|
|
@ -22,10 +22,10 @@ import im.vector.app.core.ui.list.GenericItem_
|
|||
import im.vector.app.core.utils.createUIHandler
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequest
|
||||
import javax.inject.Inject
|
||||
|
||||
class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyController<OutgoingRoomKeyRequest>(
|
||||
class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyController<OutgoingKeyRequest>(
|
||||
// Important it must match the PageList builder notify Looper
|
||||
modelBuildingHandler = createUIHandler()
|
||||
) {
|
||||
|
@ -36,7 +36,7 @@ class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyCo
|
|||
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
override fun buildItemModel(currentPosition: Int, item: OutgoingRoomKeyRequest?): EpoxyModel<*> {
|
||||
override fun buildItemModel(currentPosition: Int, item: OutgoingKeyRequest?): EpoxyModel<*> {
|
||||
val roomKeyRequest = item ?: return GenericItem_().apply { id(currentPosition) }
|
||||
|
||||
return GenericItem_().apply {
|
||||
|
|
|
@ -66,7 +66,7 @@ class FakeSharedSecretStorageService : SharedSecretStorageService {
|
|||
|
||||
override fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?) = integrityResult
|
||||
|
||||
override fun requestSecret(name: String, myOtherDeviceId: String) {
|
||||
override suspend fun requestSecret(name: String, myOtherDeviceId: String) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue