Refactor key and secret request managers

use megolm backup before sending key request
This commit is contained in:
Valere 2022-03-14 14:39:04 +01:00
parent d694ea16a8
commit 9177cb11d5
61 changed files with 2499 additions and 1852 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,55 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.crypto.model
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest
/**
* Represents an outgoing room key request
*/
@JsonClass(generateAdapter = true)
data class OutgoingRoomKeyRequest(
// RequestBody
val requestBody: RoomKeyRequestBody?,
// list of recipients for the request
override val recipients: Map<String, List<String>>,
// Unique id for this request. Used for both
// an id within the request for later pairing with a cancellation, and for
// the transaction id when sending the to_device messages to our local
override val requestId: String, // current state of this request
override val state: OutgoingGossipingRequestState
// transaction id for the cancellation, if any
// override var cancellationTxnId: String? = null
) : OutgoingGossipingRequest {
/**
* Used only for log.
*
* @return the room id.
*/
val roomId: String?
get() = requestBody?.roomId
/**
* Used only for log.
*
* @return the session id
*/
val sessionId: String?
get() = requestBody?.sessionId
}

View file

@ -52,7 +52,13 @@ data class RoomKeyWithHeldContent(
/**
* A human-readable reason for why the key was not sent. The receiving client should only use this string if it does not understand the code.
*/
@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?

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,23 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.algorithms
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
internal interface IMXWithHeldExtension {
fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent)
}

View file

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

View file

@ -28,7 +28,6 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.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")

View file

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

View file

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

View file

@ -40,7 +40,7 @@ import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec
import org.matrix.android.sdk.api.session.securestorage.SsssPassphrase
import org.matrix.android.sdk.api.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)
}
}

View file

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

View file

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

View file

@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo013
import org.matrix.android.sdk.internal.crypto.store.db.migration.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()
}
}

View file

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

View file

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

View file

@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* 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
}

View file

@ -0,0 +1,85 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.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() = ""
}
)
}
}
}
}

View file

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

View file

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

View file

@ -0,0 +1,35 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.store.db.model
import io.realm.RealmObject
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.di.MoshiProvider
internal open class KeyRequestReplyEntity(
var senderId: String? = null,
var fromDevice: String? = null,
var eventJson: String? = null
) : RealmObject() {
companion object
fun getEvent(): Event? {
return eventJson?.let {
MoshiProvider.providesMoshi().adapter(Event::class.java).fromJson(it)
}
}
}

View file

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

View file

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

View file

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

View file

@ -23,8 +23,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerific
import org.matrix.android.sdk.api.session.crypto.verification.SasMode
import org.matrix.android.sdk.api.session.crypto.verification.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,

View file

@ -20,8 +20,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.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,

View file

@ -59,9 +59,9 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent
import org.matrix.android.sdk.api.session.room.model.message.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,

View file

@ -20,8 +20,8 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningServic
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.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

View file

@ -23,8 +23,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.SasMode
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.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,

View file

@ -22,8 +22,8 @@ import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerification
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import org.matrix.android.sdk.api.session.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,

View file

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

View file

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

View file

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

View file

@ -42,7 +42,6 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.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)
}

View file

@ -27,10 +27,8 @@ import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.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")
// }
}
}

View file

@ -32,10 +32,10 @@ import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.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,

View file

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

View file

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

View file

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

View file

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

View file

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