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

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

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

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

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

@ -0,0 +1,4 @@
- New API to enable/disable key forwarding CryptoService#enableKeyGossiping()
- New API to limit room key request only to own devices MXCryptoConfig#limitRoomKeyRequestsToMyDevices
- Event Trail API has changed, now using AuditTrail events
- New API to manually accept an incoming key request CryptoService#manuallyAcceptRoomKeyRequest()

View file

@ -19,6 +19,7 @@ package org.matrix.android.sdk.common
import android.os.SystemClock
import android.util.Log
import androidx.lifecycle.Observer
import org.amshove.kluent.fail
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
@ -31,8 +32,16 @@ import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
@ -40,13 +49,19 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxStat
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.awaitCallback
import org.matrix.android.sdk.api.util.toBase64NoPadding
import java.util.UUID
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@ -296,33 +311,94 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
}
}
/**
* Initialize cross-signing, set up megolm backup and save all in 4S
*/
fun bootstrapSecurity(session: Session) {
initializeCrossSigning(session)
val ssssService = session.sharedSecretStorageService()
testHelper.runBlockingTest {
val keyInfo = ssssService.generateKey(
UUID.randomUUID().toString(),
null,
"ssss_key",
EmptyKeySigner()
)
ssssService.setDefaultKey(keyInfo.keyId)
ssssService.storeSecret(
MASTER_KEY_SSSS_NAME,
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master!!,
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
)
ssssService.storeSecret(
SELF_SIGNING_KEY_SSSS_NAME,
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned!!,
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
)
ssssService.storeSecret(
USER_SIGNING_KEY_SSSS_NAME,
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user!!,
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
)
// set up megolm backup
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
}
val version = awaitCallback<KeysVersion> {
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
}
// Save it for gossiping
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret ->
ssssService.storeSecret(
KEYBACKUP_SECRET_SSSS_NAME,
secret,
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
)
}
}
}
fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) {
assertTrue(alice.cryptoService().crossSigningService().canCrossSign())
assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
val requestID = UUID.randomUUID().toString()
val aliceVerificationService = alice.cryptoService().verificationService()
val bobVerificationService = bob.cryptoService().verificationService()
aliceVerificationService.beginKeyVerificationInDMs(
VerificationMethod.SAS,
requestID,
roomId,
bob.myUserId,
bob.sessionParams.credentials.deviceId!!
)
val localId = UUID.randomUUID().toString()
aliceVerificationService.requestKeyVerificationInDMs(
localId = localId,
methods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
otherUserId = bob.myUserId,
roomId = roomId
).transactionId
// we should reach SHOW SAS on both
var alicePovTx: OutgoingSasVerificationTransaction? = null
var bobPovTx: IncomingSasVerificationTransaction? = null
// wait for alice to get the ready
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
if (bobPovTx?.state == VerificationTxState.OnStarted) {
bobPovTx?.performAccept()
bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull {
it.requestInfo?.fromDevice == alice.sessionParams.deviceId
} != null
}
}
val incomingRequest = bobVerificationService.getExistingVerificationRequests(alice.myUserId).first {
it.requestInfo?.fromDevice == alice.sessionParams.deviceId
}
bobVerificationService.readyPendingVerification(listOf(VerificationMethod.SAS), alice.myUserId, incomingRequest.transactionId!!)
var requestID: String? = null
// wait for it to be readied
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
val outgoingRequest = aliceVerificationService.getExistingVerificationRequests(bob.myUserId)
.firstOrNull { it.localId == localId }
if (outgoingRequest?.isReady == true) {
requestID = outgoingRequest.transactionId!!
true
} else {
false
@ -330,9 +406,20 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
}
}
aliceVerificationService.beginKeyVerificationInDMs(
VerificationMethod.SAS,
requestID!!,
roomId,
bob.myUserId,
bob.sessionParams.credentials.deviceId!!)
// we should reach SHOW SAS on both
var alicePovTx: OutgoingSasVerificationTransaction? = null
var bobPovTx: IncomingSasVerificationTransaction? = null
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID) as? OutgoingSasVerificationTransaction
alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction
Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}")
alicePovTx?.state == VerificationTxState.ShortCodeReady
}
@ -340,7 +427,7 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
// wait for alice to get the ready
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID!!) as? IncomingSasVerificationTransaction
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
if (bobPovTx?.state == VerificationTxState.OnStarted) {
bobPovTx?.performAccept()
@ -392,4 +479,50 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
return CryptoTestData(roomId, sessions)
}
fun ensureCanDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, messagesText: List<String>) {
sentEventIds.forEachIndexed { index, sentEventId ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root
testHelper.runBlockingTest {
try {
session.cryptoService().decryptEvent(event, "").let { result ->
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
}
} catch (error: MXCryptoError) {
// nop
}
}
Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}")
event.getClearType() == EventType.MESSAGE &&
messagesText[index] == event.getClearContent()?.toModel<MessageContent>()?.body
}
}
}
}
fun ensureCannotDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType? = null) {
sentEventIds.forEach { sentEventId ->
val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root
testHelper.runBlockingTest {
try {
session.cryptoService().decryptEvent(event, "")
fail("Should not be able to decrypt event")
} catch (error: MXCryptoError) {
val errorType = (error as? MXCryptoError.Base)?.errorType
if (expectedError == null) {
assertNotNull(errorType)
} else {
assertEquals("Unexpected reason", expectedError, errorType)
}
}
}
}
}
}

View file

@ -30,13 +30,21 @@ import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.RequestResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.getRoomSummary
@ -52,15 +60,13 @@ import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.common.TestMatrixCallback
import java.util.concurrent.CountDownLatch
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
class E2eeSanityTests : InstrumentedTest {
private val testHelper = CommonTestHelper(context())
private val cryptoTestHelper = CryptoTestHelper(testHelper)
/**
* Simple test that create an e2ee room.
* Some new members are added, and a message is sent.
@ -72,16 +78,24 @@ class E2eeSanityTests : InstrumentedTest {
*/
@Test
fun testSendingE2EEMessages() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
val e2eRoomID = cryptoTestData.roomId
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
// we want to disable key gossiping to just check initial sending of keys
aliceSession.cryptoService().enableKeyGossiping(false)
cryptoTestData.secondSession?.cryptoService()?.enableKeyGossiping(false)
// add some more users and invite them
val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu")
.map {
testHelper.createAccount(it, SessionTestParams(true))
testHelper.createAccount(it, SessionTestParams(true)).also {
it.cryptoService().enableKeyGossiping(false)
}
}
Log.v("#E2E TEST", "All accounts created")
@ -95,18 +109,18 @@ class E2eeSanityTests : InstrumentedTest {
// All user should accept invite
otherAccounts.forEach { otherSession ->
waitForAndAcceptInviteInRoom(otherSession, e2eRoomID)
waitForAndAcceptInviteInRoom(testHelper, otherSession, e2eRoomID)
Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
}
// check that alice see them as joined (not really necessary?)
ensureMembersHaveJoined(aliceSession, otherAccounts, e2eRoomID)
ensureMembersHaveJoined(testHelper, aliceSession, otherAccounts, e2eRoomID)
Log.v("#E2E TEST", "All users have joined the room")
Log.v("#E2E TEST", "Alice is sending the message")
val text = "This is my message"
val sentEventId: String? = sendMessageInRoom(aliceRoomPOV, text)
val sentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, text)
// val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first()
Assert.assertTrue("Message should be sent", sentEventId != null)
@ -114,10 +128,10 @@ class E2eeSanityTests : InstrumentedTest {
otherAccounts.forEach { otherSession ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE
val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
@ -136,10 +150,10 @@ class E2eeSanityTests : InstrumentedTest {
}
newAccount.forEach {
waitForAndAcceptInviteInRoom(it, e2eRoomID)
waitForAndAcceptInviteInRoom(testHelper, it, e2eRoomID)
}
ensureMembersHaveJoined(aliceSession, newAccount, e2eRoomID)
ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID)
// wait a bit
testHelper.runBlockingTest {
@ -164,7 +178,7 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Alice sends a new message")
val secondMessage = "2 This is my message"
val secondSentEventId: String? = sendMessageInRoom(aliceRoomPOV, secondMessage)
val secondSentEventId: String? = sendMessageInRoom(testHelper, aliceRoomPOV, secondMessage)
// new members should be able to decrypt it
newAccount.forEach { otherSession ->
@ -188,6 +202,14 @@ class E2eeSanityTests : InstrumentedTest {
cryptoTestData.cleanUp(testHelper)
}
@Test
fun testKeyGossipingIsEnabledByDefault() {
val testHelper = CommonTestHelper(context())
val session = testHelper.createAccount("alice", SessionTestParams(true))
Assert.assertTrue("Key gossiping should be enabled by default", session.cryptoService().isKeyGossipingEnabled())
testHelper.signOutAndClose(session)
}
/**
* Quick test for basic key backup
* 1. Create e2e between Alice and Bob
@ -204,6 +226,9 @@ class E2eeSanityTests : InstrumentedTest {
*/
@Test
fun testBasicBackupImport() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession!!
@ -227,16 +252,16 @@ class E2eeSanityTests : InstrumentedTest {
val sentEventIds = mutableListOf<String>()
val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning")
messagesText.forEach { text ->
val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also {
sentEventIds.add(it)
}
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE
val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
// we want more so let's discard the session
@ -289,22 +314,23 @@ class E2eeSanityTests : InstrumentedTest {
}
}
// after initial sync events are not decrypted, so we have to try manually
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
// Let's now import keys from backup
newBobSession.cryptoService().keysBackupService().let { keysBackupService ->
newBobSession.cryptoService().keysBackupService().let { kbs ->
val keyVersionResult = testHelper.doSync<KeysVersionResult?> {
keysBackupService.getVersion(version.version, it)
kbs.getVersion(version.version, it)
}
val importedResult = testHelper.doSync<ImportRoomKeysResult> {
keysBackupService.restoreKeyBackupWithPassword(
kbs.restoreKeyBackupWithPassword(
keyVersionResult!!,
keyBackupPassword,
null,
null,
null, it
null,
it
)
}
@ -312,7 +338,7 @@ class E2eeSanityTests : InstrumentedTest {
}
// ensure bob can now decrypt
ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
testHelper.signOutAndClose(newBobSession)
}
@ -323,6 +349,9 @@ class E2eeSanityTests : InstrumentedTest {
*/
@Test
fun testSimpleGossip() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
val bobSession = cryptoTestData.secondSession!!
@ -330,30 +359,28 @@ class E2eeSanityTests : InstrumentedTest {
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
cryptoTestHelper.initializeCrossSigning(bobSession)
// let's send a few message to bob
val sentEventIds = mutableListOf<String>()
val messagesText = listOf("1. Hello", "2. Bob")
Log.v("#E2E TEST", "Alice sends some messages")
messagesText.forEach { text ->
val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
val sentEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!.also {
sentEventIds.add(it)
}
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE
val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
// Ensure bob can decrypt
ensureIsDecrypted(sentEventIds, bobSession, e2eRoomID)
ensureIsDecrypted(testHelper, sentEventIds, bobSession, e2eRoomID)
// Let's now add a new bob session
// Create a new session for bob
@ -363,7 +390,11 @@ class E2eeSanityTests : InstrumentedTest {
// check that new bob can't currently decrypt
Log.v("#E2E TEST", "check that new bob can't currently decrypt")
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
// newBobSession.cryptoService().getOutgoingRoomKeyRequests()
// .firstOrNull {
// it.sessionId ==
// }
// Try to request
sentEventIds.forEach { sentEventId ->
@ -372,12 +403,34 @@ class E2eeSanityTests : InstrumentedTest {
}
// wait a bit
testHelper.runBlockingTest {
delay(10_000)
}
// we need to wait a couple of syncs to let sharing occurs
// testHelper.waitFewSyncs(newBobSession, 6)
// Ensure that new bob still can't decrypt (keys must have been withheld)
ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.KEYS_WITHHELD)
sentEventIds.forEach { sentEventId ->
val megolmSessionId = newBobSession.getRoom(e2eRoomID)!!
.getTimelineEvent(sentEventId)!!
.root.content.toModel<EncryptedEventContent>()!!.sessionId
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests()
.first {
it.sessionId == megolmSessionId &&
it.roomId == e2eRoomID
}
.results.also {
Log.w("##TEST", "result list is $it")
}
.firstOrNull { it.userId == aliceSession.myUserId }
?.result
aliceReply != null &&
aliceReply is RequestResult.Failure &&
WithHeldCode.UNAUTHORISED == aliceReply.code
}
}
}
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
// Now mark new bob session as verified
@ -390,12 +443,7 @@ class E2eeSanityTests : InstrumentedTest {
newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
}
// wait a bit
testHelper.runBlockingTest {
delay(10_000)
}
ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
cryptoTestData.cleanUp(testHelper)
testHelper.signOutAndClose(newBobSession)
@ -406,6 +454,9 @@ class E2eeSanityTests : InstrumentedTest {
*/
@Test
fun testForwardBetterKey() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession
val bobSessionWithBetterKey = cryptoTestData.secondSession!!
@ -413,35 +464,33 @@ class E2eeSanityTests : InstrumentedTest {
val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
cryptoTestHelper.initializeCrossSigning(bobSessionWithBetterKey)
// let's send a few message to bob
var firstEventId: String
val firstMessage = "1. Hello"
Log.v("#E2E TEST", "Alice sends some messages")
firstMessage.let { text ->
firstEventId = sendMessageInRoom(aliceRoomPOV, text)!!
firstEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timelineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE
val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
// Ensure bob can decrypt
ensureIsDecrypted(listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID)
ensureIsDecrypted(testHelper, listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID)
// Let's add a new unverified session from bob
val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true))
// check that new bob can't currently decrypt
Log.v("#E2E TEST", "check that new bob can't currently decrypt")
ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
cryptoTestHelper.ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, null)
// Now let alice send a new message. this time the new bob session will be able to decrypt
var secondEventId: String
@ -449,14 +498,14 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "Alice sends some messages")
secondMessage.let { text ->
secondEventId = sendMessageInRoom(aliceRoomPOV, text)!!
secondEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE
val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
@ -475,9 +524,7 @@ class E2eeSanityTests : InstrumentedTest {
try {
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
fail("Should not be able to decrypt event")
} catch (error: MXCryptoError) {
val errorType = (error as? MXCryptoError.Base)?.errorType
assertEquals(MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, errorType)
} catch (_: MXCryptoError) {
}
}
@ -499,41 +546,45 @@ class E2eeSanityTests : InstrumentedTest {
.markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!)
// now let new session request
newBobSession.cryptoService().requestRoomKeyForEvent(firstEventNewBobPov.root)
newBobSession.cryptoService().reRequestRoomKeyForEvent(firstEventNewBobPov.root)
// wait a bit
testHelper.runBlockingTest {
delay(10_000)
}
// We need to wait for the key request to be sent out and then a reply to be received
// old session should have shared the key at earliest known index now
// we should be able to decrypt both
testHelper.runBlockingTest {
try {
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
} catch (error: MXCryptoError) {
fail("Should be able to decrypt first event now $error")
}
}
testHelper.runBlockingTest {
try {
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
} catch (error: MXCryptoError) {
fail("Should be able to decrypt event $error")
testHelper.waitWithLatch {
testHelper.retryPeriodicallyWithLatch(it) {
val canDecryptFirst = try {
testHelper.runBlockingTest {
newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
}
true
} catch (error: MXCryptoError) {
false
}
val canDecryptSecond = try {
testHelper.runBlockingTest {
newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
}
true
} catch (error: MXCryptoError) {
false
}
canDecryptFirst && canDecryptSecond
}
}
cryptoTestData.cleanUp(testHelper)
testHelper.signOutAndClose(aliceSession)
testHelper.signOutAndClose(bobSessionWithBetterKey)
testHelper.signOutAndClose(newBobSession)
}
private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? {
private fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? {
aliceRoomPOV.sendService().sendTextMessage(text)
var sentEventId: String? = null
testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60))
timeline.start()
testHelper.retryPeriodicallyWithLatch(latch) {
val decryptedMsg = timeline.getSnapshot()
.filter { it.root.getClearType() == EventType.MESSAGE }
@ -552,7 +603,157 @@ class E2eeSanityTests : InstrumentedTest {
return sentEventId
}
private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {
/**
* Test that if a better key is forwared (lower index, it is then used)
*/
@Test
fun testSelfInteractiveVerificationAndGossip() {
val testHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(testHelper)
val aliceSession = testHelper.createAccount("alice", SessionTestParams(true))
cryptoTestHelper.bootstrapSecurity(aliceSession)
// now let's create a new login from alice
val aliceNewSession = testHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
val oldCompleteLatch = CountDownLatch(1)
lateinit var oldCode: String
aliceSession.cryptoService().verificationService().addListener(object : VerificationService.Listener {
override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
val readyInfo = pr.readyInfo
if (readyInfo != null) {
aliceSession.cryptoService().verificationService().beginKeyVerification(
VerificationMethod.SAS,
aliceSession.myUserId,
readyInfo.fromDevice,
readyInfo.transactionId
)
}
}
override fun transactionUpdated(tx: VerificationTransaction) {
Log.d("##TEST", "exitsingPov: $tx")
val sasTx = tx as OutgoingSasVerificationTransaction
when (sasTx.uxState) {
OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> {
// for the test we just accept?
oldCode = sasTx.getDecimalCodeRepresentation()
sasTx.userHasVerifiedShortCode()
}
OutgoingSasVerificationTransaction.UxState.VERIFIED -> {
// we can release this latch?
oldCompleteLatch.countDown()
}
else -> Unit
}
}
})
val newCompleteLatch = CountDownLatch(1)
lateinit var newCode: String
aliceNewSession.cryptoService().verificationService().addListener(object : VerificationService.Listener {
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
// let's ready
aliceNewSession.cryptoService().verificationService().readyPendingVerification(
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
aliceSession.myUserId,
pr.transactionId!!
)
}
var matchOnce = true
override fun transactionUpdated(tx: VerificationTransaction) {
Log.d("##TEST", "newPov: $tx")
val sasTx = tx as IncomingSasVerificationTransaction
when (sasTx.uxState) {
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
// no need to accept as there was a request first it will auto accept
}
IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
if (matchOnce) {
sasTx.userHasVerifiedShortCode()
newCode = sasTx.getDecimalCodeRepresentation()
matchOnce = false
}
}
IncomingSasVerificationTransaction.UxState.VERIFIED -> {
newCompleteLatch.countDown()
}
else -> Unit
}
}
})
// initiate self verification
aliceSession.cryptoService().verificationService().requestKeyVerification(
listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
aliceNewSession.myUserId,
listOf(aliceNewSession.sessionParams.deviceId!!)
)
testHelper.await(oldCompleteLatch)
testHelper.await(newCompleteLatch)
assertEquals("Decimal code should have matched", oldCode, newCode)
// Assert that devices are verified
val newDeviceFromOldPov: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId)
val oldDeviceFromNewPov: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId)
Assert.assertTrue("new device should be verified from old point of view", newDeviceFromOldPov!!.isVerified)
Assert.assertTrue("old device should be verified from new point of view", oldDeviceFromNewPov!!.isVerified)
// wait for secret gossiping to happen
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown()
}
}
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null
}
}
assertEquals(
"MSK Private parts should be the same",
aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master,
aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master
)
assertEquals(
"USK Private parts should be the same",
aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user,
aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user
)
assertEquals(
"SSK Private parts should be the same",
aliceSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned,
aliceNewSession.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned
)
// Let's check that we have the megolm backup key
assertEquals(
"Megolm key should be the same",
aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey,
aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.recoveryKey
)
assertEquals(
"Megolm version should be the same",
aliceSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version,
aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()!!.version
)
testHelper.signOutAndClose(aliceSession)
testHelper.signOutAndClose(aliceNewSession)
}
private fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
otherAccounts.map {
@ -564,7 +765,7 @@ class E2eeSanityTests : InstrumentedTest {
}
}
private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) {
private fun waitForAndAcceptInviteInRoom(testHelper: CommonTestHelper, otherSession: Session, e2eRoomID: String) {
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val roomSummary = otherSession.getRoomSummary(e2eRoomID)
@ -576,7 +777,8 @@ class E2eeSanityTests : InstrumentedTest {
}
}
testHelper.runBlockingTest(60_000) {
// not sure why it's taking so long :/
testHelper.runBlockingTest(90_000) {
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
try {
otherSession.roomService().joinRoom(e2eRoomID)
@ -594,59 +796,14 @@ class E2eeSanityTests : InstrumentedTest {
}
}
private fun ensureCanDecrypt(sentEventIds: MutableList<String>, session: Session, e2eRoomID: String, messagesText: List<String>) {
sentEventIds.forEachIndexed { index, sentEventId ->
testHelper.waitWithLatch { latch ->
testHelper.retryPeriodicallyWithLatch(latch) {
val event = session.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
testHelper.runBlockingTest {
try {
session.cryptoService().decryptEvent(event, "").let { result ->
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
}
} catch (error: MXCryptoError) {
// nop
}
}
event.getClearType() == EventType.MESSAGE &&
messagesText[index] == event.getClearContent()?.toModel<MessageContent>()?.body
}
}
}
}
private fun ensureIsDecrypted(sentEventIds: List<String>, session: Session, e2eRoomID: String) {
private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List<String>, session: Session, e2eRoomID: String) {
testHelper.waitWithLatch { latch ->
sentEventIds.forEach { sentEventId ->
testHelper.retryPeriodicallyWithLatch(latch) {
val timelineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE
}
}
}
}
private fun ensureCannotDecrypt(sentEventIds: List<String>, newBobSession: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType?) {
sentEventIds.forEach { sentEventId ->
val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
testHelper.runBlockingTest {
try {
newBobSession.cryptoService().decryptEvent(event, "")
fail("Should not be able to decrypt event")
} catch (error: MXCryptoError) {
val errorType = (error as? MXCryptoError.Base)?.errorType
if (expectedError == null) {
Assert.assertNotNull(errorType)
} else {
assertEquals(expectedError, errorType, "Message expected to be UISI")
}
val timeLineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
timeLineEvent != null &&
timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE
}
}
}

View file

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

View file

@ -19,59 +19,45 @@ package org.matrix.android.sdk.internal.crypto.gossiping
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import junit.framework.TestCase.fail
import org.amshove.kluent.internal.assertEquals
import org.junit.Assert
import org.junit.Assert.assertNull
import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
import org.matrix.android.sdk.api.session.crypto.RequestResult
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
class KeyShareTests : InstrumentedTest {
private val commonTestHelper = CommonTestHelper(context())
@Test
@Ignore("This test will be ignored until it is fixed")
fun test_DoNotSelfShareIfNotTrusted() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}")
// Create an encrypted room and add a message
val roomId = commonTestHelper.runBlockingTest {
@ -86,11 +72,18 @@ class KeyShareTests : InstrumentedTest {
assertNotNull(room)
Thread.sleep(4_000)
assertTrue(room?.roomCryptoService()?.isEncrypted() == true)
val sentEventId = commonTestHelper.sendTextMessage(room!!, "My Message", 1).first().eventId
// Open a new sessionx
val sentEvent = commonTestHelper.sendTextMessage(room!!, "My Message", 1).first()
val sentEventId = sentEvent.eventId
val sentEventText = sentEvent.getLastMessageContent()?.body
val aliceSession2 = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
// Open a new session
val aliceSession2 = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(false))
// block key requesting for now as decrypt will send requests (room summary is trying to decrypt)
aliceSession2.cryptoService().enableKeyGossiping(false)
commonTestHelper.syncSession(aliceSession2)
Log.v("TEST", "=======> AliceSession 2 is ${aliceSession2.sessionParams.deviceId}")
val roomSecondSessionPOV = aliceSession2.getRoom(roomId)
@ -107,7 +100,10 @@ class KeyShareTests : InstrumentedTest {
}
val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
assertEquals("There should be no request as it's disabled", 0, outgoingRequestsBefore.size)
// Try to request
aliceSession2.cryptoService().enableKeyGossiping(true)
aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root)
val eventMegolmSessionId = receivedEvent.root.content.toModel<EncryptedEventContent>()?.sessionId
@ -117,10 +113,6 @@ class KeyShareTests : InstrumentedTest {
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
.filter { req ->
// filter out request that was known before
!outgoingRequestsBefore.any { req.requestId == it.requestId }
}
.let {
val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
outGoingRequestId = outgoing?.requestId
@ -141,20 +133,34 @@ class KeyShareTests : InstrumentedTest {
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
// DEBUG LOGS
aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
// aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
// Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
// Log.v("TEST", "=========================")
// it.forEach { keyRequest ->
// Log.v("TEST", "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
// }
// Log.v("TEST", "=========================")
// }
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
incoming != null
}
}
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
// DEBUG LOGS
aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest ->
Log.v("TEST", "=========================")
it.forEach { keyRequest ->
Log.v(
"TEST",
"[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId} is ${keyRequest.state}"
)
}
Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
Log.v("TEST", "=========================")
}
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
incoming?.state == GossipingRequestState.REJECTED
val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
val reply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
val resultCode = (reply?.result as? RequestResult.Failure)?.code
resultCode == WithHeldCode.UNVERIFIED
}
}
@ -175,254 +181,301 @@ class KeyShareTests : InstrumentedTest {
// Re request
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
Log.v("TEST", "Incoming request Session 1")
Log.v("TEST", "=========================")
it.forEach {
Log.v("TEST", "requestId ${it.requestId}, for sessionId ${it.requestBody?.sessionId} is ${it.state}")
}
Log.v("TEST", "=========================")
it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == GossipingRequestState.ACCEPTED }
}
}
}
Thread.sleep(6_000)
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
aliceSession2.cryptoService().getOutgoingRoomKeyRequests().let {
it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == OutgoingGossipingRequestState.CANCELLED }
}
}
}
try {
commonTestHelper.runBlockingTest {
aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
}
} catch (failure: Throwable) {
fail("should have been able to decrypt")
}
cryptoTestHelper.ensureCanDecrypt(listOf(receivedEvent.eventId), aliceSession2, roomId, listOf(sentEventText ?: ""))
commonTestHelper.signOutAndClose(aliceSession)
commonTestHelper.signOutAndClose(aliceSession2)
}
// See E2ESanityTest for a test regarding secret sharing
/**
* Test that the sender of a message accepts to re-share to another user
* if the key was originally shared with him
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun test_ShareSSSSSecret() {
val aliceSession1 = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
fun test_reShareIfWasIntendedToBeShared() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
commonTestHelper.doSync<Unit> {
aliceSession1.cryptoService().crossSigningService()
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = aliceSession1.myUserId,
password = TestConstants.PASSWORD
)
)
}
}, it
)
}
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = testData.firstSession
val roomFromAlice = aliceSession.getRoom(testData.roomId)!!
val bobSession = testData.secondSession!!
// Also bootstrap keybackup on first session
val creationInfo = commonTestHelper.doSync<MegolmBackupCreationInfo> {
aliceSession1.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
}
val version = commonTestHelper.doSync<KeysVersion> {
aliceSession1.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
}
// Save it for gossiping
aliceSession1.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first()
val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
val aliceSession2 = commonTestHelper.logIntoAccount(aliceSession1.myUserId, SessionTestParams(true))
// bob should be able to decrypt
cryptoTestHelper.ensureCanDecrypt(listOf(sentEvent.eventId), bobSession, testData.roomId, listOf(sentEvent.getLastMessageContent()?.body ?: ""))
val aliceVerificationService1 = aliceSession1.cryptoService().verificationService()
val aliceVerificationService2 = aliceSession2.cryptoService().verificationService()
// force keys download
commonTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
aliceSession1.cryptoService().downloadKeys(listOf(aliceSession1.myUserId), true, it)
}
commonTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
aliceSession2.cryptoService().downloadKeys(listOf(aliceSession2.myUserId), true, it)
}
var session1ShortCode: String? = null
var session2ShortCode: String? = null
aliceVerificationService1.addListener(object : VerificationService.Listener {
override fun transactionUpdated(tx: VerificationTransaction) {
Log.d("#TEST", "AA: tx incoming?:${tx.isIncoming} state ${tx.state}")
if (tx is SasVerificationTransaction) {
if (tx.state == VerificationTxState.OnStarted) {
(tx as IncomingSasVerificationTransaction).performAccept()
}
if (tx.state == VerificationTxState.ShortCodeReady) {
session1ShortCode = tx.getDecimalCodeRepresentation()
Thread.sleep(500)
tx.userHasVerifiedShortCode()
}
}
}
})
aliceVerificationService2.addListener(object : VerificationService.Listener {
override fun transactionUpdated(tx: VerificationTransaction) {
Log.d("#TEST", "BB: tx incoming?:${tx.isIncoming} state ${tx.state}")
if (tx is SasVerificationTransaction) {
if (tx.state == VerificationTxState.ShortCodeReady) {
session2ShortCode = tx.getDecimalCodeRepresentation()
Thread.sleep(500)
tx.userHasVerifiedShortCode()
}
}
}
})
val txId = "m.testVerif12"
aliceVerificationService2.beginKeyVerification(
VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId
?: "", txId
)
// Let's try to request any how.
// As it was share previously alice should accept to reshare
bobSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.deviceId ?: "")?.isVerified == true
val outgoing = bobSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val aliceReply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
aliceReply != null && aliceReply.result is RequestResult.Success
}
}
assertNotNull(session1ShortCode)
Log.d("#TEST", "session1ShortCode: $session1ShortCode")
assertNotNull(session2ShortCode)
Log.d("#TEST", "session2ShortCode: $session2ShortCode")
assertEquals(session1ShortCode, session2ShortCode)
// SSK and USK private keys should have been shared
commonTestHelper.waitWithLatch(60_000) { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
Log.d("#TEST", "CAN XS :${aliceSession2.cryptoService().crossSigningService().getMyCrossSigningKeys()}")
aliceSession2.cryptoService().crossSigningService().canCrossSign()
}
}
// Test that key backup key has been shared to
commonTestHelper.waitWithLatch(60_000) { latch ->
val keysBackupService = aliceSession2.cryptoService().keysBackupService()
commonTestHelper.retryPeriodicallyWithLatch(latch) {
Log.d("#TEST", "Recovery :${keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
}
}
commonTestHelper.signOutAndClose(aliceSession1)
commonTestHelper.signOutAndClose(aliceSession2)
}
/**
* Test that our own devices accept to reshare to unverified device if it was shared initialy
* if the key was originally shared with him
*/
@Test
@Ignore("This test will be ignored until it is fixed")
fun test_ImproperKeyShareBug() {
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
fun test_reShareToUnverifiedIfWasIntendedToBeShared() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
commonTestHelper.doSync<Unit> {
aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, it
)
val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true)
val aliceSession = testData.firstSession
val roomFromAlice = aliceSession.getRoom(testData.roomId)!!
val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
// we wait for alice first session to be aware of that session?
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
val newSession = aliceSession.cryptoService().getUserDevices(aliceSession.myUserId)
.firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
newSession != null
}
}
val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first()
val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
// Create an encrypted room and send a couple of messages
val roomId = commonTestHelper.runBlockingTest {
aliceSession.roomService().createRoom(
CreateRoomParams().apply {
visibility = RoomDirectoryVisibility.PRIVATE
enableEncryption()
}
)
// Let's try to request any how.
// As it was share previously alice should accept to reshare
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val ownDeviceReply =
outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
ownDeviceReply != null && ownDeviceReply.result is RequestResult.Success
}
}
val roomAlicePov = aliceSession.getRoom(roomId)
assertNotNull(roomAlicePov)
Thread.sleep(1_000)
assertTrue(roomAlicePov?.roomCryptoService()?.isEncrypted() == true)
val secondEventId = commonTestHelper.sendTextMessage(roomAlicePov!!, "Message", 3)[1].eventId
}
// Create bob session
/**
* Tests that keys reshared with own verified session are done from the earliest known index
*/
@Test
fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true))
commonTestHelper.doSync<Unit> {
bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, it
)
}
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = testData.firstSession
val bobSession = testData.secondSession!!
val roomFromBob = bobSession.getRoom(testData.roomId)!!
// Let alice invite bob
commonTestHelper.runBlockingTest {
roomAlicePov.membershipService().invite(bobSession.myUserId, null)
}
val sentEvents = commonTestHelper.sendTextMessage(roomFromBob, "Hello", 3)
val sentEventMegolmSession = sentEvents.first().root.content.toModel<EncryptedEventContent>()!!.sessionId!!
commonTestHelper.runBlockingTest {
bobSession.roomService().joinRoom(roomAlicePov.roomId, null, emptyList())
}
// Let alice now add a new session
val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(false))
aliceNewSession.cryptoService().enableKeyGossiping(false)
commonTestHelper.syncSession(aliceNewSession)
// we want to discard alice outbound session
aliceSession.cryptoService().discardOutboundSession(roomAlicePov.roomId)
// and now resend a new message to reset index to 0
commonTestHelper.sendTextMessage(roomAlicePov, "After", 1)
val roomRoomBobPov = aliceSession.getRoom(roomId)
val beforeJoin = roomRoomBobPov!!.getTimelineEvent(secondEventId)
var dRes = tryOrNull {
commonTestHelper.runBlockingTest {
bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "")
// we wait bob first session to be aware of that session?
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId)
.firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
newSession != null
}
}
assert(dRes == null)
val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first()
val newEventId = newEvent.eventId
val newEventText = newEvent.getLastMessageContent()!!.body
// Try to re-ask the keys
// alice should be able to decrypt the new one
cryptoTestHelper.ensureCanDecrypt(listOf(newEventId), aliceNewSession, testData.roomId, listOf(newEventText))
// but not the first one!
cryptoTestHelper.ensureCannotDecrypt(sentEvents.map { it.eventId }, aliceNewSession, testData.roomId)
bobSession.cryptoService().reRequestRoomKeyForEvent(beforeJoin!!.root)
// All should be using the same session id
sentEvents.forEach {
assertEquals(sentEventMegolmSession, it.root.content.toModel<EncryptedEventContent>()!!.sessionId)
}
assertEquals(sentEventMegolmSession, newEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId)
Thread.sleep(3_000)
// Request a first time, bob should reply with unauthorized and alice should reply with unverified
aliceNewSession.cryptoService().enableKeyGossiping(true)
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(newEvent.root)
// With the bug the first session would have improperly reshare that key :/
dRes = tryOrNull {
commonTestHelper.runBlockingTest {
bobSession.cryptoService().decryptEvent(beforeJoin.root, "")
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val ownDeviceReply = outgoing?.results
?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
val result = ownDeviceReply?.result
Log.v("TEST", "own device result is $result")
result != null && result is RequestResult.Failure && result.code == WithHeldCode.UNVERIFIED
}
}
Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel<MessageContent>()?.body}")
assert(dRes?.clearEvent == null)
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val bobDeviceReply = outgoing?.results
?.firstOrNull { it.userId == bobSession.myUserId && it.fromDevice == bobSession.sessionParams.deviceId }
val result = bobDeviceReply?.result
Log.v("TEST", "bob device result is $result")
result != null && result is RequestResult.Success && result.chainIndex > 0
}
}
// it's a success but still can't decrypt first message
cryptoTestHelper.ensureCannotDecrypt(sentEvents.map { it.eventId }, aliceNewSession, testData.roomId)
// Mark the new session as verified
aliceSession.cryptoService()
.verificationService()
.markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!)
// Let's now try to request
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root)
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
// DEBUG LOGS
aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest ->
Log.v("TEST", "=========================")
Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
Log.v("TEST", "=========================")
}
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val ownDeviceReply =
outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
val result = ownDeviceReply?.result
result != null && result is RequestResult.Success && result.chainIndex == 0
}
}
// now the new session should be able to decrypt all!
cryptoTestHelper.ensureCanDecrypt(
sentEvents.map { it.eventId },
aliceNewSession,
testData.roomId,
sentEvents.map { it.getLastMessageContent()!!.body }
)
// Additional test, can we check that bob replied successfully but with a ratcheted key
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId }
val result = bobReply?.result
result != null && result is RequestResult.Success && result.chainIndex == 3
}
}
commonTestHelper.signOutAndClose(aliceNewSession)
commonTestHelper.signOutAndClose(aliceSession)
commonTestHelper.signOutAndClose(bobSession)
}
/**
* Tests that we don't cancel a request to early on first forward if the index is not good enough
*/
@Test
fun test_dontCancelToEarly() {
val commonTestHelper = CommonTestHelper(context())
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = testData.firstSession
val bobSession = testData.secondSession!!
val roomFromBob = bobSession.getRoom(testData.roomId)!!
val sentEvents = commonTestHelper.sendTextMessage(roomFromBob, "Hello", 3)
val sentEventMegolmSession = sentEvents.first().root.content.toModel<EncryptedEventContent>()!!.sessionId!!
// Let alice now add a new session
val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
// we wait bob first session to be aware of that session?
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId)
.firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
newSession != null
}
}
val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first()
val newEventId = newEvent.eventId
val newEventText = newEvent.getLastMessageContent()!!.body
// alice should be able to decrypt the new one
cryptoTestHelper.ensureCanDecrypt(listOf(newEventId), aliceNewSession, testData.roomId, listOf(newEventText))
// but not the first one!
cryptoTestHelper.ensureCannotDecrypt(sentEvents.map { it.eventId }, aliceNewSession, testData.roomId)
// All should be using the same session id
sentEvents.forEach {
assertEquals(sentEventMegolmSession, it.root.content.toModel<EncryptedEventContent>()!!.sessionId)
}
assertEquals(sentEventMegolmSession, newEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId)
// Mark the new session as verified
aliceSession.cryptoService()
.verificationService()
.markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!)
// /!\ Stop initial alice session syncing so that it can't reply
aliceSession.cryptoService().enableKeyGossiping(false)
aliceSession.stopSync()
// Let's now try to request
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root)
// Should get a reply from bob and not from alice
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
// Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}")
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId }
val result = bobReply?.result
result != null && result is RequestResult.Success && result.chainIndex == 3
}
}
val outgoingReq = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
assertNull("We should not have a reply from first session", outgoingReq!!.results.firstOrNull { it.fromDevice == aliceSession.sessionParams.deviceId })
assertEquals("The request should not be canceled", OutgoingRoomKeyRequestState.SENT, outgoingReq.state)
// let's wake up alice
aliceSession.cryptoService().enableKeyGossiping(true)
aliceSession.startSync(true)
// We should now get a reply from first session
commonTestHelper.waitWithLatch { latch ->
commonTestHelper.retryPeriodicallyWithLatch(latch) {
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
val ownDeviceReply =
outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
val result = ownDeviceReply?.result
result != null && result is RequestResult.Success && result.chainIndex == 0
}
}
// It should be in sent then cancel
val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
assertEquals("The request should be canceled", OutgoingRoomKeyRequestState.SENT_THEN_CANCELED, outgoing!!.state)
commonTestHelper.signOutAndClose(aliceNewSession)
commonTestHelper.signOutAndClose(aliceSession)
commonTestHelper.signOutAndClose(bobSession)
}
}

View file

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

View file

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

View file

@ -31,5 +31,11 @@ data class MXCryptoConfig constructor(
* If set to false, the request will be forwarded to the application layer; in this
* case the application can decide to prompt the user.
*/
val discardRoomKeyRequestsFromUntrustedDevices: Boolean = true
val discardRoomKeyRequestsFromUntrustedDevices: Boolean = true,
/**
* Currently megolm keys are requested to the sender device and to all of our devices.
* You can limit request only to your sessions by turning this setting to `true`
*/
val limitRoomKeyRequestsToMyDevices: Boolean = false,
)

View file

@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningServic
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
@ -35,8 +36,6 @@ import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
@ -76,6 +75,15 @@ interface CryptoService {
fun setGlobalBlacklistUnverifiedDevices(block: Boolean)
/**
* Enable or disable key gossiping.
* Default is true.
* If set to false this device won't send key_request nor will accept key forwarded
*/
fun enableKeyGossiping(enable: Boolean)
fun isKeyGossipingEnabled(): Boolean
fun setRoomUnBlacklistUnverifiedDevices(roomId: String)
fun getDeviceTrackingStatus(userId: String): Int
@ -94,8 +102,6 @@ interface CryptoService {
fun reRequestRoomKeyForEvent(event: Event)
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody)
fun addRoomKeysRequestListener(listener: GossipingRequestListener)
fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
@ -142,14 +148,20 @@ interface CryptoService {
fun addNewSessionListener(newSessionListener: NewSessionListener)
fun removeSessionListener(listener: NewSessionListener)
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest>
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>>
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
fun getGossipingEvents(): List<Event>
/**
* Can be called by the app layer to accept a request manually
* Use carefully as it is prone to social attacks
*/
suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest)
fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>>
fun getGossipingEvents(): List<AuditTrail>
// For testing shared session
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.crypto
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
data class RequestReply(
val userId: String,
val fromDevice: String?,
val result: RequestResult
)
sealed class RequestResult {
data class Success(val chainIndex: Int) : RequestResult()
data class Failure(val code: WithHeldCode) : RequestResult()
}
data class OutgoingKeyRequest(
var requestBody: RoomKeyRequestBody?,
// recipients for the request map of users to list of deviceId
val recipients: Map<String, List<String>>,
val fromIndex: Int,
// Unique id for this request. Used for both
// an id within the request for later pairing with a cancellation, and for
// the transaction id when sending the to_device messages to our local
val requestId: String, // current state of this request
val state: OutgoingRoomKeyRequestState,
val results: List<RequestReply>
) {
/**
* Used only for log.
*
* @return the room id.
*/
val roomId = requestBody?.roomId
/**
* Used only for log.
*
* @return the session id
*/
val sessionId = requestBody?.sessionId
}

View file

@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,14 +14,20 @@
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.crypto.model
package org.matrix.android.sdk.api.session.crypto
enum class OutgoingGossipingRequestState {
enum class OutgoingRoomKeyRequestState {
UNSENT,
SENDING,
SENT,
CANCELLING,
CANCELLED,
FAILED_TO_SEND,
FAILED_TO_CANCEL
SENT_THEN_CANCELED,
CANCELLATION_PENDING,
CANCELLATION_PENDING_AND_WILL_RESEND;
companion object {
fun pendingStates() = setOf(
UNSENT,
CANCELLATION_PENDING_AND_WILL_RESEND,
CANCELLATION_PENDING
)
}
}

View file

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

View file

@ -0,0 +1,85 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.crypto.model
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
enum class TrailType {
OutgoingKeyForward,
IncomingKeyForward,
OutgoingKeyWithheld,
IncomingKeyRequest,
Unknown
}
interface AuditInfo {
val roomId: String
val sessionId: String
val senderKey: String
val alg: String
val userId: String
val deviceId: String
}
@JsonClass(generateAdapter = true)
data class ForwardInfo(
override val roomId: String,
override val sessionId: String,
override val senderKey: String,
override val alg: String,
override val userId: String,
override val deviceId: String,
val chainIndex: Long?
) : AuditInfo
object UnknownInfo : AuditInfo {
override val roomId: String = ""
override val sessionId: String = ""
override val senderKey: String = ""
override val alg: String = ""
override val userId: String = ""
override val deviceId: String = ""
}
@JsonClass(generateAdapter = true)
data class WithheldInfo(
override val roomId: String,
override val sessionId: String,
override val senderKey: String,
override val alg: String,
val code: WithHeldCode,
override val userId: String,
override val deviceId: String
) : AuditInfo
@JsonClass(generateAdapter = true)
data class IncomingKeyRequestInfo(
override val roomId: String,
override val sessionId: String,
override val senderKey: String,
override val alg: String,
override val userId: String,
override val deviceId: String,
val requestId: String
) : AuditInfo
data class AuditTrail(
val ageLocalTs: Long,
val type: TrailType,
val info: AuditInfo
)

View file

@ -1,64 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.crypto.model
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation
/**
* IncomingRequestCancellation describes the incoming room key cancellation.
*/
data class IncomingRequestCancellation(
/**
* The user id
*/
override val userId: String? = null,
/**
* The device id
*/
override val deviceId: String? = null,
/**
* The request id
*/
override val requestId: String? = null,
override val localCreationTimestamp: Long?
) : IncomingShareRequestCommon {
companion object {
/**
* Factory
*
* @param event the event
* @param currentTimeMillis the current time in milliseconds
*/
fun fromEvent(event: Event, currentTimeMillis: Long): IncomingRequestCancellation? {
return event.getClearContent()
.toModel<ShareRequestCancellation>()
?.let {
IncomingRequestCancellation(
userId = event.senderId,
deviceId = it.requestingDeviceId,
requestId = it.requestId,
localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis
)
}
}
}
}

View file

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

View file

@ -1,83 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.crypto.model
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
/**
* IncomingSecretShareRequest class defines the incoming secret keys request.
*/
data class IncomingSecretShareRequest(
/**
* The user id
*/
override val userId: String? = null,
/**
* The device id
*/
override val deviceId: String? = null,
/**
* The request id
*/
override val requestId: String? = null,
/**
* The request body
*/
val secretName: String? = null,
/**
* The runnable to call to accept to share the keys
*/
@Transient
var share: ((String) -> Unit)? = null,
/**
* The runnable to call to ignore the key share request.
*/
@Transient
var ignore: Runnable? = null,
override val localCreationTimestamp: Long?
) : IncomingShareRequestCommon {
companion object {
/**
* Factory
*
* @param event the event
* @param currentTimeMillis the current time in milliseconds
*/
fun fromEvent(event: Event, currentTimeMillis: Long): IncomingSecretShareRequest? {
return event.getClearContent()
.toModel<SecretShareRequest>()
?.let {
IncomingSecretShareRequest(
userId = event.senderId,
deviceId = it.requestingDeviceId,
requestId = it.requestId,
secretName = it.secretName,
localCreationTimestamp = event.ageLocalTs ?: currentTimeMillis
)
}
}
}
}

View file

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

View file

@ -52,7 +52,13 @@ data class RoomKeyWithHeldContent(
/**
* A human-readable reason for why the key was not sent. The receiving client should only use this string if it does not understand the code.
*/
@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,123 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import android.content.Context
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.util.time.Clock
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import javax.inject.Inject
internal class CancelGossipRequestWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) :
SessionSafeCoroutineWorker<CancelGossipRequestWorker.Params>(context, params, sessionManager, Params::class.java) {
@JsonClass(generateAdapter = true)
internal data class Params(
override val sessionId: String,
val requestId: String,
val recipients: Map<String, List<String>>,
// The txnId for the sendToDevice request. Nullable for compatibility reasons, but MUST always be provided
// to use the same value if this worker is retried.
val txnId: String? = null,
override val lastFailureMessage: String? = null
) : SessionWorkerParams {
companion object {
fun fromRequest(sessionId: String, request: OutgoingGossipingRequest): Params {
return Params(
sessionId = sessionId,
requestId = request.requestId,
recipients = request.recipients,
txnId = createUniqueTxnId(),
lastFailureMessage = null
)
}
}
}
@Inject lateinit var sendToDeviceTask: SendToDeviceTask
@Inject lateinit var cryptoStore: IMXCryptoStore
@Inject lateinit var credentials: Credentials
@Inject lateinit var clock: Clock
override fun injectWith(injector: SessionComponent) {
injector.inject(this)
}
override suspend fun doSafeWork(params: Params): Result {
// params.txnId should be provided in all cases now. But Params can be deserialized by
// the WorkManager from data serialized in a previous version of the application, so without the txnId field.
// So if not present, we create a txnId
val txnId = params.txnId ?: createUniqueTxnId()
val contentMap = MXUsersDevicesMap<Any>()
val toDeviceContent = ShareRequestCancellation(
requestingDeviceId = credentials.deviceId,
requestId = params.requestId
)
cryptoStore.saveGossipingEvent(Event(
type = EventType.ROOM_KEY_REQUEST,
content = toDeviceContent.toContent(),
senderId = credentials.userId
).also {
it.ageLocalTs = clock.epochMillis()
})
params.recipients.forEach { userToDeviceMap ->
userToDeviceMap.value.forEach { deviceId ->
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
}
}
try {
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLING)
sendToDeviceTask.execute(
SendToDeviceTask.Params(
eventType = EventType.ROOM_KEY_REQUEST,
contentMap = contentMap,
transactionId = txnId
)
)
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLED)
return Result.success()
} catch (throwable: Throwable) {
return if (throwable.shouldBeRetried()) {
Result.retry()
} else {
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.FAILED_TO_CANCEL)
buildErrorResult(params, throwable.localizedMessage ?: "error")
}
}
}
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
}

View file

@ -42,12 +42,14 @@ import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
@ -57,15 +59,13 @@ import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
import org.matrix.android.sdk.api.session.crypto.model.TrailType
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
@ -76,7 +76,6 @@ import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption
import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
@ -91,6 +90,7 @@ import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask
import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor
import org.matrix.android.sdk.internal.di.DeviceId
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.di.UserId
@ -156,9 +156,10 @@ internal class DefaultCryptoService @Inject constructor(
private val crossSigningService: DefaultCrossSigningService,
//
private val incomingGossipingRequestManager: IncomingGossipingRequestManager,
private val incomingKeyRequestManager: IncomingKeyRequestManager,
private val secretShareManager: SecretShareManager,
//
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
// Actions
private val setDeviceVerificationAction: SetDeviceVerificationAction,
private val megolmSessionDataImporter: MegolmSessionDataImporter,
@ -178,6 +179,7 @@ internal class DefaultCryptoService @Inject constructor(
private val taskExecutor: TaskExecutor,
private val cryptoCoroutineScope: CoroutineScope,
private val eventDecryptor: EventDecryptor,
private val verificationMessageProcessor: VerificationMessageProcessor,
private val liveEventManager: Lazy<StreamEventsManager>
) : CryptoService {
@ -192,7 +194,7 @@ internal class DefaultCryptoService @Inject constructor(
}
}
fun onLiveEvent(roomId: String, event: Event) {
fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean) {
// handle state events
if (event.isStateEvent()) {
when (event.type) {
@ -201,9 +203,18 @@ internal class DefaultCryptoService @Inject constructor(
EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
}
}
// handle verification
if (!isInitialSync) {
if (event.type != null && verificationMessageProcessor.shouldProcess(event.type)) {
cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) {
verificationMessageProcessor.process(event)
}
}
}
}
val gossipingBuffer = mutableListOf<Event>()
// val gossipingBuffer = mutableListOf<Event>()
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
setDeviceNameTask
@ -379,27 +390,8 @@ internal class DefaultCryptoService @Inject constructor(
// Open the store
cryptoStore.open()
runCatching {
// if (isInitialSync) {
// // refresh the devices list for each known room members
// deviceListManager.invalidateAllDeviceLists()
// deviceListManager.refreshOutdatedDeviceLists()
// } else {
// Why would we do that? it will be called at end of syn
incomingGossipingRequestManager.processReceivedGossipingRequests()
// }
}.fold(
{
isStarting.set(false)
isStarted.set(true)
},
{
isStarting.set(false)
isStarted.set(false)
Timber.tag(loggerTag.value).e(it, "Start failed")
}
)
isStarting.set(false)
isStarted.set(true)
}
/**
@ -407,7 +399,8 @@ internal class DefaultCryptoService @Inject constructor(
*/
fun close() = runBlocking(coroutineDispatchers.crypto) {
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
incomingGossipingRequestManager.close()
incomingKeyRequestManager.close()
outgoingKeyRequestManager.close()
olmDevice.release()
cryptoStore.close()
}
@ -472,15 +465,28 @@ internal class DefaultCryptoService @Inject constructor(
}
oneTimeKeysUploader.maybeUploadOneTimeKeys()
incomingGossipingRequestManager.processReceivedGossipingRequests()
}
}
tryOrNull {
gossipingBuffer.toList().let {
cryptoStore.saveGossipingEvents(it)
// Process pending key requests
try {
if (toDevices.isEmpty()) {
// this is not blocking
outgoingKeyRequestManager.requireProcessAllPendingKeyRequests()
} else {
Timber.tag(loggerTag.value)
.w("Don't process key requests yet as there might be more to_device to catchup")
}
} catch (failure: Throwable) {
// just for safety but should not throw
Timber.tag(loggerTag.value).w("failed to process pending request")
}
try {
incomingKeyRequestManager.processIncomingRequests()
} catch (failure: Throwable) {
// just for safety but should not throw
Timber.tag(loggerTag.value).w("failed to process incoming room key requests")
}
gossipingBuffer.clear()
}
}
}
@ -594,7 +600,7 @@ internal class DefaultCryptoService @Inject constructor(
// (for now at least. Maybe we should alert the user somehow?)
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
if (existingAlgorithm == algorithm && roomEncryptorsStore.get(roomId) != null) {
if (existingAlgorithm == algorithm) {
// ignore
Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in $roomId")
return false
@ -787,19 +793,25 @@ internal class DefaultCryptoService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
when (event.getClearType()) {
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
gossipingBuffer.add(event)
// Keys are imported directly, not waiting for end of sync
onRoomKeyEvent(event)
}
EventType.REQUEST_SECRET,
EventType.REQUEST_SECRET -> {
secretShareManager.handleSecretRequest(event)
}
EventType.ROOM_KEY_REQUEST -> {
// save audit trail
gossipingBuffer.add(event)
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
incomingGossipingRequestManager.onGossipingRequestEvent(event)
event.getClearContent().toModel<RoomKeyShareRequest>()?.let { req ->
// We'll always get these because we send room key requests to
// '*' (ie. 'all devices') which includes the sending device,
// so ignore requests from ourself because apart from it being
// very silly, it won't work because an Olm session cannot send
// messages to itself.
if (req.requestingDeviceId != deviceId) { // ignore self requests
event.senderId?.let { incomingKeyRequestManager.addNewIncomingRequest(it, req) }
}
}
}
EventType.SEND_SECRET -> {
gossipingBuffer.add(event)
onSecretSendReceived(event)
}
EventType.ROOM_KEY_WITHHELD -> {
@ -837,50 +849,38 @@ internal class DefaultCryptoService @Inject constructor(
val withHeldContent = event.getClearContent().toModel<RoomKeyWithHeldContent>() ?: return Unit.also {
Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields")
}
Timber.tag(loggerTag.value).i("onKeyWithHeldReceived() received from:${event.senderId}, content <$withHeldContent>")
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm)
if (alg is IMXWithHeldExtension) {
alg.onRoomKeyWithHeldEvent(withHeldContent)
} else {
Timber.tag(loggerTag.value).e("onKeyWithHeldReceived() from:${event.senderId}: Unable to handle WithHeldContent for ${withHeldContent.algorithm}")
return
val senderId = event.senderId ?: return Unit.also {
Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields")
}
withHeldContent.sessionId ?: return
withHeldContent.algorithm ?: return
withHeldContent.roomId ?: return
withHeldContent.senderKey ?: return
outgoingKeyRequestManager.onRoomKeyWithHeld(
sessionId = withHeldContent.sessionId,
algorithm = withHeldContent.algorithm,
roomId = withHeldContent.roomId,
senderKey = withHeldContent.senderKey,
fromDevice = withHeldContent.fromDevice,
event = Event(
type = EventType.ROOM_KEY_WITHHELD,
senderId = senderId,
content = event.getClearContent()
)
)
}
private fun onSecretSendReceived(event: Event) {
Timber.tag(loggerTag.value).i("GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}")
if (!event.isEncrypted()) {
// secret send messages must be encrypted
Timber.tag(loggerTag.value).e("GOSSIP onSecretSend() :Received unencrypted secret send event")
return
}
// Was that sent by us?
if (event.senderId != userId) {
Timber.tag(loggerTag.value).e("GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}")
return
}
val secretContent = event.getClearContent().toModel<SecretSendEventContent>() ?: return
val existingRequest = cryptoStore
.getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId }
if (existingRequest == null) {
Timber.tag(loggerTag.value).i("GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
return
}
if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) {
// TODO Ask to application layer?
Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK")
private suspend fun onSecretSendReceived(event: Event) {
secretShareManager.onSecretSendReceived(event) { secretName, secretValue ->
handleSDKLevelGossip(secretName, secretValue)
}
}
/**
* Returns true if handled by SDK, otherwise should be sent to application layer
*/
private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean {
private fun handleSDKLevelGossip(secretName: String?,
secretValue: String): Boolean {
return when (secretName) {
MASTER_KEY_SSSS_NAME -> {
crossSigningService.onSecretMSKGossip(secretValue)
@ -1095,6 +1095,12 @@ internal class DefaultCryptoService @Inject constructor(
cryptoStore.setGlobalBlacklistUnverifiedDevices(block)
}
override fun enableKeyGossiping(enable: Boolean) {
cryptoStore.enableKeyGossiping(enable)
}
override fun isKeyGossipingEnabled() = cryptoStore.isKeyGossipingEnabled()
/**
* Tells whether the client should ever send encrypted messages to unverified devices.
* The default value is false.
@ -1158,52 +1164,17 @@ internal class DefaultCryptoService @Inject constructor(
setRoomBlacklistUnverifiedDevices(roomId, false)
}
// TODO Check if this method is still necessary
/**
* Cancel any earlier room key request
*
* @param requestBody requestBody
*/
override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
outgoingGossipingRequestManager.cancelRoomKeyRequest(requestBody)
}
/**
* Re request the encryption keys required to decrypt an event.
*
* @param event the event to decrypt again.
*/
override fun reRequestRoomKeyForEvent(event: Event) {
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
Timber.tag(loggerTag.value).e("reRequestRoomKeyForEvent Failed to re-request key, null content")
}
val requestBody = RoomKeyRequestBody(
algorithm = wireContent.algorithm,
roomId = event.roomId,
senderKey = wireContent.senderKey,
sessionId = wireContent.sessionId
)
outgoingGossipingRequestManager.resendRoomKeyRequest(requestBody)
outgoingKeyRequestManager.requestKeyForEvent(event, true)
}
override fun requestRoomKeyForEvent(event: Event) {
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
Timber.tag(loggerTag.value).e("requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}")
}
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
// if (!isStarted()) {
// Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init")
// internalStart(false)
// }
roomDecryptorProvider
.getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
?.requestKeysForEvent(event, false) ?: run {
Timber.tag(loggerTag.value).v("requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
}
}
outgoingKeyRequestManager.requestKeyForEvent(event, false)
}
/**
@ -1212,7 +1183,8 @@ internal class DefaultCryptoService @Inject constructor(
* @param listener listener
*/
override fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
incomingGossipingRequestManager.addRoomKeysRequestListener(listener)
incomingKeyRequestManager.addRoomKeysRequestListener(listener)
secretShareManager.addListener(listener)
}
/**
@ -1221,42 +1193,10 @@ internal class DefaultCryptoService @Inject constructor(
* @param listener listener
*/
override fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
incomingGossipingRequestManager.removeRoomKeysRequestListener(listener)
incomingKeyRequestManager.removeRoomKeysRequestListener(listener)
secretShareManager.removeListener(listener)
}
// private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
// val deviceKey = deviceInfo.identityKey()
//
// val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
// val now = clock.epochMillis()
// if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
// Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
// return
// }
//
// Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
// lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
//
// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
// ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
//
// // Now send a blank message on that session so the other side knows about it.
// // (The keyshare request is sent in the clear so that won't do)
// // We send this first such that, as long as the toDevice messages arrive in the
// // same order we sent them, the other end will get this first, set up the new session,
// // then get the keyshare request and send the key over this new session (because it
// // is the session it has most recently received a message on).
// val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY)
//
// val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
// val sendToDeviceMap = MXUsersDevicesMap<Any>()
// sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
// Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
// val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
// sendToDeviceTask.execute(sendToDeviceParams)
// }
// }
/**
* Provides the list of unknown devices
*
@ -1302,27 +1242,41 @@ internal class DefaultCryptoService @Inject constructor(
return "DefaultCryptoService of $userId ($deviceId)"
}
override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> {
override fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest> {
return cryptoStore.getOutgoingRoomKeyRequests()
}
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>> {
return cryptoStore.getOutgoingRoomKeyRequestsPaged()
}
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
return cryptoStore.getIncomingRoomKeyRequestsPaged()
}
override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
return cryptoStore.getIncomingRoomKeyRequests()
return cryptoStore.getGossipingEvents()
.mapNotNull {
IncomingRoomKeyRequest.fromEvent(it)
}
}
override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> {
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
return cryptoStore.getGossipingEventsTrail(TrailType.IncomingKeyRequest) {
IncomingRoomKeyRequest.fromEvent(it)
?: IncomingRoomKeyRequest(localCreationTimestamp = 0L)
}
}
/**
* If you registered a `GossipingRequestListener`, you will be notified of key request
* that was not accepted by the SDK. You can call back this manually to accept anyhow.
*/
override suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest) {
incomingKeyRequestManager.manuallyAcceptRoomKeyRequest(request)
}
override fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>> {
return cryptoStore.getGossipingEventsTrail()
}
override fun getGossipingEvents(): List<Event> {
override fun getGossipingEvents(): List<AuditTrail> {
return cryptoStore.getGossipingEvents()
}
@ -1346,8 +1300,8 @@ internal class DefaultCryptoService @Inject constructor(
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members")
callback.onFailure(failure)
return@launch
// we probably shouldn't block sending on that (but questionable)
// but some members won't be able to decrypt
}
val userIds = getRoomUserIds(roomId)

View file

@ -315,10 +315,19 @@ internal class DeviceListManager @Inject constructor(
} else {
Timber.v("## CRYPTO | downloadKeys() : starts")
val t0 = clock.epochMillis()
val result = doKeyDownloadForUsers(downloadUsers)
Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms")
result.also {
it.addEntriesFromMap(stored)
try {
val result = doKeyDownloadForUsers(downloadUsers)
Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${clock.epochMillis() - t0} ms")
result.also {
it.addEntriesFromMap(stored)
}
} catch (failure: Throwable) {
Timber.w(failure, "## CRYPTO | downloadKeys() : doKeyDownloadForUsers failed after ${clock.epochMillis() - t0} ms")
if (forceDownload) {
throw failure
} else {
stored
}
}
}
}

View file

@ -1,58 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import androidx.work.BackoffPolicy
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.ListenableWorker
import androidx.work.OneTimeWorkRequest
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.util.CancelableWork
import org.matrix.android.sdk.internal.worker.startChain
import java.util.UUID
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@SessionScope
internal class GossipingWorkManager @Inject constructor(
private val workManagerProvider: WorkManagerProvider
) {
inline fun <reified W : ListenableWorker> createWork(data: Data, startChain: Boolean): OneTimeWorkRequest {
return workManagerProvider.matrixOneTimeWorkRequestBuilder<W>()
.setConstraints(WorkManagerProvider.workConstraints)
.startChain(startChain)
.setInputData(data)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.build()
}
// Prevent sending queue to stay broken after app restart
// The unique queue id will stay the same as long as this object is instantiated
private val queueSuffixApp = UUID.randomUUID()
fun postWork(workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable {
workManagerProvider.workManager
.beginUniqueWork(this::class.java.name + "_$queueSuffixApp", policy, workRequest)
.enqueue()
return CancelableWork(workManagerProvider.workManager, workRequest.id)
}
}

View file

@ -1,475 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
import org.matrix.android.sdk.api.session.crypto.model.IncomingRequestCancellation
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.util.toBase64NoPadding
import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption
import org.matrix.android.sdk.internal.crypto.model.rest.GossipingDefaultContent
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.util.time.Clock
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import timber.log.Timber
import java.util.concurrent.Executors
import javax.inject.Inject
@SessionScope
internal class IncomingGossipingRequestManager @Inject constructor(
@SessionId private val sessionId: String,
private val credentials: Credentials,
private val cryptoStore: IMXCryptoStore,
private val cryptoConfig: MXCryptoConfig,
private val gossipingWorkManager: GossipingWorkManager,
private val roomEncryptorsStore: RoomEncryptorsStore,
private val roomDecryptorProvider: RoomDecryptorProvider,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope,
private val clock: Clock,
) {
private val executor = Executors.newSingleThreadExecutor()
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
// we received in the current sync.
private val receivedGossipingRequests = ArrayList<IncomingShareRequestCommon>()
private val receivedRequestCancellations = ArrayList<IncomingRequestCancellation>()
// the listeners
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet()
init {
receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
}
fun close() {
executor.shutdownNow()
}
// Recently verified devices (map of deviceId and timestamp)
private val recentlyVerifiedDevices = HashMap<String, Long>()
/**
* Called when a session has been verified.
* This information can be used by the manager to decide whether or not to fullfil gossiping requests
*/
fun onVerificationCompleteForDevice(deviceId: String) {
// For now we just keep an in memory cache
synchronized(recentlyVerifiedDevices) {
recentlyVerifiedDevices[deviceId] = clock.epochMillis()
}
}
private fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean {
val verifTimestamp: Long?
synchronized(recentlyVerifiedDevices) {
verifTimestamp = recentlyVerifiedDevices[deviceId]
}
if (verifTimestamp == null) return false
val age = clock.epochMillis() - verifTimestamp
return age < FIVE_MINUTES_IN_MILLIS
}
/**
* Called when we get an m.room_key_request event
* It must be called on CryptoThread
*
* @param event the announcement event.
*/
fun onGossipingRequestEvent(event: Event) {
val roomKeyShare = event.getClearContent().toModel<GossipingDefaultContent>()
Timber.i("## CRYPTO | GOSSIP onGossipingRequestEvent received type ${event.type} from user:${event.senderId}, content:$roomKeyShare")
// val ageLocalTs = event.unsignedData?.age?.let { clock.epochMillis() - it }
when (roomKeyShare?.action) {
GossipingToDeviceObject.ACTION_SHARE_REQUEST -> {
if (event.getClearType() == EventType.REQUEST_SECRET) {
IncomingSecretShareRequest.fromEvent(event, clock.epochMillis())?.let {
if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) {
// ignore, it was sent by me as *
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
} else {
// // save in DB
// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
receivedGossipingRequests.add(it)
}
}
} else if (event.getClearType() == EventType.ROOM_KEY_REQUEST) {
IncomingRoomKeyRequest.fromEvent(event, clock.epochMillis())?.let {
if (event.senderId == credentials.userId && it.deviceId == credentials.deviceId) {
// ignore, it was sent by me as *
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
} else {
// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
receivedGossipingRequests.add(it)
}
}
}
}
GossipingToDeviceObject.ACTION_SHARE_CANCELLATION -> {
IncomingRequestCancellation.fromEvent(event, clock.epochMillis())?.let {
receivedRequestCancellations.add(it)
}
}
else -> {
Timber.e("## GOSSIP onGossipingRequestEvent() : unsupported action ${roomKeyShare?.action}")
}
}
}
/**
* Process any m.room_key_request or m.secret.request events which were queued up during the
* current sync.
* It must be called on CryptoThread
*/
fun processReceivedGossipingRequests() {
val roomKeyRequestsToProcess = receivedGossipingRequests.toList()
receivedGossipingRequests.clear()
Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : ${roomKeyRequestsToProcess.size} request to process")
var receivedRequestCancellations: List<IncomingRequestCancellation>? = null
synchronized(this.receivedRequestCancellations) {
if (this.receivedRequestCancellations.isNotEmpty()) {
receivedRequestCancellations = this.receivedRequestCancellations.toList()
this.receivedRequestCancellations.clear()
}
}
executor.execute {
cryptoStore.storeIncomingGossipingRequests(roomKeyRequestsToProcess)
for (request in roomKeyRequestsToProcess) {
if (request is IncomingRoomKeyRequest) {
processIncomingRoomKeyRequest(request)
} else if (request is IncomingSecretShareRequest) {
processIncomingSecretShareRequest(request)
}
}
receivedRequestCancellations?.forEach { request ->
Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
// we should probably only notify the app of cancellations we told it
// about, but we don't currently have a record of that, so we just pass
// everything through.
if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) {
// ignore remote echo
return@forEach
}
val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "")
if (matchingIncoming == null) {
// ignore that?
return@forEach
} else {
// If it was accepted from this device, keep the information, do not mark as cancelled
if (matchingIncoming.state != GossipingRequestState.ACCEPTED) {
onRoomKeyRequestCancellation(request)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER)
}
}
}
}
}
private fun processIncomingRoomKeyRequest(request: IncomingRoomKeyRequest) {
val userId = request.userId ?: return
val deviceId = request.deviceId ?: return
val body = request.requestBody ?: return
val roomId = body.roomId ?: return
val alg = body.algorithm ?: return
Timber.v("## CRYPTO | GOSSIP processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
if (credentials.userId != userId) {
handleKeyRequestFromOtherUser(body, request, alg, roomId, userId, deviceId)
return
}
// TODO: should we queue up requests we don't yet have keys for, in case they turn up later?
// if we don't have a decryptor for this room/alg, we don't have
// the keys for the requested events, and can drop the requests.
val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg)
if (null == decryptor) {
Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
if (!decryptor.hasKeysForKeyRequest(request)) {
Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
if (credentials.deviceId == deviceId && credentials.userId == userId) {
Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : oneself device - ignored")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
request.share = Runnable {
decryptor.shareKeysWithDevice(request)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
}
request.ignore = Runnable {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
}
// if the device is verified already, share the keys
val device = cryptoStore.getUserDevice(userId, deviceId)
if (device != null) {
if (device.isVerified) {
Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : device is already verified: sharing keys")
request.share?.run()
return
}
if (device.isBlocked) {
Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : device is blocked -> ignored")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
}
// As per config we automatically discard untrusted devices request
if (cryptoConfig.discardRoomKeyRequestsFromUntrustedDevices) {
Timber.v("## CRYPTO | processReceivedGossipingRequests() : discardRoomKeyRequestsFromUntrustedDevices")
// At this point the device is unknown, we don't want to bother user with that
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
// Pass to application layer to decide what to do
onRoomKeyRequest(request)
}
private fun handleKeyRequestFromOtherUser(body: RoomKeyRequestBody,
request: IncomingRoomKeyRequest,
alg: String,
roomId: String,
userId: String,
deviceId: String) {
Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request from other user")
val senderKey = body.senderKey ?: return Unit
.also { Timber.w("missing senderKey") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
val sessionId = body.sessionId ?: return Unit
.also { Timber.w("missing sessionId") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
if (alg != MXCRYPTO_ALGORITHM_MEGOLM) {
return Unit
.also { Timber.w("Only megolm is accepted here") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
}
val roomEncryptor = roomEncryptorsStore.get(roomId) ?: return Unit
.also { Timber.w("no room Encryptor") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
if (roomEncryptor is IMXGroupEncryption) {
val isSuccess = roomEncryptor.reshareKey(sessionId, userId, deviceId, senderKey)
if (isSuccess) {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
} else {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.UNABLE_TO_PROCESS)
}
} else {
Timber.e("## CRYPTO | handleKeyRequestFromOtherUser() from:$userId: Unable to handle IMXGroupEncryption.reshareKey for $alg")
}
}
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.RE_REQUESTED)
}
private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) {
val secretName = request.secretName ?: return Unit.also {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Missing secret name")
}
val userId = request.userId
if (userId == null || credentials.userId != userId) {
Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from other users")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
val deviceId = request.deviceId
?: return Unit.also {
Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Malformed request, no ")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
}
val device = cryptoStore.getUserDevice(userId, deviceId)
?: return Unit.also {
Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
}
if (!device.isVerified || device.isBlocked) {
Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return
}
val isDeviceLocallyVerified = cryptoStore.getUserDevice(userId, deviceId)?.trustLevel?.isLocallyVerified()
when (secretName) {
MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master
SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned
USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user
KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey
?.let {
extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding()
}
else -> null
}?.let { secretValue ->
Timber.i("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted")
if (isDeviceLocallyVerified == true && hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId)) {
val params = SendGossipWorker.Params(
sessionId = sessionId,
secretValue = secretValue,
requestUserId = request.userId,
requestDeviceId = request.deviceId,
requestId = request.requestId,
txnId = createUniqueTxnId()
)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING)
val workRequest = gossipingWorkManager.createWork<SendGossipWorker>(WorkerParamsFactory.toData(params), true)
gossipingWorkManager.postWork(workRequest)
} else {
Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Can't share secret $secretName with $device, verification too old")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
}
return
}
Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : $secretName unknown at SDK level, asking to app layer")
request.ignore = Runnable {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
}
request.share = { secretValue ->
val params = SendGossipWorker.Params(
sessionId = userId,
secretValue = secretValue,
requestUserId = request.userId,
requestDeviceId = request.deviceId,
requestId = request.requestId,
txnId = createUniqueTxnId()
)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTING)
val workRequest = gossipingWorkManager.createWork<SendGossipWorker>(WorkerParamsFactory.toData(params), true)
gossipingWorkManager.postWork(workRequest)
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
}
onShareRequest(request)
}
/**
* Dispatch onRoomKeyRequest
*
* @param request the request
*/
private fun onRoomKeyRequest(request: IncomingRoomKeyRequest) {
synchronized(gossipingRequestListeners) {
for (listener in gossipingRequestListeners) {
try {
listener.onRoomKeyRequest(request)
} catch (e: Exception) {
Timber.e(e, "## CRYPTO | onRoomKeyRequest() failed")
}
}
}
}
/**
* Ask for a value to the listeners, and take the first one
*/
private fun onShareRequest(request: IncomingSecretShareRequest) {
synchronized(gossipingRequestListeners) {
for (listener in gossipingRequestListeners) {
try {
if (listener.onSecretShareRequest(request)) {
return
}
} catch (e: Exception) {
Timber.e(e, "## CRYPTO | GOSSIP onRoomKeyRequest() failed")
}
}
}
// Not handled, ignore
request.ignore?.run()
}
/**
* A room key request cancellation has been received.
*
* @param request the cancellation request
*/
private fun onRoomKeyRequestCancellation(request: IncomingRequestCancellation) {
synchronized(gossipingRequestListeners) {
for (listener in gossipingRequestListeners) {
try {
listener.onRoomKeyRequestCancellation(request)
} catch (e: Exception) {
Timber.e(e, "## CRYPTO | GOSSIP onRoomKeyRequestCancellation() failed")
}
}
}
}
fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
synchronized(gossipingRequestListeners) {
gossipingRequestListeners.add(listener)
}
}
fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
synchronized(gossipingRequestListeners) {
gossipingRequestListeners.remove(listener)
}
}
companion object {
private const val FIVE_MINUTES_IN_MILLIS = 5 * 60 * 1000
}
}

View file

@ -0,0 +1,463 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
import java.util.concurrent.Executors
import javax.inject.Inject
import kotlin.system.measureTimeMillis
private val loggerTag = LoggerTag("IncomingKeyRequestManager", LoggerTag.CRYPTO)
@SessionScope
internal class IncomingKeyRequestManager @Inject constructor(
private val credentials: Credentials,
private val cryptoStore: IMXCryptoStore,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val olmDevice: MXOlmDevice,
private val cryptoConfig: MXCryptoConfig,
private val messageEncrypter: MessageEncrypter,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sendToDeviceTask: SendToDeviceTask,
private val clock: Clock,
) {
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher)
val sequencer = SemaphoreCoroutineSequencer()
private val incomingRequestBuffer = mutableListOf<ValidMegolmRequestBody>()
// the listeners
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet()
enum class MegolmRequestAction {
Request, Cancel
}
data class ValidMegolmRequestBody(
val requestId: String,
val requestingUserId: String,
val requestingDeviceId: String,
val roomId: String,
val senderKey: String,
val sessionId: String,
val action: MegolmRequestAction
) {
fun shortDbgString() = "Request from $requestingUserId|$requestingDeviceId for session $sessionId in room $roomId"
}
private fun RoomKeyShareRequest.toValidMegolmRequest(senderId: String): ValidMegolmRequestBody? {
val deviceId = requestingDeviceId ?: return null
val body = body ?: return null
val roomId = body.roomId ?: return null
val sessionId = body.sessionId ?: return null
val senderKey = body.senderKey ?: return null
val requestId = this.requestId ?: return null
if (body.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null
val action = when (this.action) {
"request" -> MegolmRequestAction.Request
"request_cancellation" -> MegolmRequestAction.Cancel
else -> null
} ?: return null
return ValidMegolmRequestBody(
requestId = requestId,
requestingUserId = senderId,
requestingDeviceId = deviceId,
roomId = roomId,
senderKey = senderKey,
sessionId = sessionId,
action = action
)
}
fun addNewIncomingRequest(senderId: String, request: RoomKeyShareRequest) {
if (!cryptoStore.isKeyGossipingEnabled()) {
Timber.tag(loggerTag.value)
.i("Ignore incoming key request as per crypto config in room ${request.body?.roomId}")
return
}
outgoingRequestScope.launch {
// It is important to handle requests in order
sequencer.post {
val validMegolmRequest = request.toValidMegolmRequest(senderId) ?: return@post Unit.also {
Timber.tag(loggerTag.value).w("Received key request for unknown algorithm ${request.body?.algorithm}")
}
// is there already one like that?
val existing = incomingRequestBuffer.firstOrNull { it == validMegolmRequest }
if (existing == null) {
when (validMegolmRequest.action) {
MegolmRequestAction.Request -> {
// just add to the buffer
incomingRequestBuffer.add(validMegolmRequest)
}
MegolmRequestAction.Cancel -> {
// ignore, we can't cancel as it's not known (probably already processed)
// still notify app layer if it was passed up previously
IncomingRoomKeyRequest.fromRestRequest(senderId, request, clock)?.let { iReq ->
outgoingRequestScope.launch(coroutineDispatchers.computation) {
val listenersCopy = synchronized(gossipingRequestListeners) {
gossipingRequestListeners.toList()
}
listenersCopy.onEach {
tryOrNull {
withContext(coroutineDispatchers.main) {
it.onRequestCancelled(iReq)
}
}
}
}
}
}
}
} else {
when (validMegolmRequest.action) {
MegolmRequestAction.Request -> {
// it's already in buffer, nop keep existing
}
MegolmRequestAction.Cancel -> {
// discard the request in buffer
incomingRequestBuffer.remove(existing)
outgoingRequestScope.launch(coroutineDispatchers.computation) {
val listenersCopy = synchronized(gossipingRequestListeners) {
gossipingRequestListeners.toList()
}
listenersCopy.onEach {
IncomingRoomKeyRequest.fromRestRequest(senderId, request, clock)?.let { iReq ->
withContext(coroutineDispatchers.main) {
tryOrNull { it.onRequestCancelled(iReq) }
}
}
}
}
}
}
}
}
}
}
fun processIncomingRequests() {
outgoingRequestScope.launch {
sequencer.post {
measureTimeMillis {
Timber.tag(loggerTag.value).v("processIncomingKeyRequests : ${incomingRequestBuffer.size} request to process")
incomingRequestBuffer.forEach {
// should not happen, we only store requests
if (it.action != MegolmRequestAction.Request) return@forEach
try {
handleIncomingRequest(it)
} catch (failure: Throwable) {
// ignore and continue, should not happen
Timber.tag(loggerTag.value).w(failure, "processIncomingKeyRequests : failed to process request $it")
}
}
incomingRequestBuffer.clear()
}.let { duration ->
Timber.tag(loggerTag.value).v("Finish processing incoming key request in $duration ms")
}
}
}
}
private suspend fun handleIncomingRequest(request: ValidMegolmRequestBody) {
// We don't want to download keys, if we don't know the device yet we won't share any how?
val requestingDevice =
cryptoStore.getUserDevice(request.requestingUserId, request.requestingDeviceId)
?: return Unit.also {
Timber.tag(loggerTag.value).d("Ignoring key request: ${request.shortDbgString()}")
}
cryptoStore.saveIncomingKeyRequestAuditTrail(
request.requestId,
request.roomId,
request.sessionId,
request.senderKey,
MXCRYPTO_ALGORITHM_MEGOLM,
request.requestingUserId,
request.requestingDeviceId
)
val roomAlgorithm = // withContext(coroutineDispatchers.crypto) {
cryptoStore.getRoomAlgorithm(request.roomId)
// }
if (roomAlgorithm != MXCRYPTO_ALGORITHM_MEGOLM) {
// strange we received a request for a room that is not encrypted
// maybe a broken state?
Timber.tag(loggerTag.value).w("Received a key request in a room with unsupported alg:$roomAlgorithm , req:${request.shortDbgString()}")
return
}
// Is it for one of our sessions?
if (request.requestingUserId == credentials.userId) {
Timber.tag(loggerTag.value).v("handling request from own user: megolm session ${request.sessionId}")
if (request.requestingDeviceId == credentials.deviceId) {
// ignore it's a remote echo
return
}
// If it's verified we share from the early index we know
// if not we check if it was originaly shared or not
if (requestingDevice.isVerified) {
// we share from the earliest known chain index
shareMegolmKey(request, requestingDevice, null)
} else {
shareIfItWasPreviouslyShared(request, requestingDevice)
}
} else {
if (cryptoConfig.limitRoomKeyRequestsToMyDevices) {
Timber.tag(loggerTag.value).v("Ignore request from other user as per crypto config: ${request.shortDbgString()}")
return
}
Timber.tag(loggerTag.value).v("handling request from other user: megolm session ${request.sessionId}")
if (requestingDevice.isBlocked) {
// it's blocked, so send a withheld code
sendWithheldForRequest(request, WithHeldCode.BLACKLISTED)
} else {
shareIfItWasPreviouslyShared(request, requestingDevice)
}
}
}
private suspend fun shareIfItWasPreviouslyShared(request: ValidMegolmRequestBody, requestingDevice: CryptoDeviceInfo) {
// we don't reshare unless it was previously shared with
val wasSessionSharedWithUser = withContext(coroutineDispatchers.crypto) {
cryptoStore.getSharedSessionInfo(request.roomId, request.sessionId, requestingDevice)
}
if (wasSessionSharedWithUser.found && wasSessionSharedWithUser.chainIndex != null) {
// we share from the index it was previously shared with
shareMegolmKey(request, requestingDevice, wasSessionSharedWithUser.chainIndex.toLong())
} else {
val isOwnDevice = requestingDevice.userId == credentials.userId
sendWithheldForRequest(request, if (isOwnDevice) WithHeldCode.UNVERIFIED else WithHeldCode.UNAUTHORISED)
// if it's our device we could delegate to the app layer to decide
if (isOwnDevice) {
outgoingRequestScope.launch(coroutineDispatchers.computation) {
val listenersCopy = synchronized(gossipingRequestListeners) {
gossipingRequestListeners.toList()
}
val iReq = IncomingRoomKeyRequest(
userId = requestingDevice.userId,
deviceId = requestingDevice.deviceId,
requestId = request.requestId,
requestBody = RoomKeyRequestBody(
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
senderKey = request.senderKey,
sessionId = request.sessionId,
roomId = request.roomId
),
localCreationTimestamp = clock.epochMillis()
)
listenersCopy.onEach {
withContext(coroutineDispatchers.main) {
tryOrNull { it.onRoomKeyRequest(iReq) }
}
}
}
}
}
}
private suspend fun sendWithheldForRequest(request: ValidMegolmRequestBody, code: WithHeldCode) {
Timber.tag(loggerTag.value)
.w("Send withheld $code for req: ${request.shortDbgString()}")
val withHeldContent = RoomKeyWithHeldContent(
roomId = request.roomId,
senderKey = request.senderKey,
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
sessionId = request.sessionId,
codeString = code.value,
fromDevice = credentials.deviceId
)
val params = SendToDeviceTask.Params(
EventType.ROOM_KEY_WITHHELD,
MXUsersDevicesMap<Any>().apply {
setObject(request.requestingUserId, request.requestingDeviceId, withHeldContent)
}
)
try {
withContext(coroutineDispatchers.io) {
sendToDeviceTask.execute(params)
Timber.tag(loggerTag.value)
.d("Send withheld $code req: ${request.shortDbgString()}")
}
cryptoStore.saveWithheldAuditTrail(
roomId = request.roomId,
sessionId = request.sessionId,
senderKey = request.senderKey,
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
code = code,
userId = request.requestingUserId,
deviceId = request.requestingDeviceId
)
} catch (failure: Throwable) {
// Ignore it's not that important?
// do we want to fallback to a worker?
Timber.tag(loggerTag.value)
.w("Failed to send withheld $code req: ${request.shortDbgString()} reason:${failure.localizedMessage}")
}
}
suspend fun manuallyAcceptRoomKeyRequest(request: IncomingRoomKeyRequest) {
request.requestId ?: return
request.deviceId ?: return
request.userId ?: return
request.requestBody?.roomId ?: return
request.requestBody.senderKey ?: return
request.requestBody.sessionId ?: return
val validReq = ValidMegolmRequestBody(
requestId = request.requestId,
requestingDeviceId = request.deviceId,
requestingUserId = request.userId,
roomId = request.requestBody.roomId,
senderKey = request.requestBody.senderKey,
sessionId = request.requestBody.sessionId,
action = MegolmRequestAction.Request
)
val requestingDevice =
cryptoStore.getUserDevice(request.userId, request.deviceId)
?: return Unit.also {
Timber.tag(loggerTag.value).d("Ignoring key request: ${validReq.shortDbgString()}")
}
shareMegolmKey(validReq, requestingDevice, null)
}
private suspend fun shareMegolmKey(validRequest: ValidMegolmRequestBody,
requestingDevice: CryptoDeviceInfo,
chainIndex: Long?): Boolean {
Timber.tag(loggerTag.value)
.d("try to re-share Megolm Key at index $chainIndex for ${validRequest.shortDbgString()}")
val devicesByUser = mapOf(validRequest.requestingUserId to listOf(requestingDevice))
val usersDeviceMap = try {
ensureOlmSessionsForDevicesAction.handle(devicesByUser)
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.w("Failed to establish olm session")
sendWithheldForRequest(validRequest, WithHeldCode.NO_OLM)
return false
}
val olmSessionResult = usersDeviceMap.getObject(requestingDevice.userId, requestingDevice.deviceId)
if (olmSessionResult?.sessionId == null) {
Timber.tag(loggerTag.value)
.w("reshareKey: no session with this device, probably because there were no one-time keys")
sendWithheldForRequest(validRequest, WithHeldCode.NO_OLM)
return false
}
val sessionHolder = try {
olmDevice.getInboundGroupSession(validRequest.sessionId, validRequest.senderKey, validRequest.roomId)
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.e(failure, "shareKeysWithDevice: failed to get session ${validRequest.requestingUserId}")
// It's unavailable
sendWithheldForRequest(validRequest, WithHeldCode.UNAVAILABLE)
return false
}
val export = sessionHolder.mutex.withLock {
sessionHolder.wrapper.exportKeys(chainIndex)
} ?: return false.also {
Timber.tag(loggerTag.value)
.e("shareKeysWithDevice: failed to export group session ${validRequest.sessionId}")
}
val payloadJson = mapOf(
"type" to EventType.FORWARDED_ROOM_KEY,
"content" to export
)
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(requestingDevice))
val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(requestingDevice.userId, requestingDevice.deviceId, encodedPayload)
Timber.tag(loggerTag.value).d("reshareKey() : try sending session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
return try {
sendToDeviceTask.execute(sendToDeviceParams)
Timber.tag(loggerTag.value)
.i("successfully re-shared session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}")
cryptoStore.saveForwardKeyAuditTrail(
validRequest.roomId,
validRequest.sessionId,
validRequest.senderKey,
MXCRYPTO_ALGORITHM_MEGOLM,
requestingDevice.userId,
requestingDevice.deviceId,
chainIndex
)
true
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.e(failure, "fail to re-share session ${validRequest.sessionId} to ${requestingDevice.shortDebugString()}")
false
}
}
fun addRoomKeysRequestListener(listener: GossipingRequestListener) {
synchronized(gossipingRequestListeners) {
gossipingRequestListeners.add(listener)
}
}
fun removeRoomKeysRequestListener(listener: GossipingRequestListener) {
synchronized(gossipingRequestListeners) {
gossipingRequestListeners.remove(listener)
}
}
fun close() {
try {
outgoingRequestScope.cancel("User Terminate")
incomingRequestBuffer.clear()
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).w("Failed to shutDown request manager")
}
}
}

View file

@ -1,36 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
internal interface IncomingShareRequestCommon {
/**
* The user id
*/
val userId: String?
/**
* The device id
*/
val deviceId: String?
/**
* The request id
*/
val requestId: String?
val localCreationTimestamp: Long?
}

View file

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

View file

@ -1,27 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
internal interface OutgoingGossipingRequest {
val recipients: Map<String, List<String>>
val requestId: String
val state: OutgoingGossipingRequestState
// transaction id for the cancellation, if any
// var cancellationTxnId: String?
}

View file

@ -1,167 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import timber.log.Timber
import javax.inject.Inject
@SessionScope
internal class OutgoingGossipingRequestManager @Inject constructor(
@SessionId private val sessionId: String,
private val cryptoStore: IMXCryptoStore,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope,
private val gossipingWorkManager: GossipingWorkManager) {
/**
* Send off a room key request, if we haven't already done so.
*
*
* The `requestBody` is compared (with a deep-equality check) against
* previous queued or sent requests and if it matches, no change is made.
* Otherwise, a request is added to the pending list, and a job is started
* in the background to send it.
*
* @param requestBody requestBody
* @param recipients recipients
*/
fun sendRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients)?.let {
// Don't resend if it's already done, you need to cancel first (reRequest)
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
Timber.v("## CRYPTO - GOSSIP sendOutgoingRoomKeyRequest() : we already request for that session: $it")
return@launch
}
sendOutgoingGossipingRequest(it)
}
}
}
fun sendSecretShareRequest(secretName: String, recipients: Map<String, List<String>>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
// A bit dirty, but for better stability give other party some time to mark
// devices trusted :/
delay(1500)
cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let {
// TODO check if there is already one that is being sent?
if (it.state == OutgoingGossipingRequestState.SENDING
/**|| it.state == OutgoingGossipingRequestState.SENT*/
) {
Timber.v("## CRYPTO - GOSSIP sendSecretShareRequest() : we are already sending for that session: $it")
return@launch
}
sendOutgoingGossipingRequest(it)
}
}
}
/**
* Cancel room key requests, if any match the given details
*
* @param requestBody requestBody
*/
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
cancelRoomKeyRequest(requestBody, false)
}
}
/**
* Cancel room key requests, if any match the given details, and resend
*
* @param requestBody requestBody
*/
fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) {
cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
cancelRoomKeyRequest(requestBody, true)
}
}
/**
* Cancel room key requests, if any match the given details, and resend
*
* @param requestBody requestBody
* @param andResend true to resend the key request
*/
private fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody, andResend: Boolean) {
val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody) // no request was made for this key
?: return Unit.also {
Timber.v("## CRYPTO - GOSSIP cancelRoomKeyRequest() Unknown request $requestBody")
}
sendOutgoingRoomKeyRequestCancellation(req, andResend)
}
/**
* Send the outgoing key request.
*
* @param request the request
*/
private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) {
Timber.v("## CRYPTO - GOSSIP sendOutgoingGossipingRequest() : Requesting keys $request")
val params = SendGossipRequestWorker.Params(
sessionId = sessionId,
keyShareRequest = request as? OutgoingRoomKeyRequest,
secretShareRequest = request as? OutgoingSecretRequest,
txnId = createUniqueTxnId()
)
cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.SENDING)
val workRequest = gossipingWorkManager.createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
gossipingWorkManager.postWork(workRequest)
}
/**
* Given a OutgoingRoomKeyRequest, cancel it and delete the request record
*
* @param request the request
*/
private fun sendOutgoingRoomKeyRequestCancellation(request: OutgoingRoomKeyRequest, resend: Boolean = false) {
Timber.v("## CRYPTO - sendOutgoingRoomKeyRequestCancellation $request")
val params = CancelGossipRequestWorker.Params.fromRequest(sessionId, request)
cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.CANCELLING)
val workRequest = gossipingWorkManager.createWork<CancelGossipRequestWorker>(WorkerParamsFactory.toData(params), true)
gossipingWorkManager.postWork(workRequest)
if (resend) {
val reSendParams = SendGossipRequestWorker.Params(
sessionId = sessionId,
keyShareRequest = request.copy(requestId = RequestIdHelper.createUniqueRequestId()),
txnId = createUniqueTxnId()
)
val reSendWorkRequest = gossipingWorkManager.createWork<SendGossipRequestWorker>(WorkerParamsFactory.toData(reSendParams), true)
gossipingWorkManager.postWork(reSendWorkRequest)
}
}
}

View file

@ -0,0 +1,518 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.util.fromBase64
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
import timber.log.Timber
import java.util.Stack
import java.util.concurrent.Executors
import javax.inject.Inject
import kotlin.system.measureTimeMillis
private val loggerTag = LoggerTag("OutgoingKeyRequestManager", LoggerTag.CRYPTO)
/**
* This class is responsible for sending key requests to other devices when a message failed to decrypt.
* It's lifecycle is based on the sync pulse:
* - You can post queries for session, or report when you got a session
* - At the end of the sync (onSyncComplete) it will then process all the posted request and send to devices
* If a request failed it will be retried at the end of the next sync
*/
@SessionScope
internal class OutgoingKeyRequestManager @Inject constructor(
@SessionId private val sessionId: String,
@UserId private val myUserId: String,
private val cryptoStore: IMXCryptoStore,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoConfig: MXCryptoConfig,
private val inboundGroupSessionStore: InboundGroupSessionStore,
private val sendToDeviceTask: SendToDeviceTask,
private val deviceListManager: DeviceListManager,
private val perSessionBackupQueryRateLimiter: PerSessionBackupQueryRateLimiter) {
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val outgoingRequestScope = CoroutineScope(SupervisorJob() + dispatcher)
private val sequencer = SemaphoreCoroutineSequencer()
// We only have one active key request per session, so we don't request if it's already requested
// But it could make sense to check more the backup, as it's evolving.
// We keep a stack as we consider that the key requested last is more likely to be on screen?
private val requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup = Stack<Pair<String, String>>()
fun requestKeyForEvent(event: Event, force: Boolean) {
val (targets, body) = getRoomKeyRequestTargetForEvent(event) ?: return
val index = ratchetIndexForMessage(event) ?: 0
postRoomKeyRequest(body, targets, index, force)
}
private fun getRoomKeyRequestTargetForEvent(event: Event): Pair<Map<String, List<String>>, RoomKeyRequestBody>? {
val sender = event.senderId ?: return null
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return null.also {
Timber.tag(loggerTag.value).e("getRoomKeyRequestTargetForEvent Failed to re-request key, null content")
}
if (encryptedEventContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null
val senderDevice = encryptedEventContent.deviceId
val recipients = if (cryptoConfig.limitRoomKeyRequestsToMyDevices) {
mapOf(
myUserId to listOf("*")
)
} else {
if (event.senderId == myUserId) {
mapOf(
myUserId to listOf("*")
)
} else {
// for the case where you share the key with a device that has a broken olm session
// The other user might Re-shares a megolm session key with devices if the key has already been
// sent to them.
mapOf(
myUserId to listOf("*"),
// We might not have deviceId in the future due to https://github.com/matrix-org/matrix-spec-proposals/pull/3700
// so in this case query to all
sender to listOf(senderDevice ?: "*")
)
}
}
val requestBody = RoomKeyRequestBody(
roomId = event.roomId,
algorithm = encryptedEventContent.algorithm,
senderKey = encryptedEventContent.senderKey,
sessionId = encryptedEventContent.sessionId
)
return recipients to requestBody
}
private fun ratchetIndexForMessage(event: Event): Int? {
val encryptedContent = event.content.toModel<EncryptedEventContent>() ?: return null
if (encryptedContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return null
return encryptedContent.ciphertext?.fromBase64()?.inputStream()?.reader()?.let {
tryOrNull {
val megolmVersion = it.read()
if (megolmVersion != 3) return@tryOrNull null
/** Int tag */
if (it.read() != 8) return@tryOrNull null
it.read()
}
}
}
fun postRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>, fromIndex: Int, force: Boolean = false) {
outgoingRequestScope.launch {
sequencer.post {
internalQueueRequest(requestBody, recipients, fromIndex, force)
}
}
}
/**
* Typically called when we the session as been imported or received meanwhile
*/
fun postCancelRequestForSessionIfNeeded(sessionId: String, roomId: String, senderKey: String, fromIndex: Int) {
outgoingRequestScope.launch {
sequencer.post {
internalQueueCancelRequest(sessionId, roomId, senderKey, fromIndex)
}
}
}
fun onSelfCrossSigningTrustChanged(newTrust: Boolean) {
if (newTrust) {
// we were previously not cross signed, but we are now
// so there is now more chances to get better replies for existing request
// Let's forget about sent request so that next time we try to decrypt we will resend requests
// We don't resend all because we don't want to generate a bulk of traffic
outgoingRequestScope.launch {
sequencer.post {
cryptoStore.deleteOutgoingRoomKeyRequestInState(OutgoingRoomKeyRequestState.SENT)
}
sequencer.post {
delay(1000)
perSessionBackupQueryRateLimiter.refreshBackupInfoIfNeeded(true)
}
}
}
}
fun onRoomKeyForwarded(sessionId: String,
algorithm: String,
roomId: String,
senderKey: String,
fromDevice: String?,
fromIndex: Int,
event: Event) {
Timber.tag(loggerTag.value).d("Key forwarded for $sessionId from ${event.senderId}|$fromDevice at index $fromIndex")
outgoingRequestScope.launch {
sequencer.post {
cryptoStore.updateOutgoingRoomKeyReply(
roomId = roomId,
sessionId = sessionId,
algorithm = algorithm,
senderKey = senderKey,
fromDevice = fromDevice,
// strip out encrypted stuff as it's just a trail?
event = event.copy(
type = event.getClearType(),
content = mapOf(
"chain_index" to fromIndex
)
)
)
}
}
}
fun onRoomKeyWithHeld(sessionId: String,
algorithm: String,
roomId: String,
senderKey: String,
fromDevice: String?,
event: Event) {
outgoingRequestScope.launch {
sequencer.post {
Timber.tag(loggerTag.value).d("Withheld received for $sessionId from ${event.senderId}|$fromDevice")
Timber.tag(loggerTag.value).v("Withheld content ${event.getClearContent()}")
// We want to store withheld code from the sender of the message (owner of the megolm session), not from
// other devices that might gossip the key. If not the initial reason might be overridden
// by a request to one of our session.
event.getClearContent().toModel<RoomKeyWithHeldContent>()?.let { withheld ->
withContext(coroutineDispatchers.crypto) {
tryOrNull {
deviceListManager.downloadKeys(listOf(event.senderId ?: ""), false)
}
cryptoStore.getUserDeviceList(event.senderId ?: "")
.also { devices ->
Timber.tag(loggerTag.value)
.v("Withheld Devices for ${event.senderId} are ${devices.orEmpty().joinToString { it.identityKey() ?: "" }}")
}
?.firstOrNull {
it.identityKey() == senderKey
}
}.also {
Timber.tag(loggerTag.value).v("Withheld device for sender key $senderKey is from ${it?.shortDebugString()}")
}?.let {
if (it.userId == event.senderId) {
if (fromDevice != null) {
if (it.deviceId == fromDevice) {
Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}")
cryptoStore.addWithHeldMegolmSession(withheld)
}
} else {
Timber.tag(loggerTag.value).v("Storing sender Withheld code ${withheld.code} for ${withheld.sessionId}")
cryptoStore.addWithHeldMegolmSession(withheld)
}
}
}
}
// Here we store the replies from a given request
cryptoStore.updateOutgoingRoomKeyReply(
roomId = roomId,
sessionId = sessionId,
algorithm = algorithm,
senderKey = senderKey,
fromDevice = fromDevice,
event = event
)
}
}
}
/**
* Should be called after a sync, ideally if no catchup sync needed (as keys might arrive in those)
*/
fun requireProcessAllPendingKeyRequests() {
outgoingRequestScope.launch {
sequencer.post {
internalProcessPendingKeyRequests()
}
}
}
private fun internalQueueCancelRequest(sessionId: String, roomId: String, senderKey: String, localKnownChainIndex: Int) {
// do we have known requests for that session??
Timber.tag(loggerTag.value).v("Cancel Key Request if needed for $sessionId")
val knownRequest = cryptoStore.getOutgoingRoomKeyRequest(
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
roomId = roomId,
sessionId = sessionId,
senderKey = senderKey
)
if (knownRequest.isEmpty()) return Unit.also {
Timber.tag(loggerTag.value).v("Handle Cancel Key Request for $sessionId -- Was not currently requested")
}
if (knownRequest.size > 1) {
// It's worth logging, there should be only one
Timber.tag(loggerTag.value).w("Found multiple requests for same sessionId $sessionId")
}
knownRequest.forEach { request ->
when (request.state) {
OutgoingRoomKeyRequestState.UNSENT -> {
if (request.fromIndex >= localKnownChainIndex) {
// we have a good index we can cancel
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
}
}
OutgoingRoomKeyRequestState.SENT -> {
// It was already sent, and index satisfied we can cancel
if (request.fromIndex >= localKnownChainIndex) {
cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING)
}
}
OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> {
// It is already marked to be cancelled
}
OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> {
if (request.fromIndex >= localKnownChainIndex) {
// we just want to cancel now
cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING)
}
}
OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> {
// was already canceled
// if we need a better index, should we resend?
}
}
}
}
fun close() {
try {
outgoingRequestScope.cancel("User Terminate")
requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.clear()
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).w("Failed to shutDown request manager")
}
}
private fun internalQueueRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>, fromIndex: Int, force: Boolean) {
if (!cryptoStore.isKeyGossipingEnabled()) {
// we might want to try backup?
if (requestBody.roomId != null && requestBody.sessionId != null) {
requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(requestBody.roomId to requestBody.sessionId)
}
Timber.tag(loggerTag.value).d("discarding request for ${requestBody.sessionId} as gossiping is disabled")
return
}
Timber.tag(loggerTag.value).d("Queueing key request for ${requestBody.sessionId} force:$force")
val existing = cryptoStore.getOutgoingRoomKeyRequest(requestBody)
Timber.tag(loggerTag.value).v("Queueing key request exiting is ${existing?.state}")
when (existing?.state) {
null -> {
// create a new one
cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex)
}
OutgoingRoomKeyRequestState.UNSENT -> {
// nothing it's new or not yet handled
}
OutgoingRoomKeyRequestState.SENT -> {
// it was already requested
Timber.tag(loggerTag.value).d("The session ${requestBody.sessionId} is already requested")
if (force) {
// update to UNSENT
Timber.tag(loggerTag.value).d(".. force to request ${requestBody.sessionId}")
cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND)
} else {
if (existing.roomId != null && existing.sessionId != null) {
requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.push(existing.roomId to existing.sessionId)
}
}
}
OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> {
// request is canceled only if I got the keys so what to do here...
if (force) {
cryptoStore.updateOutgoingRoomKeyRequestState(existing.requestId, OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND)
}
}
OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> {
// It's already going to resend
}
OutgoingRoomKeyRequestState.SENT_THEN_CANCELED -> {
if (force) {
cryptoStore.deleteOutgoingRoomKeyRequest(existing.requestId)
cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients, fromIndex)
}
}
}
if (existing != null && existing.fromIndex >= fromIndex) {
// update the required index
cryptoStore.updateOutgoingRoomKeyRequiredIndex(existing.requestId, fromIndex)
}
}
private suspend fun internalProcessPendingKeyRequests() {
val toProcess = cryptoStore.getOutgoingRoomKeyRequests(OutgoingRoomKeyRequestState.pendingStates())
Timber.tag(loggerTag.value).v("Processing all pending key requests (found ${toProcess.size} pending)")
measureTimeMillis {
toProcess.forEach {
when (it.state) {
OutgoingRoomKeyRequestState.UNSENT -> handleUnsentRequest(it)
OutgoingRoomKeyRequestState.CANCELLATION_PENDING -> handleRequestToCancel(it)
OutgoingRoomKeyRequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> handleRequestToCancelWillResend(it)
OutgoingRoomKeyRequestState.SENT_THEN_CANCELED,
OutgoingRoomKeyRequestState.SENT -> {
// these are filtered out
}
}
}
}.let {
Timber.tag(loggerTag.value).v("Finish processing pending key request in $it ms")
}
val maxBackupCallsBySync = 60
var currentCalls = 0
measureTimeMillis {
while (requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.isNotEmpty() && currentCalls < maxBackupCallsBySync) {
requestDiscardedBecauseAlreadySentThatCouldBeTriedWithBackup.pop().let { (roomId, sessionId) ->
// we want to rate limit that somehow :/
perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)
}
currentCalls++
}
}.let {
Timber.tag(loggerTag.value).v("Finish querying backup in $it ms")
}
}
private suspend fun handleUnsentRequest(request: OutgoingKeyRequest) {
// In order to avoid generating to_device traffic, we can first check if the key is backed up
Timber.tag(loggerTag.value).v("Handling unsent request for megolm session ${request.sessionId} in ${request.roomId}")
val sessionId = request.sessionId ?: return
val roomId = request.roomId ?: return
if (perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)) {
// let's see what's the index
val knownIndex = tryOrNull {
inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "")?.wrapper?.firstKnownIndex
}
if (knownIndex != null && knownIndex <= request.fromIndex) {
// we found the key in backup with good enough index, so we can just mark as cancelled, no need to send request
Timber.tag(loggerTag.value).v("Megolm session $sessionId successfully restored from backup, do not send request")
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
return
}
}
// we need to send the request
val toDeviceContent = RoomKeyShareRequest(
requestingDeviceId = cryptoStore.getDeviceId(),
requestId = request.requestId,
action = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
body = request.requestBody
)
val contentMap = MXUsersDevicesMap<Any>()
request.recipients.forEach { userToDeviceMap ->
userToDeviceMap.value.forEach { deviceId ->
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
}
}
val params = SendToDeviceTask.Params(
eventType = EventType.ROOM_KEY_REQUEST,
contentMap = contentMap,
transactionId = request.requestId
)
try {
withContext(coroutineDispatchers.io) {
sendToDeviceTask.executeRetry(params, 3)
}
Timber.tag(loggerTag.value).d("Key request sent for $sessionId in room $roomId to ${request.recipients}")
// The request was sent, so update state
cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT)
// TODO update the audit trail
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).v("Failed to request $sessionId targets:${request.recipients}")
}
}
private suspend fun handleRequestToCancel(request: OutgoingKeyRequest): Boolean {
Timber.tag(loggerTag.value).v("handleRequestToCancel for megolm session ${request.sessionId}")
// we have to cancel this
val toDeviceContent = RoomKeyShareRequest(
requestingDeviceId = cryptoStore.getDeviceId(),
requestId = request.requestId,
action = GossipingToDeviceObject.ACTION_SHARE_CANCELLATION
)
val contentMap = MXUsersDevicesMap<Any>()
request.recipients.forEach { userToDeviceMap ->
userToDeviceMap.value.forEach { deviceId ->
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
}
}
val params = SendToDeviceTask.Params(
eventType = EventType.ROOM_KEY_REQUEST,
contentMap = contentMap,
transactionId = request.requestId
)
return try {
withContext(coroutineDispatchers.io) {
sendToDeviceTask.executeRetry(params, 3)
}
// The request cancellation was sent, we don't delete yet because we want
// to keep trace of the sent replies
cryptoStore.updateOutgoingRoomKeyRequestState(request.requestId, OutgoingRoomKeyRequestState.SENT_THEN_CANCELED)
true
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).v("Failed to cancel request ${request.requestId} for session $sessionId targets:${request.recipients}")
false
}
}
private suspend fun handleRequestToCancelWillResend(request: OutgoingKeyRequest) {
if (handleRequestToCancel(request)) {
// this will create a new unsent request with no replies that will be process in the following call
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
request.requestBody?.let { cryptoStore.getOrAddOutgoingRoomKeyRequest(it, request.recipients, request.fromIndex) }
}
}
}

View file

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

View file

@ -0,0 +1,133 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import dagger.Lazy
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.util.awaitCallback
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
import javax.inject.Inject
// I keep the same name as OutgoingGossipingRequestManager to ease filtering of logs
private val loggerTag = LoggerTag("OutgoingGossipingRequestManager", LoggerTag.CRYPTO)
/**
* Used to try to get the key for UISI messages before sending room key request.
* We are adding some rate limiting to avoid querying too much for a key not in backup.
* Nonetheless the backup can be updated so we might want to retry from time to time.
*/
internal class PerSessionBackupQueryRateLimiter @Inject constructor(
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val keysBackupService: Lazy<DefaultKeysBackupService>,
private val cryptoStore: IMXCryptoStore,
private val clock: Clock,
) {
companion object {
val MIN_TRY_BACKUP_PERIOD_MILLIS = 60 * 60_000 // 1 hour
}
data class Info(
val megolmSessionId: String,
val roomId: String
)
data class LastTry(
val backupVersion: String,
val timestamp: Long
)
/**
* Remember what we already tried (a key not in backup or some server issue)
* We might want to retry from time to time as the backup could have been updated
*/
private val lastFailureMap = mutableMapOf<Info, LastTry>()
private var backupVersion: KeysVersionResult? = null
private var savedKeyBackupKeyInfo: SavedKeyBackupKeyInfo? = null
var backupWasCheckedFromServer: Boolean = false
val now = clock.epochMillis()
fun refreshBackupInfoIfNeeded(force: Boolean = false) {
if (backupWasCheckedFromServer && !force) return
Timber.tag(loggerTag.value).v("Checking if can access a backup")
backupWasCheckedFromServer = true
val knownBackupSecret = cryptoStore.getKeyBackupRecoveryKeyInfo()
?: return Unit.also {
Timber.tag(loggerTag.value).v("We don't have the backup secret!")
}
this.backupVersion = keysBackupService.get().keysBackupVersion
this.savedKeyBackupKeyInfo = knownBackupSecret
}
suspend fun tryFromBackupIfPossible(sessionId: String, roomId: String): Boolean {
Timber.tag(loggerTag.value).v("tryFromBackupIfPossible for session:$sessionId in $roomId")
refreshBackupInfoIfNeeded()
val currentVersion = backupVersion
if (savedKeyBackupKeyInfo?.version == null ||
currentVersion == null ||
currentVersion.version != savedKeyBackupKeyInfo?.version) {
// We can't access the backup
Timber.tag(loggerTag.value).v("Can't get backup version info")
return false
}
val cacheKey = Info(sessionId, roomId)
val lastTry = lastFailureMap[cacheKey]
val shouldQuery =
lastTry == null ||
lastTry.backupVersion != currentVersion.version ||
(now - lastTry.timestamp) > MIN_TRY_BACKUP_PERIOD_MILLIS
if (!shouldQuery) return false
val successfullyImported = withContext(coroutineDispatchers.io) {
try {
awaitCallback<ImportRoomKeysResult> {
keysBackupService.get().restoreKeysWithRecoveryKey(
currentVersion,
savedKeyBackupKeyInfo?.recoveryKey ?: "",
roomId,
sessionId,
null,
it
)
}.successfullyNumberOfImportedKeys
} catch (failure: Throwable) {
// Fail silently
Timber.tag(loggerTag.value).v("getFromBackup failed ${failure.localizedMessage}")
0
}
}
if (successfullyImported == 1) {
Timber.tag(loggerTag.value).v("Found key in backup session:$sessionId in $roomId")
lastFailureMap.remove(cacheKey)
return true
} else {
Timber.tag(loggerTag.value).v("Failed to find key in backup session:$sessionId in $roomId")
lastFailureMap[cacheKey] = LastTry(currentVersion.version, clock.epochMillis())
return false
}
}
}

View file

@ -16,12 +16,21 @@
package org.matrix.android.sdk.internal.crypto
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.session.SessionScope
import javax.inject.Inject
@SessionScope
internal class RoomEncryptorsStore @Inject constructor() {
internal class RoomEncryptorsStore @Inject constructor(
private val cryptoStore: IMXCryptoStore,
private val megolmEncryptionFactory: MXMegolmEncryptionFactory,
private val olmEncryptionFactory: MXOlmEncryptionFactory,
) {
// MXEncrypting instance for each room.
private val roomEncryptors = mutableMapOf<String, IMXEncrypting>()
@ -34,7 +43,18 @@ internal class RoomEncryptorsStore @Inject constructor() {
fun get(roomId: String): IMXEncrypting? {
return synchronized(roomEncryptors) {
roomEncryptors[roomId]
val cache = roomEncryptors[roomId]
if (cache != null) {
return@synchronized cache
} else {
val alg: IMXEncrypting? = when (cryptoStore.getRoomAlgorithm(roomId)) {
MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId)
MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId)
else -> null
}
alg?.let { roomEncryptors.put(roomId, it) }
return@synchronized alg
}
}
}
}

View file

@ -0,0 +1,300 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.util.toBase64NoPadding
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("SecretShareManager", LoggerTag.CRYPTO)
@SessionScope
internal class SecretShareManager @Inject constructor(
private val credentials: Credentials,
private val cryptoStore: IMXCryptoStore,
private val cryptoCoroutineScope: CoroutineScope,
private val messageEncrypter: MessageEncrypter,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val sendToDeviceTask: SendToDeviceTask,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val clock: Clock,
) {
companion object {
private const val SECRET_SHARE_WINDOW_DURATION = 5 * 60 * 1000 // 5 minutes
}
/**
* Secret gossiping only occurs during a limited window period after interactive verification.
* We keep track of recent verification in memory for that purpose (no need to persist)
*/
private val recentlyVerifiedDevices = mutableMapOf<String, Long>()
private val verifMutex = Mutex()
/**
* Secrets are exchanged as part of interactive verification,
* so we can just store in memory.
*/
private val outgoingSecretRequests = mutableListOf<SecretShareRequest>()
// the listeners
private val gossipingRequestListeners: MutableSet<GossipingRequestListener> = HashSet()
fun addListener(listener: GossipingRequestListener) {
synchronized(gossipingRequestListeners) {
gossipingRequestListeners.add(listener)
}
}
fun removeListener(listener: GossipingRequestListener) {
synchronized(gossipingRequestListeners) {
gossipingRequestListeners.remove(listener)
}
}
/**
* Called when a session has been verified.
* This information can be used by the manager to decide whether or not to fullfill gossiping requests.
* This should be called as fast as possible after a successful self interactive verification
*/
fun onVerificationCompleteForDevice(deviceId: String) {
// For now we just keep an in memory cache
cryptoCoroutineScope.launch {
verifMutex.withLock {
recentlyVerifiedDevices[deviceId] = clock.epochMillis()
}
}
}
suspend fun handleSecretRequest(toDevice: Event) {
val request = toDevice.getClearContent().toModel<SecretShareRequest>()
?: return Unit.also {
Timber.tag(loggerTag.value)
.w("handleSecretRequest() : malformed request")
}
// val (action, requestingDeviceId, requestId, secretName) = it
val secretName = request.secretName ?: return Unit.also {
Timber.tag(loggerTag.value)
.v("handleSecretRequest() : Missing secret name")
}
val userId = toDevice.senderId ?: return Unit.also {
Timber.tag(loggerTag.value)
.v("handleSecretRequest() : Missing senderId")
}
if (userId != credentials.userId) {
// secrets are only shared between our own devices
Timber.tag(loggerTag.value)
.e("Ignoring secret share request from other users $userId")
return
}
val deviceId = request.requestingDeviceId
?: return Unit.also {
Timber.tag(loggerTag.value)
.w("handleSecretRequest() : malformed request norequestingDeviceId ")
}
val device = cryptoStore.getUserDevice(credentials.userId, deviceId)
?: return Unit.also {
Timber.tag(loggerTag.value)
.e("Received secret share request from unknown device $deviceId")
}
val isRequestingDeviceTrusted = device.isVerified
val isRecentInteractiveVerification = hasBeenVerifiedLessThanFiveMinutesFromNow(device.deviceId)
if (isRequestingDeviceTrusted && isRecentInteractiveVerification) {
// we can share the secret
val secretValue = when (secretName) {
MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master
SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned
USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user
KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey
?.let {
extractCurveKeyFromRecoveryKey(it)?.toBase64NoPadding()
}
else -> null
}
if (secretValue == null) {
Timber.tag(loggerTag.value)
.i("The secret is unknown $secretName, passing to app layer")
val toList = synchronized(gossipingRequestListeners) { gossipingRequestListeners.toList() }
toList.onEach { listener ->
listener.onSecretShareRequest(request)
}
return
}
val payloadJson = mapOf(
"type" to EventType.SEND_SECRET,
"content" to mapOf(
"request_id" to request.requestId,
"secret" to secretValue
)
)
// Is it possible that we don't have an olm session?
val devicesByUser = mapOf(device.userId to listOf(device))
val usersDeviceMap = try {
ensureOlmSessionsForDevicesAction.handle(devicesByUser)
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.w("Can't share secret ${request.secretName}: Failed to establish olm session")
return
}
val olmSessionResult = usersDeviceMap.getObject(device.userId, device.deviceId)
if (olmSessionResult?.sessionId == null) {
Timber.tag(loggerTag.value)
.w("secret share: no session with this device $deviceId, probably because there were no one-time keys")
return
}
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(device))
val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(device.userId, device.deviceId, encodedPayload)
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
try {
// raise the retries for secret
sendToDeviceTask.executeRetry(sendToDeviceParams, 6)
Timber.tag(loggerTag.value)
.i("successfully shared secret $secretName to ${device.shortDebugString()}")
// TODO add a trail for that in audit logs
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.e(failure, "failed to send shared secret $secretName to ${device.shortDebugString()}")
}
} else {
Timber.tag(loggerTag.value)
.d(" Received secret share request from un-authorised device ${device.deviceId}")
}
}
private suspend fun hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId: String): Boolean {
val verifTimestamp = verifMutex.withLock {
recentlyVerifiedDevices[deviceId]
} ?: return false
val age = clock.epochMillis() - verifTimestamp
return age < SECRET_SHARE_WINDOW_DURATION
}
suspend fun requestSecretTo(deviceId: String, secretName: String) {
val cryptoDeviceInfo = cryptoStore.getUserDevice(credentials.userId, deviceId) ?: return Unit.also {
Timber.tag(loggerTag.value)
.d("Can't request secret for $secretName unknown device $deviceId")
}
val toDeviceContent = SecretShareRequest(
requestingDeviceId = credentials.deviceId,
secretName = secretName,
requestId = createUniqueTxnId()
)
verifMutex.withLock {
outgoingSecretRequests.add(toDeviceContent)
}
val contentMap = MXUsersDevicesMap<Any>()
contentMap.setObject(cryptoDeviceInfo.userId, cryptoDeviceInfo.deviceId, toDeviceContent)
val params = SendToDeviceTask.Params(
eventType = EventType.REQUEST_SECRET,
contentMap = contentMap
)
try {
withContext(coroutineDispatchers.io) {
sendToDeviceTask.executeRetry(params, 3)
}
Timber.tag(loggerTag.value)
.d("Secret request sent for $secretName to ${cryptoDeviceInfo.shortDebugString()}")
// TODO update the audit trail
} catch (failure: Throwable) {
Timber.tag(loggerTag.value)
.w("Failed to request secret $secretName to ${cryptoDeviceInfo.shortDebugString()}")
}
}
suspend fun onSecretSendReceived(toDevice: Event, handleGossip: ((name: String, value: String) -> Boolean)) {
Timber.tag(loggerTag.value)
.i("onSecretSend() from ${toDevice.senderId} : onSecretSendReceived ${toDevice.content?.get("sender_key")}")
if (!toDevice.isEncrypted()) {
// secret send messages must be encrypted
Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event")
return
}
// Was that sent by us?
if (toDevice.senderId != credentials.userId) {
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}")
return
}
val secretContent = toDevice.getClearContent().toModel<SecretSendEventContent>() ?: return
val existingRequest = verifMutex.withLock {
outgoingSecretRequests.firstOrNull { it.requestId == secretContent.requestId }
}
// As per spec:
// Clients should ignore m.secret.send events received from devices that it did not send an m.secret.request event to.
if (existingRequest?.secretName == null) {
Timber.tag(loggerTag.value).i("onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
return
}
// we don't need to cancel the request as we only request to one device
// just forget about the request now
verifMutex.withLock {
outgoingSecretRequests.remove(existingRequest)
}
if (!handleGossip(existingRequest.secretName, secretContent.secretValue)) {
// TODO Ask to application layer?
Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK")
}
}
}

View file

@ -1,153 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import android.content.Context
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.util.time.Clock
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import timber.log.Timber
import javax.inject.Inject
internal class SendGossipRequestWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) :
SessionSafeCoroutineWorker<SendGossipRequestWorker.Params>(context, params, sessionManager, Params::class.java) {
@JsonClass(generateAdapter = true)
internal data class Params(
override val sessionId: String,
val keyShareRequest: OutgoingRoomKeyRequest? = null,
val secretShareRequest: OutgoingSecretRequest? = null,
// The txnId for the sendToDevice request. Nullable for compatibility reasons, but MUST always be provided
// to use the same value if this worker is retried.
val txnId: String? = null,
override val lastFailureMessage: String? = null
) : SessionWorkerParams
@Inject lateinit var sendToDeviceTask: SendToDeviceTask
@Inject lateinit var cryptoStore: IMXCryptoStore
@Inject lateinit var credentials: Credentials
@Inject lateinit var clock: Clock
override fun injectWith(injector: SessionComponent) {
injector.inject(this)
}
override suspend fun doSafeWork(params: Params): Result {
// params.txnId should be provided in all cases now. But Params can be deserialized by
// the WorkManager from data serialized in a previous version of the application, so without the txnId field.
// So if not present, we create a txnId
val txnId = params.txnId ?: createUniqueTxnId()
val contentMap = MXUsersDevicesMap<Any>()
val eventType: String
val requestId: String
when {
params.keyShareRequest != null -> {
eventType = EventType.ROOM_KEY_REQUEST
requestId = params.keyShareRequest.requestId
val toDeviceContent = RoomKeyShareRequest(
requestingDeviceId = credentials.deviceId,
requestId = params.keyShareRequest.requestId,
action = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
body = params.keyShareRequest.requestBody
)
cryptoStore.saveGossipingEvent(Event(
type = eventType,
content = toDeviceContent.toContent(),
senderId = credentials.userId
).also {
it.ageLocalTs = clock.epochMillis()
})
params.keyShareRequest.recipients.forEach { userToDeviceMap ->
userToDeviceMap.value.forEach { deviceId ->
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
}
}
}
params.secretShareRequest != null -> {
eventType = EventType.REQUEST_SECRET
requestId = params.secretShareRequest.requestId
val toDeviceContent = SecretShareRequest(
requestingDeviceId = credentials.deviceId,
requestId = params.secretShareRequest.requestId,
action = GossipingToDeviceObject.ACTION_SHARE_REQUEST,
secretName = params.secretShareRequest.secretName
)
cryptoStore.saveGossipingEvent(Event(
type = eventType,
content = toDeviceContent.toContent(),
senderId = credentials.userId
).also {
it.ageLocalTs = clock.epochMillis()
})
params.secretShareRequest.recipients.forEach { userToDeviceMap ->
userToDeviceMap.value.forEach { deviceId ->
contentMap.setObject(userToDeviceMap.key, deviceId, toDeviceContent)
}
}
}
else -> {
return buildErrorResult(params, "Unknown empty gossiping request").also {
Timber.e("Unknown empty gossiping request: $params")
}
}
}
try {
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENDING)
sendToDeviceTask.execute(
SendToDeviceTask.Params(
eventType = eventType,
contentMap = contentMap,
transactionId = txnId
)
)
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENT)
return Result.success()
} catch (throwable: Throwable) {
return if (throwable.shouldBeRetried()) {
Result.retry()
} else {
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.FAILED_TO_SEND)
buildErrorResult(params, throwable.localizedMessage ?: "error")
}
}
}
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
}

View file

@ -1,170 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import android.content.Context
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.util.time.Clock
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import timber.log.Timber
import javax.inject.Inject
internal class SendGossipWorker(
context: Context,
params: WorkerParameters,
sessionManager: SessionManager
) : SessionSafeCoroutineWorker<SendGossipWorker.Params>(context, params, sessionManager, Params::class.java) {
@JsonClass(generateAdapter = true)
internal data class Params(
override val sessionId: String,
val secretValue: String,
val requestUserId: String?,
val requestDeviceId: String?,
val requestId: String?,
// The txnId for the sendToDevice request. Nullable for compatibility reasons, but MUST always be provided
// to use the same value if this worker is retried.
val txnId: String? = null,
override val lastFailureMessage: String? = null
) : SessionWorkerParams
@Inject lateinit var sendToDeviceTask: SendToDeviceTask
@Inject lateinit var cryptoStore: IMXCryptoStore
@Inject lateinit var credentials: Credentials
@Inject lateinit var messageEncrypter: MessageEncrypter
@Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction
@Inject lateinit var clock: Clock
override fun injectWith(injector: SessionComponent) {
injector.inject(this)
}
override suspend fun doSafeWork(params: Params): Result {
// params.txnId should be provided in all cases now. But Params can be deserialized by
// the WorkManager from data serialized in a previous version of the application, so without the txnId field.
// So if not present, we create a txnId
val txnId = params.txnId ?: createUniqueTxnId()
val eventType: String = EventType.SEND_SECRET
val toDeviceContent = SecretSendEventContent(
requestId = params.requestId ?: "",
secretValue = params.secretValue
)
val requestingUserId = params.requestUserId ?: ""
val requestingDeviceId = params.requestDeviceId ?: ""
val deviceInfo = cryptoStore.getUserDevice(requestingUserId, requestingDeviceId)
?: return buildErrorResult(params, "Unknown deviceInfo, cannot send message").also {
cryptoStore.updateGossipingRequestState(
requestUserId = params.requestUserId,
requestDeviceId = params.requestDeviceId,
requestId = params.requestId,
state = GossipingRequestState.FAILED_TO_ACCEPTED
)
Timber.e("Unknown deviceInfo, cannot send message, sessionId: ${params.requestDeviceId}")
}
val sendToDeviceMap = MXUsersDevicesMap<Any>()
val devicesByUser = mapOf(requestingUserId to listOf(deviceInfo))
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
val olmSessionResult = usersDeviceMap.getObject(requestingUserId, requestingDeviceId)
if (olmSessionResult?.sessionId == null) {
// no session with this device, probably because there
// were no one-time keys.
return buildErrorResult(params, "no session with this device").also {
cryptoStore.updateGossipingRequestState(
requestUserId = params.requestUserId,
requestDeviceId = params.requestDeviceId,
requestId = params.requestId,
state = GossipingRequestState.FAILED_TO_ACCEPTED
)
Timber.e("no session with this device $requestingDeviceId, probably because there were no one-time keys.")
}
}
val payloadJson = mapOf(
"type" to EventType.SEND_SECRET,
"content" to toDeviceContent.toContent()
)
try {
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
sendToDeviceMap.setObject(requestingUserId, requestingDeviceId, encodedPayload)
} catch (failure: Throwable) {
Timber.e("## Fail to encrypt gossip + ${failure.localizedMessage}")
}
cryptoStore.saveGossipingEvent(Event(
type = eventType,
content = toDeviceContent.toContent(),
senderId = credentials.userId
).also {
it.ageLocalTs = clock.epochMillis()
})
try {
sendToDeviceTask.execute(
SendToDeviceTask.Params(
eventType = EventType.ENCRYPTED,
contentMap = sendToDeviceMap,
transactionId = txnId
)
)
cryptoStore.updateGossipingRequestState(
requestUserId = params.requestUserId,
requestDeviceId = params.requestDeviceId,
requestId = params.requestId,
state = GossipingRequestState.ACCEPTED
)
return Result.success()
} catch (throwable: Throwable) {
return if (throwable.shouldBeRetried()) {
Result.retry()
} else {
cryptoStore.updateGossipingRequestState(
requestUserId = params.requestUserId,
requestDeviceId = params.requestDeviceId,
requestId = params.requestId,
state = GossipingRequestState.FAILED_TO_ACCEPTED
)
buildErrorResult(params, throwable.localizedMessage ?: "error")
}
}
}
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
}

View file

@ -17,12 +17,13 @@
package org.matrix.android.sdk.internal.crypto.actions
import androidx.annotation.WorkerThread
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
import org.matrix.android.sdk.internal.crypto.RoomDecryptorProvider
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
@ -30,12 +31,13 @@ import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
import javax.inject.Inject
internal class MegolmSessionDataImporter @Inject constructor(
private val olmDevice: MXOlmDevice,
private val roomDecryptorProvider: RoomDecryptorProvider,
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
private val cryptoStore: IMXCryptoStore,
private val clock: Clock,
private val loggerTag = LoggerTag("MegolmSessionDataImporter", LoggerTag.CRYPTO)
internal class MegolmSessionDataImporter @Inject constructor(private val olmDevice: MXOlmDevice,
private val roomDecryptorProvider: RoomDecryptorProvider,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val cryptoStore: IMXCryptoStore,
private val clock: Clock,
) {
/**
@ -66,19 +68,23 @@ internal class MegolmSessionDataImporter @Inject constructor(
if (null != decrypting) {
try {
val sessionId = megolmSessionData.sessionId
Timber.v("## importRoomKeys retrieve senderKey " + megolmSessionData.senderKey + " sessionId " + sessionId)
Timber.tag(loggerTag.value).v("## importRoomKeys retrieve senderKey ${megolmSessionData.senderKey} sessionId $sessionId")
totalNumbersOfImportedKeys++
// cancel any outstanding room key requests for this session
val roomKeyRequestBody = RoomKeyRequestBody(
algorithm = megolmSessionData.algorithm,
roomId = megolmSessionData.roomId,
senderKey = megolmSessionData.senderKey,
sessionId = megolmSessionData.sessionId
)
outgoingGossipingRequestManager.cancelRoomKeyRequest(roomKeyRequestBody)
Timber.tag(loggerTag.value).d("Imported megolm session $sessionId from backup=$fromBackup in ${megolmSessionData.roomId}")
outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(
megolmSessionData.sessionId ?: "",
megolmSessionData.roomId ?: "",
megolmSessionData.senderKey ?: "",
tryOrNull {
olmInboundGroupSessionWrappers
.firstOrNull { it.olmInboundGroupSession?.sessionIdentifier() == megolmSessionData.sessionId }
?.firstKnownIndex?.toInt()
} ?: 0
)
// Have another go at decrypting events sent with this session
when (decrypting) {
@ -87,7 +93,7 @@ internal class MegolmSessionDataImporter @Inject constructor(
}
}
} catch (e: Exception) {
Timber.e(e, "## importRoomKeys() : onNewSession failed")
Timber.tag(loggerTag.value).e(e, "## importRoomKeys() : onNewSession failed")
}
}
@ -109,7 +115,7 @@ internal class MegolmSessionDataImporter @Inject constructor(
val t1 = clock.epochMillis()
Timber.v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)")
Timber.tag(loggerTag.value).v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)")
return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys)
}

View file

@ -17,8 +17,6 @@
package org.matrix.android.sdk.internal.crypto.algorithms
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
@ -44,23 +42,4 @@ internal interface IMXDecrypting {
* @param event the key event.
*/
fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {}
/**
* Determine if we have the keys necessary to respond to a room key request
*
* @param request keyRequest
* @return true if we have the keys and could (theoretically) share
*/
fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean = false
/**
* Send the response to a room key request.
*
* @param request keyRequest
*/
fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {}
fun shareSecretWithDevice(request: IncomingSecretShareRequest, secretValue: String) {}
fun requestKeysForEvent(event: Event, withHeld: Boolean)
}

View file

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

View file

@ -17,51 +17,32 @@
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import dagger.Lazy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.withLock
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.session.StreamEventsManager
import timber.log.Timber
private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO)
internal class MXMegolmDecryption(private val userId: String,
private val olmDevice: MXOlmDevice,
private val deviceListManager: DeviceListManager,
private val outgoingGossipingRequestManager: OutgoingGossipingRequestManager,
private val messageEncrypter: MessageEncrypter,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val cryptoStore: IMXCryptoStore,
private val sendToDeviceTask: SendToDeviceTask,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope,
private val liveEventManager: Lazy<StreamEventsManager>
) : IMXDecrypting, IMXWithHeldExtension {
internal class MXMegolmDecryption(
private val olmDevice: MXOlmDevice,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val cryptoStore: IMXCryptoStore,
private val liveEventManager: Lazy<StreamEventsManager>
) : IMXDecrypting {
var newSessionListener: NewSessionListener? = null
@ -73,10 +54,7 @@ internal class MXMegolmDecryption(private val userId: String,
@Throws(MXCryptoError::class)
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
// If cross signing is enabled, we don't send request until the keys are trusted
// There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once
val requestOnFail = cryptoStore.getMyCrossSigningInfo()?.isTrusted() == true
return decryptEvent(event, timeline, requestOnFail)
return decryptEvent(event, timeline, true)
}
@Throws(MXCryptoError::class)
@ -126,13 +104,14 @@ internal class MXMegolmDecryption(private val userId: String,
if (throwable is MXCryptoError.OlmError) {
// TODO Check the value of .message
if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
// addEventToPendingList(event, timeline)
// The session might has been partially withheld (and only pass ratcheted)
// So we know that session, but it's ratcheted and we can't decrypt at that index
if (requestKeysOnFail) {
requestKeysForEvent(event)
}
// Check if partially withheld
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
if (withHeldInfo != null) {
if (requestKeysOnFail) {
requestKeysForEvent(event, true)
}
// Encapsulate as withHeld exception
throw MXCryptoError.Base(
MXCryptoError.ErrorType.KEYS_WITHHELD,
@ -141,10 +120,6 @@ internal class MXMegolmDecryption(private val userId: String,
)
}
if (requestKeysOnFail) {
requestKeysForEvent(event, false)
}
throw MXCryptoError.Base(
MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
"UNKNOWN_MESSAGE_INDEX",
@ -162,27 +137,22 @@ internal class MXMegolmDecryption(private val userId: String,
)
}
if (throwable is MXCryptoError.Base) {
if (
/** if the session is unknown*/
throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
) {
if (throwable.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
// Check if it was withheld by sender to enrich error code
val withHeldInfo = cryptoStore.getWithHeldMegolmSession(event.roomId, encryptedEventContent.sessionId)
if (withHeldInfo != null) {
if (requestKeysOnFail) {
requestKeysForEvent(event, true)
requestKeysForEvent(event)
}
// Encapsulate as withHeld exception
throw MXCryptoError.Base(
MXCryptoError.ErrorType.KEYS_WITHHELD,
withHeldInfo.code?.value ?: "",
withHeldInfo.reason
)
} else {
// This is un-used in Matrix Android SDK2, not sure if needed
// addEventToPendingList(event, timeline)
if (requestKeysOnFail) {
requestKeysForEvent(event, false)
}
withHeldInfo.reason)
}
if (requestKeysOnFail) {
requestKeysForEvent(event)
}
}
}
@ -198,55 +168,10 @@ internal class MXMegolmDecryption(private val userId: String,
*
* @param event the event
*/
override fun requestKeysForEvent(event: Event, withHeld: Boolean) {
val sender = event.senderId ?: return
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
val senderDevice = encryptedEventContent?.deviceId ?: return
val recipients = if (event.senderId == userId || withHeld) {
mapOf(
userId to listOf("*")
)
} else {
// for the case where you share the key with a device that has a broken olm session
// The other user might Re-shares a megolm session key with devices if the key has already been
// sent to them.
mapOf(
userId to listOf("*"),
sender to listOf(senderDevice)
)
}
val requestBody = RoomKeyRequestBody(
roomId = event.roomId,
algorithm = encryptedEventContent.algorithm,
senderKey = encryptedEventContent.senderKey,
sessionId = encryptedEventContent.sessionId
)
outgoingGossipingRequestManager.sendRoomKeyRequest(requestBody, recipients)
private fun requestKeysForEvent(event: Event) {
outgoingKeyRequestManager.requestKeyForEvent(event, false)
}
// /**
// * Add an event to the list of those we couldn't decrypt the first time we
// * saw them.
// *
// * @param event the event to try to decrypt later
// * @param timelineId the timeline identifier
// */
// private fun addEventToPendingList(event: Event, timelineId: String) {
// val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return
// val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}"
//
// val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() }
// val events = timeline.getOrPut(timelineId) { ArrayList() }
//
// if (event !in events) {
// Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}")
// events.add(event)
// }
// }
/**
* Handle a key event.
*
@ -266,6 +191,11 @@ internal class MXMegolmDecryption(private val userId: String,
return
}
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
if (!cryptoStore.isKeyGossipingEnabled()) {
Timber.tag(loggerTag.value)
.i("onRoomKeyEvent(), ignore forward adding as per crypto config : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
return
}
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>()
?: return
@ -306,7 +236,7 @@ internal class MXMegolmDecryption(private val userId: String,
}
Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
val added = olmDevice.addInboundGroupSession(
val addSessionResult = olmDevice.addInboundGroupSession(
roomKeyContent.sessionId,
roomKeyContent.sessionKey,
roomKeyContent.roomId,
@ -316,18 +246,47 @@ internal class MXMegolmDecryption(private val userId: String,
exportFormat
)
if (added) {
when (addSessionResult) {
is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex
is MXOlmDevice.AddSessionResult.NotImportedHigherIndex -> addSessionResult.newIndex
else -> null
}?.let { index ->
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
val fromDevice = (event.content?.get("sender_key") as? String)?.let { senderDeviceIdentityKey ->
cryptoStore.getUserDeviceList(event.senderId ?: "")
?.firstOrNull {
it.identityKey() == senderDeviceIdentityKey
}
}?.deviceId
outgoingKeyRequestManager.onRoomKeyForwarded(
sessionId = roomKeyContent.sessionId,
algorithm = roomKeyContent.algorithm ?: "",
roomId = roomKeyContent.roomId,
senderKey = senderKey,
fromIndex = index,
fromDevice = fromDevice,
event = event)
cryptoStore.saveIncomingForwardKeyAuditTrail(
roomId = roomKeyContent.roomId,
sessionId = roomKeyContent.sessionId,
senderKey = senderKey,
algorithm = roomKeyContent.algorithm ?: "",
userId = event.senderId ?: "",
deviceId = fromDevice ?: "",
chainIndex = index.toLong())
// The index is used to decide if we cancel sent request or if we wait for a better key
outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, senderKey, index)
}
}
if (addSessionResult is MXOlmDevice.AddSessionResult.Imported) {
Timber.tag(loggerTag.value)
.d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}")
defaultKeysBackupService.maybeBackupKeys()
val content = RoomKeyRequestBody(
algorithm = roomKeyContent.algorithm,
roomId = roomKeyContent.roomId,
sessionId = roomKeyContent.sessionId,
senderKey = senderKey
)
outgoingGossipingRequestManager.cancelRoomKeyRequest(content)
onNewSession(roomKeyContent.roomId, senderKey, roomKeyContent.sessionId)
}
}
@ -343,77 +302,4 @@ internal class MXMegolmDecryption(private val userId: String,
Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey")
newSessionListener?.onNewSession(roomId, senderKey, sessionId)
}
override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean {
val roomId = request.requestBody?.roomId ?: return false
val senderKey = request.requestBody.senderKey ?: return false
val sessionId = request.requestBody.sessionId ?: return false
return olmDevice.hasInboundSessionKeys(roomId, senderKey, sessionId)
}
override fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {
// sanity checks
if (request.requestBody == null) {
return
}
val userId = request.userId ?: return
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
val body = request.requestBody
val sessionHolder = try {
olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId)
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session for request $body")
return@launch
}
val export = sessionHolder.mutex.withLock {
sessionHolder.wrapper.exportKeys()
} ?: return@launch Unit.also {
Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session ${body.sessionId}")
}
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
.mapCatching {
val deviceId = request.deviceId
val deviceInfo = cryptoStore.getUserDevice(userId, deviceId ?: "")
if (deviceInfo == null) {
throw RuntimeException()
} else {
val devicesByUser = mapOf(userId to listOf(deviceInfo))
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
val olmSessionResult = usersDeviceMap.getObject(userId, deviceId)
if (olmSessionResult?.sessionId == null) {
// no session with this device, probably because there
// were no one-time keys.
Timber.tag(loggerTag.value).e("no session with this device $deviceId, probably because there were no one-time keys.")
return@mapCatching
}
Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sharing session ${body.sessionId} with device $userId:$deviceId")
val payloadJson = mapOf(
"type" to EventType.FORWARDED_ROOM_KEY,
"content" to export
)
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sending ${body.sessionId} to $userId:$deviceId")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
try {
sendToDeviceTask.execute(sendToDeviceParams)
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice() : Failed to send ${body.sessionId} to $userId:$deviceId")
}
}
}
}
}
override fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoStore.addWithHeldMegolmSession(withHeldInfo)
}
}
}

View file

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

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

View file

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

View file

@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.fromBase64
import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
@ -70,6 +71,7 @@ internal class DefaultCrossSigningService @Inject constructor(
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope,
private val workManagerProvider: WorkManagerProvider,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
) : CrossSigningService,
DeviceListManager.UserDevicesUpdateListener {
@ -785,7 +787,8 @@ internal class DefaultCrossSigningService @Inject constructor(
// If it's me, recheck trust of all users and devices?
val users = ArrayList<String>()
if (otherUserId == userId && currentTrust != trusted) {
// reRequestAllPendingRoomKeyRequest()
// notify key requester
outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted)
cryptoStore.updateUsersTrust {
users.add(it)
checkUserTrust(it).isVerified()
@ -800,19 +803,4 @@ internal class DefaultCrossSigningService @Inject constructor(
}
}
}
// private fun reRequestAllPendingRoomKeyRequest() {
// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
// Timber.d("## CrossSigning - reRequest pending outgoing room key requests")
// cryptoStore.getOutgoingRoomKeyRequests().forEach {
// it.requestBody?.let { requestBody ->
// if (cryptoStore.getInboundGroupSession(requestBody.sessionId ?: "", requestBody.senderKey ?: "") == null) {
// outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody)
// } else {
// outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody)
// }
// }
// }
// }
// }
}

View file

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

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 {
@ -380,10 +380,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
return IntegrityResult.Success(keyInfo.content.passphrase != null)
}
override fun requestSecret(name: String, myOtherDeviceId: String) {
outgoingGossipingRequestManager.sendSecretShareRequest(
name,
mapOf(userId to listOf(myOtherDeviceId))
)
override suspend fun requestSecret(name: String, myOtherDeviceId: String) {
secretShareManager.requestSecretTo(myOtherDeviceId, name)
}
}

View file

@ -19,23 +19,22 @@ package org.matrix.android.sdk.internal.crypto.store
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.api.session.crypto.model.TrailType
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
@ -82,6 +81,15 @@ internal interface IMXCryptoStore {
*/
fun setGlobalBlacklistUnverifiedDevices(block: Boolean)
/**
* Enable or disable key gossiping.
* Default is true.
* If set to false this device won't send key_request nor will accept key forwarded
*/
fun enableKeyGossiping(enable: Boolean)
fun isKeyGossipingEnabled(): Boolean
/**
* Provides the rooms ids list in which the messages are not encrypted for the unverified devices.
*
@ -125,18 +133,6 @@ internal interface IMXCryptoStore {
*/
fun getDeviceTrackingStatuses(): Map<String, Int>
/**
* @return the pending IncomingRoomKeyRequest requests
*/
fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon>
fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?)
fun storeIncomingGossipingRequests(requests: List<IncomingShareRequestCommon>)
// fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest>
/**
* Indicate if the store contains data for the passed account.
*
@ -377,7 +373,9 @@ internal interface IMXCryptoStore {
* @param requestBody the request body
* @return an OutgoingRoomKeyRequest instance or null
*/
fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest?
fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest?
fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest?
fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List<OutgoingKeyRequest>
/**
* Look for an existing outgoing room key request, and if none is found, add a new one.
@ -385,39 +383,59 @@ internal interface IMXCryptoStore {
* @param request the request
* @return either the same instance as passed in, or the existing one.
*/
fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>): OutgoingRoomKeyRequest?
fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>, fromIndex: Int): OutgoingKeyRequest
fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState)
fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int)
fun updateOutgoingRoomKeyReply(
roomId: String,
sessionId: String,
algorithm: String,
senderKey: String,
fromDevice: String?,
event: Event)
fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest?
fun deleteOutgoingRoomKeyRequest(requestId: String)
fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState)
fun saveGossipingEvent(event: Event) = saveGossipingEvents(listOf(event))
fun saveIncomingKeyRequestAuditTrail(
requestId: String,
roomId: String,
sessionId: String,
senderKey: String,
algorithm: String,
fromUser: String,
fromDevice: String
)
fun saveGossipingEvents(events: List<Event>)
fun saveWithheldAuditTrail(
roomId: String,
sessionId: String,
senderKey: String,
algorithm: String,
code: WithHeldCode,
userId: String,
deviceId: String
)
fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) {
updateGossipingRequestState(
requestUserId = request.userId,
requestDeviceId = request.deviceId,
requestId = request.requestId,
state = state
)
}
fun saveForwardKeyAuditTrail(
roomId: String,
sessionId: String,
senderKey: String,
algorithm: String,
userId: String,
deviceId: String,
chainIndex: Long?
)
fun updateGossipingRequestState(requestUserId: String?,
requestDeviceId: String?,
requestId: String?,
state: GossipingRequestState)
/**
* Search an IncomingRoomKeyRequest
*
* @param userId the user id
* @param deviceId the device id
* @param requestId the request id
* @return an IncomingRoomKeyRequest if it exists, else null
*/
fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest?
fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState)
fun saveIncomingForwardKeyAuditTrail(
roomId: String,
sessionId: String,
senderKey: String,
algorithm: String,
userId: String,
deviceId: String,
chainIndex: Long?
)
fun addNewSessionListener(listener: NewSessionListener)
@ -477,17 +495,15 @@ internal interface IMXCryptoStore {
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
// Dev tools
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest>
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
fun getGossipingEvents(): List<Event>
fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest>
fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>>
fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>>
fun <T> getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData<PagedList<T>>
fun getGossipingEvents(): List<AuditTrail>
fun setDeviceKeysUploaded(uploaded: Boolean)
fun areDeviceKeysUploaded(): Boolean
fun tidyUpDataBase()
fun logDbUsageInfo()
fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest>
}

View file

@ -24,37 +24,40 @@ import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.Sort
import io.realm.kotlin.createObject
import io.realm.kotlin.where
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo
import org.matrix.android.sdk.api.session.crypto.model.IncomingKeyRequestInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.api.session.crypto.model.TrailType
import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.crypto.GossipRequestType
import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailMapper
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper
@ -63,10 +66,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
@ -74,8 +73,8 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSess
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
@ -89,7 +88,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.query.get
import org.matrix.android.sdk.internal.crypto.store.db.query.getById
import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate
import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.tools.RealmDebugTools
import org.matrix.android.sdk.internal.di.CryptoDatabase
import org.matrix.android.sdk.internal.di.DeviceId
@ -106,6 +104,8 @@ import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import javax.inject.Inject
private val loggerTag = LoggerTag("RealmCryptoStore", LoggerTag.CRYPTO)
@SessionScope
internal class RealmCryptoStore @Inject constructor(
@CryptoDatabase private val realmConfiguration: RealmConfiguration,
@ -273,12 +273,13 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun deviceWithIdentityKey(identityKey: String): CryptoDeviceInfo? {
return doWithRealm(realmConfiguration) {
it.where<DeviceInfoEntity>()
.equalTo(DeviceInfoEntityFields.IDENTITY_KEY, identityKey)
.findFirst()
?.let { deviceInfo ->
CryptoMapper.mapToModel(deviceInfo)
return doWithRealm(realmConfiguration) { realm ->
realm.where<DeviceInfoEntity>()
.contains(DeviceInfoEntityFields.KEYS_MAP_JSON, identityKey)
.findAll()
.mapNotNull { CryptoMapper.mapToModel(it) }
.firstOrNull {
it.identityKey() == identityKey
}
}
}
@ -743,14 +744,23 @@ internal class RealmCryptoStore @Inject constructor(
if (sessionIdentifier != null) {
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey)
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
primaryKey = key
sessionId = sessionIdentifier
senderKey = session.senderKey
putInboundGroupSession(session)
}
val existing = realm.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
.findFirst()
realm.insertOrUpdate(realmOlmInboundGroupSession)
if (existing != null) {
// we want to keep the existing backup status
existing.putInboundGroupSession(session)
} else {
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
primaryKey = key
sessionId = sessionIdentifier
senderKey = session.senderKey
putInboundGroupSession(session)
}
realm.insertOrUpdate(realmOlmInboundGroupSession)
}
}
}
}
@ -879,18 +889,33 @@ internal class RealmCryptoStore @Inject constructor(
return
}
doRealmTransaction(realmConfiguration) {
doRealmTransaction(realmConfiguration) { realm ->
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
try {
val sessionIdentifier = olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier()
val key = OlmInboundGroupSessionEntity.createPrimaryKey(
olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier(),
sessionIdentifier,
olmInboundGroupSessionWrapper.senderKey
)
it.where<OlmInboundGroupSessionEntity>()
val existing = realm.where<OlmInboundGroupSessionEntity>()
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
.findFirst()
?.backedUp = true
if (existing != null) {
existing.backedUp = true
} else {
// ... might be in cache but not yet persisted, create a record to persist backedup state
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
primaryKey = key
sessionId = sessionIdentifier
senderKey = olmInboundGroupSessionWrapper.senderKey
putInboundGroupSession(olmInboundGroupSessionWrapper)
backedUp = true
}
realm.insertOrUpdate(realmOlmInboundGroupSession)
}
} catch (e: OlmException) {
Timber.e(e, "OlmException")
}
@ -929,6 +954,18 @@ internal class RealmCryptoStore @Inject constructor(
}
}
override fun enableKeyGossiping(enable: Boolean) {
doRealmTransaction(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.globalEnableKeyGossiping = enable
}
}
override fun isKeyGossipingEnabled(): Boolean {
return doWithRealm(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.globalEnableKeyGossiping
} ?: true
}
override fun getGlobalBlacklistUnverifiedDevices(): Boolean {
return doWithRealm(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.globalBlacklistUnverifiedDevices
@ -1010,12 +1047,13 @@ internal class RealmCryptoStore @Inject constructor(
?: defaultValue
}
override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? {
override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingKeyRequest? {
return monarchy.fetchAllCopiedSync { realm ->
realm.where<OutgoingGossipingRequestEntity>()
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
}.mapNotNull {
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId)
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId)
}.map {
it.toOutgoingKeyRequest()
}.firstOrNull {
it.requestBody?.algorithm == requestBody.algorithm &&
it.requestBody?.roomId == requestBody.roomId &&
@ -1024,41 +1062,37 @@ internal class RealmCryptoStore @Inject constructor(
}
}
override fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? {
override fun getOutgoingRoomKeyRequest(requestId: String): OutgoingKeyRequest? {
return monarchy.fetchAllCopiedSync { realm ->
realm.where<OutgoingGossipingRequestEntity>()
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
.equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName)
}.mapNotNull {
it.toOutgoingGossipingRequest() as? OutgoingSecretRequest
realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
}.map {
it.toOutgoingKeyRequest()
}.firstOrNull()
}
override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
override fun getOutgoingRoomKeyRequest(roomId: String, sessionId: String, algorithm: String, senderKey: String): List<OutgoingKeyRequest> {
// TODO this annoying we have to load all
return monarchy.fetchAllCopiedSync { realm ->
realm.where<IncomingGossipingRequestEntity>()
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
}.mapNotNull {
it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId)
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId)
}.map {
it.toOutgoingKeyRequest()
}.filter {
it.requestBody?.algorithm == algorithm &&
it.requestBody?.senderKey == senderKey
}
}
override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
override fun getGossipingEventsTrail(): LiveData<PagedList<AuditTrail>> {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
realm.where<IncomingGossipingRequestEntity>()
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
.sort(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Sort.DESCENDING)
realm.where<AuditTrailEntity>().sort(AuditTrailEntityFields.AGE_LOCAL_TS, Sort.DESCENDING)
}
val dataSourceFactory = realmDataSourceFactory.map {
it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
?: IncomingRoomKeyRequest(
requestBody = null,
deviceId = "",
userId = "",
requestId = "",
state = GossipingRequestState.NONE,
localCreationTimestamp = 0
)
AuditTrailMapper.map(it)
// mm we can't map not null...
?: createUnknownTrail()
}
return monarchy.findAllPagedWithChanges(
realmDataSourceFactory,
@ -1073,12 +1107,33 @@ internal class RealmCryptoStore @Inject constructor(
)
}
override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> {
private fun createUnknownTrail() = AuditTrail(
clock.epochMillis(),
TrailType.Unknown,
IncomingKeyRequestInfo(
"",
"",
"",
"",
"",
"",
"",
)
)
override fun <T> getGossipingEventsTrail(type: TrailType, mapper: ((AuditTrail) -> T)): LiveData<PagedList<T>> {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
realm.where<GossipingEventEntity>().sort(GossipingEventEntityFields.AGE_LOCAL_TS, Sort.DESCENDING)
realm.where<AuditTrailEntity>()
.equalTo(AuditTrailEntityFields.TYPE, type.name)
.sort(AuditTrailEntityFields.AGE_LOCAL_TS, Sort.DESCENDING)
}
val dataSourceFactory = realmDataSourceFactory.map { it.toModel() }
val trail = monarchy.findAllPagedWithChanges(
val dataSourceFactory = realmDataSourceFactory.map { entity ->
(AuditTrailMapper.map(entity)
// mm we can't map not null...
?: createUnknownTrail()
).let { mapper.invoke(it) }
}
return monarchy.findAllPagedWithChanges(
realmDataSourceFactory,
LivePagedListBuilder(
dataSourceFactory,
@ -1089,28 +1144,36 @@ internal class RealmCryptoStore @Inject constructor(
.build()
)
)
return trail
}
override fun getGossipingEvents(): List<Event> {
override fun getGossipingEvents(): List<AuditTrail> {
return monarchy.fetchAllCopiedSync { realm ->
realm.where<GossipingEventEntity>()
}.map {
it.toModel()
realm.where<AuditTrailEntity>()
}.mapNotNull {
AuditTrailMapper.map(it)
}
}
override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody, recipients: Map<String, List<String>>): OutgoingRoomKeyRequest? {
override fun getOrAddOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody,
recipients: Map<String, List<String>>,
fromIndex: Int): OutgoingKeyRequest {
// Insert the request and return the one passed in parameter
var request: OutgoingRoomKeyRequest? = null
lateinit var request: OutgoingKeyRequest
doRealmTransaction(realmConfiguration) { realm ->
val existing = realm.where<OutgoingGossipingRequestEntity>()
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
val existing = realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId)
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, requestBody.roomId)
.findAll()
.mapNotNull {
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
}.firstOrNull {
.map {
it.toOutgoingKeyRequest()
}.also {
if (it.size > 1) {
// there should be one or zero but not more, worth warning
Timber.tag(loggerTag.value).w("There should not be more than one active key request per session")
}
}
.firstOrNull {
it.requestBody?.algorithm == requestBody.algorithm &&
it.requestBody?.sessionId == requestBody.sessionId &&
it.requestBody?.senderKey == requestBody.senderKey &&
@ -1118,13 +1181,14 @@ internal class RealmCryptoStore @Inject constructor(
}
if (existing == null) {
request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply {
request = realm.createObject(OutgoingKeyRequestEntity::class.java).apply {
this.requestId = RequestIdHelper.createUniqueRequestId()
this.setRecipients(recipients)
this.requestState = OutgoingGossipingRequestState.UNSENT
this.type = GossipRequestType.KEY
this.requestedInfoStr = requestBody.toJson()
}.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
this.requestedIndex = fromIndex
this.requestState = OutgoingRoomKeyRequestState.UNSENT
this.setRequestBody(requestBody)
this.creationTimeStamp = clock.epochMillis()
}.toOutgoingKeyRequest()
} else {
request = existing
}
@ -1132,284 +1196,175 @@ internal class RealmCryptoStore @Inject constructor(
return request
}
override fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest? {
var request: OutgoingSecretRequest? = null
// Insert the request and return the one passed in parameter
override fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) {
doRealmTransaction(realmConfiguration) { realm ->
val existing = realm.where<OutgoingGossipingRequestEntity>()
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
.equalTo(OutgoingGossipingRequestEntityFields.REQUESTED_INFO_STR, secretName)
.findAll()
.mapNotNull {
it.toOutgoingGossipingRequest() as? OutgoingSecretRequest
}.firstOrNull()
if (existing == null) {
request = realm.createObject(OutgoingGossipingRequestEntity::class.java).apply {
this.type = GossipRequestType.SECRET
setRecipients(recipients)
this.requestState = OutgoingGossipingRequestState.UNSENT
this.requestId = RequestIdHelper.createUniqueRequestId()
this.requestedInfoStr = secretName
}.toOutgoingGossipingRequest() as? OutgoingSecretRequest
} else {
request = existing
}
}
return request
}
override fun saveGossipingEvents(events: List<Event>) {
monarchy.writeAsync { realm ->
val now = clock.epochMillis()
events.forEach { event ->
val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
val entity = GossipingEventEntity(
type = event.type,
sender = event.senderId,
ageLocalTs = ageLocalTs,
content = ContentMapper.map(event.content)
).apply {
sendState = SendState.SYNCED
decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
decryptionErrorCode = event.mCryptoError?.name
}
realm.insertOrUpdate(entity)
}
}
}
// override fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest? {
// val statesIndex = states.map { it.ordinal }.toTypedArray()
// return doRealmQueryAndCopy(realmConfiguration) { realm ->
// realm.where<GossipingEventEntity>()
// .equalTo(GossipingEventEntityFields.SENDER, credentials.userId)
// .findAll()
// .filter {entity ->
// states.any { it == entity.requestState}
// }
// }.mapNotNull {
// ContentMapper.map(it.content)?.toModel<OutgoingSecretRequest>()
// }
// ?.toOutgoingRoomKeyRequest()
// }
//
// override fun getOutgoingSecretShareRequestByState(states: Set<ShareRequestState>): OutgoingSecretRequest? {
// val statesIndex = states.map { it.ordinal }.toTypedArray()
// return doRealmQueryAndCopy(realmConfiguration) {
// it.where<OutgoingSecretRequestEntity>()
// .`in`(OutgoingSecretRequestEntityFields.STATE, statesIndex)
// .findFirst()
// }
// ?.toOutgoingSecretRequest()
// }
// override fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) {
// doRealmTransaction(realmConfiguration) {
// val obj = OutgoingRoomKeyRequestEntity().apply {
// requestId = request.requestId
// cancellationTxnId = request.cancellationTxnId
// state = request.state.ordinal
// putRecipients(request.recipients)
// putRequestBody(request.requestBody)
// }
//
// it.insertOrUpdate(obj)
// }
// }
// override fun deleteOutgoingRoomKeyRequest(transactionId: String) {
// doRealmTransaction(realmConfiguration) {
// it.where<OutgoingRoomKeyRequestEntity>()
// .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_ID, transactionId)
// .findFirst()
// ?.deleteFromRealm()
// }
// }
// override fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) {
// if (incomingRoomKeyRequest == null) {
// return
// }
//
// doRealmTransaction(realmConfiguration) {
// // Delete any previous store request with the same parameters
// it.where<IncomingRoomKeyRequestEntity>()
// .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId)
// .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId)
// .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId)
// .findAll()
// .deleteAllFromRealm()
//
// // Then store it
// it.createObject(IncomingRoomKeyRequestEntity::class.java).apply {
// userId = incomingRoomKeyRequest.userId
// deviceId = incomingRoomKeyRequest.deviceId
// requestId = incomingRoomKeyRequest.requestId
// putRequestBody(incomingRoomKeyRequest.requestBody)
// }
// }
// }
// override fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingShareRequestCommon) {
// doRealmTransaction(realmConfiguration) {
// it.where<GossipingEventEntity>()
// .equalTo(GossipingEventEntityFields.TYPE, EventType.ROOM_KEY_REQUEST)
// .notEqualTo(GossipingEventEntityFields.SENDER, credentials.userId)
// .findAll()
// .filter {
// ContentMapper.map(it.content).toModel<IncomingRoomKeyRequest>()?.let {
//
// }
// }
// // .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.userId)
// // .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.deviceId)
// // .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.requestId)
// // .findAll()
// // .deleteAllFromRealm()
// }
// }
override fun updateGossipingRequestState(requestUserId: String?,
requestDeviceId: String?,
requestId: String?,
state: GossipingRequestState) {
doRealmTransaction(realmConfiguration) { realm ->
realm.where<IncomingGossipingRequestEntity>()
.equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, requestUserId)
.equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, requestDeviceId)
.equalTo(IncomingGossipingRequestEntityFields.REQUEST_ID, requestId)
.findAll().forEach {
it.requestState = state
}
}
}
override fun updateOutgoingGossipingRequestState(requestId: String, state: OutgoingGossipingRequestState) {
doRealmTransaction(realmConfiguration) { realm ->
realm.where<OutgoingGossipingRequestEntity>()
.equalTo(OutgoingGossipingRequestEntityFields.REQUEST_ID, requestId)
.findAll().forEach {
it.requestState = state
}
}
}
override fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? {
return doWithRealm(realmConfiguration) { realm ->
realm.where<IncomingGossipingRequestEntity>()
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
.equalTo(IncomingGossipingRequestEntityFields.OTHER_DEVICE_ID, deviceId)
.equalTo(IncomingGossipingRequestEntityFields.OTHER_USER_ID, userId)
.findAll()
.mapNotNull { entity ->
entity.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
}
.firstOrNull()
}
}
override fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
return doWithRealm(realmConfiguration) {
it.where<IncomingGossipingRequestEntity>()
.equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
.equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name)
.findAll()
.map { entity ->
IncomingRoomKeyRequest(
userId = entity.otherUserId,
deviceId = entity.otherDeviceId,
requestId = entity.requestId,
requestBody = entity.getRequestedKeyInfo(),
localCreationTimestamp = entity.localCreationTimestamp
)
}
}
}
override fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon> {
return doWithRealm(realmConfiguration) {
it.where<IncomingGossipingRequestEntity>()
.equalTo(IncomingGossipingRequestEntityFields.REQUEST_STATE_STR, GossipingRequestState.PENDING.name)
.findAll()
.mapNotNull { entity ->
when (entity.type) {
GossipRequestType.KEY -> {
IncomingRoomKeyRequest(
userId = entity.otherUserId,
deviceId = entity.otherDeviceId,
requestId = entity.requestId,
requestBody = entity.getRequestedKeyInfo(),
localCreationTimestamp = entity.localCreationTimestamp
)
}
GossipRequestType.SECRET -> {
IncomingSecretShareRequest(
userId = entity.otherUserId,
deviceId = entity.otherDeviceId,
requestId = entity.requestId,
secretName = entity.getRequestedSecretName(),
localCreationTimestamp = entity.localCreationTimestamp
)
}
realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
.findFirst()?.apply {
this.requestState = newState
if (newState == OutgoingRoomKeyRequestState.UNSENT) {
// clear the old replies
this.replies.deleteAllFromRealm()
}
}
}
}
override fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) {
doRealmTransactionAsync(realmConfiguration) { realm ->
// After a clear cache, we might have a
realm.createObject(IncomingGossipingRequestEntity::class.java).let {
it.otherDeviceId = request.deviceId
it.otherUserId = request.userId
it.requestId = request.requestId ?: ""
it.requestState = GossipingRequestState.PENDING
it.localCreationTimestamp = ageLocalTS ?: clock.epochMillis()
if (request is IncomingSecretShareRequest) {
it.type = GossipRequestType.SECRET
it.requestedInfoStr = request.secretName
} else if (request is IncomingRoomKeyRequest) {
it.type = GossipRequestType.KEY
it.requestedInfoStr = request.requestBody?.toJson()
}
}
}
}
override fun storeIncomingGossipingRequests(requests: List<IncomingShareRequestCommon>) {
doRealmTransactionAsync(realmConfiguration) { realm ->
requests.forEach { request ->
// After a clear cache, we might have a
realm.createObject(IncomingGossipingRequestEntity::class.java).let {
it.otherDeviceId = request.deviceId
it.otherUserId = request.userId
it.requestId = request.requestId ?: ""
it.requestState = GossipingRequestState.PENDING
it.localCreationTimestamp = request.localCreationTimestamp ?: clock.epochMillis()
if (request is IncomingSecretShareRequest) {
it.type = GossipRequestType.SECRET
it.requestedInfoStr = request.secretName
} else if (request is IncomingRoomKeyRequest) {
it.type = GossipRequestType.KEY
it.requestedInfoStr = request.requestBody?.toJson()
override fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) {
doRealmTransaction(realmConfiguration) { realm ->
realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
.findFirst()?.apply {
this.requestedIndex = newIndex
}
}
}
override fun updateOutgoingRoomKeyReply(roomId: String,
sessionId: String,
algorithm: String,
senderKey: String,
fromDevice: String?,
event: Event) {
doRealmTransaction(realmConfiguration) { realm ->
realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId)
.equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId)
.findAll().firstOrNull { entity ->
entity.toOutgoingKeyRequest().let {
it.requestBody?.senderKey == senderKey &&
it.requestBody?.algorithm == algorithm
}
}?.apply {
event.senderId?.let { addReply(it, fromDevice, event) }
}
}
}
override fun deleteOutgoingRoomKeyRequest(requestId: String) {
doRealmTransaction(realmConfiguration) { realm ->
realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId)
.findFirst()?.deleteOnCascade()
}
}
override fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) {
doRealmTransaction(realmConfiguration) { realm ->
realm.where<OutgoingKeyRequestEntity>()
.equalTo(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, state.name)
.findAll()
// I delete like this because I want to cascade delete replies?
.onEach { it.deleteOnCascade() }
}
}
override fun saveIncomingKeyRequestAuditTrail(
requestId: String,
roomId: String,
sessionId: String,
senderKey: String,
algorithm: String,
fromUser: String,
fromDevice: String) {
monarchy.writeAsync { realm ->
val now = clock.epochMillis()
realm.createObject<AuditTrailEntity>().apply {
this.ageLocalTs = now
this.type = TrailType.IncomingKeyRequest.name
val info = IncomingKeyRequestInfo(
roomId = roomId,
sessionId = sessionId,
senderKey = senderKey,
alg = algorithm,
userId = fromUser,
deviceId = fromDevice,
requestId = requestId
)
MoshiProvider.providesMoshi().adapter(IncomingKeyRequestInfo::class.java).toJson(info)?.let {
this.contentJson = it
}
}
}
}
// override fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest> {
// return doRealmQueryAndCopyList(realmConfiguration) {
// it.where<GossipingEventEntity>()
// .findAll()
// }.map {
// it.toIncomingSecretShareRequest()
// }
// }
override fun saveWithheldAuditTrail(roomId: String,
sessionId: String,
senderKey: String,
algorithm: String,
code: WithHeldCode,
userId: String,
deviceId: String) {
monarchy.writeAsync { realm ->
val now = clock.epochMillis()
realm.createObject<AuditTrailEntity>().apply {
this.ageLocalTs = now
this.type = TrailType.OutgoingKeyWithheld.name
val info = WithheldInfo(
roomId = roomId,
sessionId = sessionId,
senderKey = senderKey,
alg = algorithm,
code = code,
userId = userId,
deviceId = deviceId
)
MoshiProvider.providesMoshi().adapter(WithheldInfo::class.java).toJson(info)?.let {
this.contentJson = it
}
}
}
}
override fun saveForwardKeyAuditTrail(roomId: String,
sessionId: String,
senderKey: String,
algorithm: String,
userId: String,
deviceId: String,
chainIndex: Long?) {
saveForwardKeyTrail(roomId, sessionId, senderKey, algorithm, userId, deviceId, chainIndex, false)
}
override fun saveIncomingForwardKeyAuditTrail(roomId: String,
sessionId: String,
senderKey: String,
algorithm: String,
userId: String,
deviceId: String,
chainIndex: Long?) {
saveForwardKeyTrail(roomId, sessionId, senderKey, algorithm, userId, deviceId, chainIndex, true)
}
private fun saveForwardKeyTrail(roomId: String,
sessionId: String,
senderKey: String,
algorithm: String,
userId: String,
deviceId: String,
chainIndex: Long?,
incoming: Boolean
) {
monarchy.writeAsync { realm ->
val now = clock.epochMillis()
realm.createObject<AuditTrailEntity>().apply {
this.ageLocalTs = now
this.type = if (incoming) TrailType.IncomingKeyForward.name else TrailType.OutgoingKeyForward.name
val info = ForwardInfo(
roomId = roomId,
sessionId = sessionId,
senderKey = senderKey,
alg = algorithm,
userId = userId,
deviceId = deviceId,
chainIndex = chainIndex
)
MoshiProvider.providesMoshi().adapter(ForwardInfo::class.java).toJson(info)?.let {
this.contentJson = it
}
}
}
}
/* ==========================================================================================
* Cross Signing
@ -1513,37 +1468,34 @@ internal class RealmCryptoStore @Inject constructor(
}
}
override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> {
override fun getOutgoingRoomKeyRequests(): List<OutgoingKeyRequest> {
return monarchy.fetchAllMappedSync({ realm ->
realm
.where(OutgoingGossipingRequestEntity::class.java)
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
.where(OutgoingKeyRequestEntity::class.java)
}, { entity ->
entity.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
entity.toOutgoingKeyRequest()
})
.filterNotNull()
}
override fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest> {
override fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest> {
return monarchy.fetchAllMappedSync({ realm ->
realm
.where(OutgoingGossipingRequestEntity::class.java)
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.SECRET.name)
.where(OutgoingKeyRequestEntity::class.java)
.`in`(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, inStates.map { it.name }.toTypedArray())
}, { entity ->
entity.toOutgoingGossipingRequest() as? OutgoingSecretRequest
entity.toOutgoingKeyRequest()
})
.filterNotNull()
}
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingKeyRequest>> {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
realm
.where(OutgoingGossipingRequestEntity::class.java)
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
.where(OutgoingKeyRequestEntity::class.java)
}
val dataSourceFactory = realmDataSourceFactory.map {
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
?: OutgoingRoomKeyRequest(requestBody = null, requestId = "?", recipients = emptyMap(), state = OutgoingGossipingRequestState.CANCELLED)
it.toOutgoingKeyRequest()
}
val trail = monarchy.findAllPagedWithChanges(
realmDataSourceFactory,
@ -1722,26 +1674,20 @@ internal class RealmCryptoStore @Inject constructor(
val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000
doRealmTransaction(realmConfiguration) { realm ->
// Only keep one week history
realm.where<IncomingGossipingRequestEntity>()
.lessThan(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, prevWeekTs)
// Clean the old ones?
realm.where<OutgoingKeyRequestEntity>()
.lessThan(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, prevWeekTs)
.findAll()
.also { Timber.i("## Crypto Clean up ${it.size} IncomingGossipingRequestEntity") }
.also { Timber.i("## Crypto Clean up ${it.size} OutgoingKeyRequestEntity") }
.deleteAllFromRealm()
// Clean the cancelled ones?
realm.where<OutgoingGossipingRequestEntity>()
.equalTo(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, OutgoingGossipingRequestState.CANCELLED.name)
.equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
.findAll()
.also { Timber.i("## Crypto Clean up ${it.size} OutgoingGossipingRequestEntity") }
.deleteAllFromRealm()
// Only keep one month history
// Only keep one week history
realm.where<GossipingEventEntity>()
.lessThan(GossipingEventEntityFields.AGE_LOCAL_TS, prevWeekTs)
val prevMonthTs = clock.epochMillis() - 4 * 7 * 24 * 60 * 60 * 1_000L
realm.where<AuditTrailEntity>()
.lessThan(AuditTrailEntityFields.AGE_LOCAL_TS, prevMonthTs)
.findAll()
.also { Timber.i("## Crypto Clean up ${it.size} GossipingEventEntityFields") }
.also { Timber.i("## Crypto Clean up ${it.size} AuditTrailEntity") }
.deleteAllFromRealm()
// Can we do something for WithHeldSessionEntity?

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

View file

@ -17,19 +17,19 @@
package org.matrix.android.sdk.internal.crypto.store.db
import io.realm.annotations.RealmModule
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.KeyRequestReplyEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
@ -51,9 +51,9 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEnti
KeyInfoEntity::class,
CrossSigningInfoEntity::class,
TrustLevelEntity::class,
GossipingEventEntity::class,
IncomingGossipingRequestEntity::class,
OutgoingGossipingRequestEntity::class,
AuditTrailEntity::class,
OutgoingKeyRequestEntity::class,
KeyRequestReplyEntity::class,
MyDeviceLastSeenInfoEntity::class,
WithHeldSessionEntity::class,
SharedSessionEntity::class,

View file

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

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.store.db.migration
import io.realm.DynamicRealm
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.KeyRequestReplyEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingKeyRequestEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator
internal class MigrateCryptoTo016(realm: DynamicRealm) : RealmMigrator(realm, 16) {
override fun doMigrate(realm: DynamicRealm) {
realm.schema.remove("OutgoingGossipingRequestEntity")
realm.schema.remove("IncomingGossipingRequestEntity")
realm.schema.remove("GossipingEventEntity")
// No need to migrate existing request, just start fresh
val replySchema = realm.schema.create("KeyRequestReplyEntity")
.addField(KeyRequestReplyEntityFields.SENDER_ID, String::class.java)
.addField(KeyRequestReplyEntityFields.FROM_DEVICE, String::class.java)
.addField(KeyRequestReplyEntityFields.EVENT_JSON, String::class.java)
realm.schema.create("OutgoingKeyRequestEntity")
.addField(OutgoingKeyRequestEntityFields.REQUEST_ID, String::class.java)
.addIndex(OutgoingKeyRequestEntityFields.REQUEST_ID)
.addField(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, String::class.java)
.addIndex(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID)
.addRealmListField(OutgoingKeyRequestEntityFields.REPLIES.`$`, replySchema)
.addField(OutgoingKeyRequestEntityFields.RECIPIENTS_DATA, String::class.java)
.addField(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, String::class.java)
.addIndex(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR)
.addField(OutgoingKeyRequestEntityFields.REQUESTED_INFO_STR, String::class.java)
.addField(OutgoingKeyRequestEntityFields.ROOM_ID, String::class.java)
.addIndex(OutgoingKeyRequestEntityFields.ROOM_ID)
.addField(OutgoingKeyRequestEntityFields.REQUESTED_INDEX, Integer::class.java)
.addField(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, Long::class.java)
.setNullable(OutgoingKeyRequestEntityFields.CREATION_TIME_STAMP, true)
realm.schema.create("AuditTrailEntity")
.addField(AuditTrailEntityFields.AGE_LOCAL_TS, Long::class.java)
.setNullable(AuditTrailEntityFields.AGE_LOCAL_TS, true)
.addField(AuditTrailEntityFields.CONTENT_JSON, String::class.java)
.addField(AuditTrailEntityFields.TYPE, String::class.java)
.addIndex(AuditTrailEntityFields.TYPE)
realm.schema.get("CryptoMetadataEntity")
?.addField(CryptoMetadataEntityFields.GLOBAL_ENABLE_KEY_GOSSIPING, Boolean::class.java)
?.transform {
// set the default value to true
it.setBoolean(CryptoMetadataEntityFields.GLOBAL_ENABLE_KEY_GOSSIPING, true)
}
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,18 +14,15 @@
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.crypto.model
package org.matrix.android.sdk.internal.crypto.store.db.model
enum class GossipingRequestState {
NONE,
PENDING,
REJECTED,
ACCEPTING,
ACCEPTED,
FAILED_TO_ACCEPTED,
import io.realm.RealmObject
import io.realm.annotations.Index
// USER_REJECTED,
UNABLE_TO_PROCESS,
CANCELLED_BY_REQUESTER,
RE_REQUESTED
internal open class AuditTrailEntity(
var ageLocalTs: Long? = null,
@Index var type: String? = null,
var contentJson: String? = null
) : RealmObject() {
companion object
}

View file

@ -0,0 +1,82 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.store.db.model
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo
import org.matrix.android.sdk.api.session.crypto.model.IncomingKeyRequestInfo
import org.matrix.android.sdk.api.session.crypto.model.TrailType
import org.matrix.android.sdk.api.session.crypto.model.UnknownInfo
import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo
import org.matrix.android.sdk.internal.di.MoshiProvider
internal object AuditTrailMapper {
fun map(entity: AuditTrailEntity): AuditTrail? {
val contentJson = entity.contentJson ?: return null
return when (entity.type) {
TrailType.OutgoingKeyForward.name -> {
val info = tryOrNull {
MoshiProvider.providesMoshi().adapter(ForwardInfo::class.java).fromJson(contentJson)
} ?: return null
AuditTrail(
ageLocalTs = entity.ageLocalTs ?: 0,
type = TrailType.OutgoingKeyForward,
info = info
)
}
TrailType.OutgoingKeyWithheld.name -> {
val info = tryOrNull {
MoshiProvider.providesMoshi().adapter(WithheldInfo::class.java).fromJson(contentJson)
} ?: return null
AuditTrail(
ageLocalTs = entity.ageLocalTs ?: 0,
type = TrailType.OutgoingKeyWithheld,
info = info
)
}
TrailType.IncomingKeyRequest.name -> {
val info = tryOrNull {
MoshiProvider.providesMoshi().adapter(IncomingKeyRequestInfo::class.java).fromJson(contentJson)
} ?: return null
AuditTrail(
ageLocalTs = entity.ageLocalTs ?: 0,
type = TrailType.IncomingKeyRequest,
info = info
)
}
TrailType.IncomingKeyForward.name -> {
val info = tryOrNull {
MoshiProvider.providesMoshi().adapter(ForwardInfo::class.java).fromJson(contentJson)
} ?: return null
AuditTrail(
ageLocalTs = entity.ageLocalTs ?: 0,
type = TrailType.IncomingKeyForward,
info = info
)
}
else -> {
AuditTrail(
ageLocalTs = entity.ageLocalTs ?: 0,
type = TrailType.Unknown,
info = UnknownInfo
)
}
}
}
}

View file

@ -33,6 +33,8 @@ internal open class CryptoMetadataEntity(
var deviceSyncToken: String? = null,
// Settings for blacklisting unverified devices.
var globalBlacklistUnverifiedDevices: Boolean = false,
// setting to enable or disable key gossiping
var globalEnableKeyGossiping: Boolean = true,
// The keys backup version currently used. Null means no backup.
var backupVersion: String? = null,

View file

@ -1,88 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.store.db.model
import com.squareup.moshi.JsonDataException
import io.realm.RealmObject
import io.realm.annotations.Index
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.di.MoshiProvider
import timber.log.Timber
/**
* Keep track of gossiping event received in toDevice messages
* (room key request, or sss secret sharing, as well as cancellations)
*
*/
internal open class GossipingEventEntity(@Index var type: String? = "",
var content: String? = null,
@Index var sender: String? = null,
var decryptionResultJson: String? = null,
var decryptionErrorCode: String? = null,
var ageLocalTs: Long? = null) : RealmObject() {
private var sendStateStr: String = SendState.UNKNOWN.name
var sendState: SendState
get() {
return SendState.valueOf(sendStateStr)
}
set(value) {
sendStateStr = value.name
}
companion object
fun setDecryptionResult(result: MXEventDecryptionResult) {
val decryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java)
decryptionResultJson = adapter.toJson(decryptionResult)
decryptionErrorCode = null
}
fun toModel(): Event {
return Event(
type = this.type ?: "",
content = ContentMapper.map(this.content),
senderId = this.sender
).also {
it.ageLocalTs = this.ageLocalTs
it.sendState = this.sendState
this.decryptionResultJson?.let { json ->
try {
it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json)
} catch (t: JsonDataException) {
Timber.e(t, "Failed to parse decryption result")
}
}
// TODO get the full crypto error object
it.mCryptoError = this.decryptionErrorCode?.let { errorCode ->
MXCryptoError.ErrorType.valueOf(errorCode)
}
}
}
}

View file

@ -1,89 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.store.db.model
import io.realm.RealmObject
import io.realm.annotations.Index
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.internal.crypto.GossipRequestType
import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
internal open class IncomingGossipingRequestEntity(@Index var requestId: String? = "",
@Index var typeStr: String? = null,
var otherUserId: String? = null,
var requestedInfoStr: String? = null,
var otherDeviceId: String? = null,
var localCreationTimestamp: Long? = null
) : RealmObject() {
fun getRequestedSecretName(): String? = if (type == GossipRequestType.SECRET) {
requestedInfoStr
} else null
fun getRequestedKeyInfo(): RoomKeyRequestBody? = if (type == GossipRequestType.KEY) {
RoomKeyRequestBody.fromJson(requestedInfoStr)
} else null
var type: GossipRequestType
get() {
return tryOrNull { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY
}
set(value) {
typeStr = value.name
}
private var requestStateStr: String = GossipingRequestState.NONE.name
var requestState: GossipingRequestState
get() {
return tryOrNull { GossipingRequestState.valueOf(requestStateStr) }
?: GossipingRequestState.NONE
}
set(value) {
requestStateStr = value.name
}
companion object
fun toIncomingGossipingRequest(): IncomingShareRequestCommon {
return when (type) {
GossipRequestType.KEY -> {
IncomingRoomKeyRequest(
requestBody = getRequestedKeyInfo(),
deviceId = otherDeviceId,
userId = otherUserId,
requestId = requestId,
state = requestState,
localCreationTimestamp = localCreationTimestamp
)
}
GossipRequestType.SECRET -> {
IncomingSecretShareRequest(
secretName = getRequestedSecretName(),
deviceId = otherDeviceId,
userId = otherUserId,
requestId = requestId,
localCreationTimestamp = localCreationTimestamp
)
}
}
}
}

View file

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

View file

@ -1,104 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.store.db.model
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Types
import io.realm.RealmObject
import io.realm.annotations.Index
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.internal.crypto.GossipRequestType
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest
import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest
import org.matrix.android.sdk.internal.di.MoshiProvider
internal open class OutgoingGossipingRequestEntity(
@Index var requestId: String? = null,
var recipientsData: String? = null,
var requestedInfoStr: String? = null,
@Index var typeStr: String? = null
) : RealmObject() {
fun getRequestedSecretName(): String? = if (type == GossipRequestType.SECRET) {
requestedInfoStr
} else null
fun getRequestedKeyInfo(): RoomKeyRequestBody? = if (type == GossipRequestType.KEY) {
RoomKeyRequestBody.fromJson(requestedInfoStr)
} else null
var type: GossipRequestType
get() {
return tryOrNull { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY
}
set(value) {
typeStr = value.name
}
private var requestStateStr: String = OutgoingGossipingRequestState.UNSENT.name
var requestState: OutgoingGossipingRequestState
get() {
return tryOrNull { OutgoingGossipingRequestState.valueOf(requestStateStr) }
?: OutgoingGossipingRequestState.UNSENT
}
set(value) {
requestStateStr = value.name
}
companion object {
private val recipientsDataMapper: JsonAdapter<Map<String, List<String>>> =
MoshiProvider
.providesMoshi()
.adapter<Map<String, List<String>>>(
Types.newParameterizedType(Map::class.java, String::class.java, List::class.java)
)
}
fun toOutgoingGossipingRequest(): OutgoingGossipingRequest {
return when (type) {
GossipRequestType.KEY -> {
OutgoingRoomKeyRequest(
requestBody = getRequestedKeyInfo(),
recipients = getRecipients().orEmpty(),
requestId = requestId ?: "",
state = requestState
)
}
GossipRequestType.SECRET -> {
OutgoingSecretRequest(
secretName = getRequestedSecretName(),
recipients = getRecipients().orEmpty(),
requestId = requestId ?: "",
state = requestState
)
}
}
}
private fun getRecipients(): Map<String, List<String>>? {
return this.recipientsData?.let { recipientsDataMapper.fromJson(it) }
}
fun setRecipients(recipients: Map<String, List<String>>) {
this.recipientsData = recipientsDataMapper.toJson(recipients)
}
}

View file

@ -0,0 +1,136 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.store.db.model
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Types
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.Index
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
import org.matrix.android.sdk.api.session.crypto.RequestReply
import org.matrix.android.sdk.api.session.crypto.RequestResult
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.internal.di.MoshiProvider
internal open class OutgoingKeyRequestEntity(
@Index var requestId: String? = null,
var requestedIndex: Int? = null,
var recipientsData: String? = null,
var requestedInfoStr: String? = null,
var creationTimeStamp: Long? = null,
// de-normalization for better query (if not have to query all and parse json)
@Index var roomId: String? = null,
@Index var megolmSessionId: String? = null,
var replies: RealmList<KeyRequestReplyEntity> = RealmList()
) : RealmObject() {
@Index private var requestStateStr: String = OutgoingRoomKeyRequestState.UNSENT.name
companion object {
private val recipientsDataMapper: JsonAdapter<Map<String, List<String>>> =
MoshiProvider
.providesMoshi()
.adapter(
Types.newParameterizedType(Map::class.java, String::class.java, List::class.java)
)
}
private fun getRequestedKeyInfo(): RoomKeyRequestBody? = RoomKeyRequestBody.fromJson(requestedInfoStr)
fun setRequestBody(body: RoomKeyRequestBody) {
requestedInfoStr = body.toJson()
roomId = body.roomId
megolmSessionId = body.sessionId
}
var requestState: OutgoingRoomKeyRequestState
get() {
return tryOrNull { OutgoingRoomKeyRequestState.valueOf(requestStateStr) }
?: OutgoingRoomKeyRequestState.UNSENT
}
set(value) {
requestStateStr = value.name
}
private fun getRecipients(): Map<String, List<String>>? {
return this.recipientsData?.let { recipientsDataMapper.fromJson(it) }
}
fun setRecipients(recipients: Map<String, List<String>>) {
this.recipientsData = recipientsDataMapper.toJson(recipients)
}
fun addReply(userId: String, fromDevice: String?, event: Event) {
val newReply = KeyRequestReplyEntity(
senderId = userId,
fromDevice = fromDevice,
eventJson = MoshiProvider.providesMoshi().adapter(Event::class.java).toJson(event)
)
replies.add(newReply)
}
fun toOutgoingKeyRequest(): OutgoingKeyRequest {
return OutgoingKeyRequest(
requestBody = getRequestedKeyInfo(),
recipients = getRecipients().orEmpty(),
requestId = requestId ?: "",
fromIndex = requestedIndex ?: 0,
state = requestState,
results = replies.mapNotNull { entity ->
val userId = entity.senderId ?: return@mapNotNull null
val result = entity.eventJson?.let {
MoshiProvider.providesMoshi().adapter(Event::class.java).fromJson(it)
}?.let { event ->
eventToResult(event)
} ?: return@mapNotNull null
RequestReply(
userId = userId,
fromDevice = entity.fromDevice,
result = result
)
}
)
}
private fun eventToResult(event: Event): RequestResult? {
return when (event.getClearType()) {
EventType.ROOM_KEY_WITHHELD -> {
event.content.toModel<RoomKeyWithHeldContent>()?.code?.let {
RequestResult.Failure(it)
}
}
EventType.FORWARDED_ROOM_KEY -> {
RequestResult.Success((event.content?.get("chain_index") as? Number)?.toInt() ?: 0)
}
else -> null
}
}
}
internal fun OutgoingKeyRequestEntity.deleteOnCascade() {
replies.deleteAllFromRealm()
deleteFromRealm()
}

View file

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

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

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

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

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

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

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

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

View file

@ -1,88 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.verification
import android.content.Context
import androidx.work.Data
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import timber.log.Timber
import javax.inject.Inject
/**
* Possible previous worker: None
* Possible next worker : None
*/
internal class SendVerificationMessageWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) :
SessionSafeCoroutineWorker<SendVerificationMessageWorker.Params>(context, params, sessionManager, Params::class.java) {
@JsonClass(generateAdapter = true)
internal data class Params(
override val sessionId: String,
val eventId: String,
override val lastFailureMessage: String? = null
) : SessionWorkerParams
@Inject lateinit var sendVerificationMessageTask: SendVerificationMessageTask
@Inject lateinit var localEchoRepository: LocalEchoRepository
@Inject lateinit var cancelSendTracker: CancelSendTracker
override fun injectWith(injector: SessionComponent) {
injector.inject(this)
}
override suspend fun doSafeWork(params: Params): Result {
val localEvent = localEchoRepository.getUpToDateEcho(params.eventId) ?: return buildErrorResult(params, "Event not found")
val localEventId = localEvent.eventId ?: ""
val roomId = localEvent.roomId ?: ""
if (cancelSendTracker.isCancelRequestedFor(localEventId, roomId)) {
return Result.success()
.also {
cancelSendTracker.markCancelled(localEventId, roomId)
Timber.e("## SendEvent: Event sending has been cancelled $localEventId")
}
}
return try {
val resultEventId = sendVerificationMessageTask.execute(
SendVerificationMessageTask.Params(
event = localEvent
)
)
Result.success(Data.Builder().putString(localEventId, resultEventId).build())
} catch (throwable: Throwable) {
if (throwable.shouldBeRetried()) {
Result.retry()
} else {
buildErrorResult(params, throwable.localizedMessage ?: "error")
}
}
}
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
}

View file

@ -15,13 +15,9 @@
*/
package org.matrix.android.sdk.internal.crypto.verification
import io.realm.Realm
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.LocalEcho
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
@ -29,22 +25,18 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationReadyContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent
import org.matrix.android.sdk.internal.crypto.EventDecryptor
import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.di.DeviceId
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
import javax.inject.Inject
internal class VerificationMessageProcessor @Inject constructor(
private val eventDecryptor: EventDecryptor,
private val clock: Clock,
private val verificationService: DefaultVerificationService,
@UserId private val userId: String,
@DeviceId private val deviceId: String?
) : EventInsertLiveProcessor {
@DeviceId private val deviceId: String?,
private val clock: Clock,
) {
private val transactionsHandledByOtherDevice = ArrayList<String>()
@ -60,40 +52,20 @@ internal class VerificationMessageProcessor @Inject constructor(
EventType.ENCRYPTED
)
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
if (insertType != EventInsertType.INCREMENTAL_SYNC) {
return false
}
return allowedTypes.contains(eventType) && !LocalEcho.isLocalEchoId(eventId)
fun shouldProcess(eventType: String): Boolean {
return allowedTypes.contains(eventType)
}
override suspend fun process(realm: Realm, event: Event) {
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
suspend fun process(event: Event) {
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.getClearType()} from ${event.senderId}")
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
// the message should be ignored by the receiver.
if (!VerificationService.isValidRequest(event.ageLocalTs ?: event.originServerTs, clock.epochMillis())) return Unit.also {
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated")
if (event.ageLocalTs != null && !VerificationService.isValidRequest(event.ageLocalTs, clock.epochMillis())) return Unit.also {
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated age:$event.ageLocalTs ms")
}
// decrypt if needed?
if (event.isEncrypted() && event.mxDecryptionResult == null) {
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
// for now decrypt sync
try {
val result = eventDecryptor.decryptEvent(event, "")
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event)
}
}
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
// Relates to is not encrypted
@ -102,7 +74,6 @@ internal class VerificationMessageProcessor @Inject constructor(
if (event.senderId == userId) {
// If it's send from me, we need to keep track of Requests or Start
// done from another device of mine
if (EventType.MESSAGE == event.getClearType()) {
val msgType = event.getClearContent().toModel<MessageContent>()?.msgType
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
@ -137,6 +108,8 @@ internal class VerificationMessageProcessor @Inject constructor(
transactionsHandledByOtherDevice.remove(it)
verificationService.onRoomRequestHandledByOtherDevice(event)
}
} else if (EventType.ENCRYPTED == event.getClearType()) {
verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event)
}
Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}")

View file

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

View file

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

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

View file

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

View file

@ -49,7 +49,6 @@ import org.matrix.android.sdk.api.util.md5
import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask
import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor
import org.matrix.android.sdk.internal.database.EventInsertLiveObserver
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory
@ -318,10 +317,6 @@ internal abstract class SessionModule {
@IntoSet
abstract fun bindRoomCreateEventProcessor(processor: RoomCreateEventProcessor): EventInsertLiveProcessor
@Binds
@IntoSet
abstract fun bindVerificationMessageProcessor(processor: VerificationMessageProcessor): EventInsertLiveProcessor
@Binds
@IntoSet
abstract fun bindCallEventProcessor(processor: CallEventProcessor): EventInsertLiveProcessor

View file

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

View file

@ -382,7 +382,10 @@ internal class RoomSyncHandler @Inject constructor(
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
for (event in eventList) {
for (rawEvent in eventList) {
// It's annoying roomId is not there, but lot of code rely on it.
// And had to do it now as copy would delete all decryption results..
val event = rawEvent.copy(roomId = roomId)
if (event.eventId == null || event.senderId == null || event.type == null) {
continue
}
@ -454,7 +457,7 @@ internal class RoomSyncHandler @Inject constructor(
}
}
// Give info to crypto module
cryptoService.onLiveEvent(roomEntity.roomId, event)
cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync)
// Try to remove local echo
event.unsignedData?.transactionId?.also {

View file

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

View file

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

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
@ -149,15 +148,8 @@ class BackupToQuadSMigrationTask @Inject constructor(
// save for gossiping
keysBackupService.saveBackupRecoveryKey(recoveryKey, version.version)
// while we are there let's restore, but do not block
session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
version,
recoveryKey,
null,
null,
null,
NoOpMatrixCallback()
)
// It's not a good idea to download the full backup, it might take very long
// and use a lot of resources
return Result.Success
} catch (failure: Throwable) {

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
@ -424,6 +423,11 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
}
private fun tentativeRestoreBackup(res: Map<String, String>?) {
// It's not a good idea to download the full backup, it might take very long
// and use a lot of resources
// Just check that the key is valid and store it, the backup will be used megolm session per
// megolm session when an UISI is encountered
viewModelScope.launch(Dispatchers.IO) {
try {
val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also {
@ -434,17 +438,13 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
session.cryptoService().keysBackupService().getCurrentVersion(it)
}.toKeysVersionResult() ?: return@launch
awaitCallback<ImportRoomKeysResult> {
session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
version,
computeRecoveryKey(secret.fromBase64()),
null,
null,
null,
it
)
val recoveryKey = computeRecoveryKey(secret.fromBase64())
val isValid = awaitCallback<Boolean> {
session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(recoveryKey, it)
}
if (isValid) {
session.cryptoService().keysBackupService().saveBackupRecoveryKey(recoveryKey, version.version)
}
awaitCallback<Unit> {
session.cryptoService().keysBackupService().trustKeysBackupVersion(version, true, it)
}

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.api.session.crypto.model.AuditTrail
import javax.inject.Inject
class GossipingEventsPaperTrailFragment @Inject constructor(
@ -64,17 +62,17 @@ class GossipingEventsPaperTrailFragment @Inject constructor(
super.onDestroyView()
}
override fun didTap(event: Event) {
if (event.isEncrypted()) {
event.toClearContentStringWithIndent()
} else {
event.toContentStringWithIndent()
}?.let {
JSonViewerDialog.newInstance(
it,
-1,
createJSonViewerStyleProvider(colorProvider)
).show(childFragmentManager, "JSON_VIEWER")
}
override fun didTap(event: AuditTrail) {
// if (event.isEncrypted()) {
// event.toClearContentStringWithIndent()
// } else {
// event.toContentStringWithIndent()
// }?.let {
// JSonViewerDialog.newInstance(
// it,
// -1,
// createJSonViewerStyleProvider(colorProvider)
// ).show(childFragmentManager, "JSON_VIEWER")
// }
}
}

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.api.session.crypto.model.AuditTrail
data class GossipingEventsPaperTrailState(
val events: Async<PagedList<Event>> = Uninitialized
val events: Async<PagedList<AuditTrail>> = Uninitialized
) : MavericksState
class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted initialState: GossipingEventsPaperTrailState,

View file

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

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,75 @@ import im.vector.app.core.ui.list.GenericItem_
import im.vector.app.core.utils.createUIHandler
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import me.gujun.android.span.span
import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent
import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.crypto.model.AuditTrail
import org.matrix.android.sdk.api.session.crypto.model.ForwardInfo
import org.matrix.android.sdk.api.session.crypto.model.TrailType
import org.matrix.android.sdk.api.session.crypto.model.WithheldInfo
import javax.inject.Inject
class GossipingTrailPagedEpoxyController @Inject constructor(
private val vectorDateFormatter: VectorDateFormatter,
private val colorProvider: ColorProvider
) : PagedListEpoxyController<Event>(
) : PagedListEpoxyController<AuditTrail>(
// Important it must match the PageList builder notify Looper
modelBuildingHandler = createUIHandler()
) {
interface InteractionListener {
fun didTap(event: Event)
fun didTap(event: AuditTrail)
}
var interactionListener: InteractionListener? = null
override fun buildItemModel(currentPosition: Int, item: Event?): EpoxyModel<*> {
override fun buildItemModel(currentPosition: Int, item: AuditTrail?): EpoxyModel<*> {
val host = this
val event = item ?: return GenericItem_().apply { id(currentPosition) }
return GenericItem_().apply {
id(event.hashCode())
itemClickAction { host.interactionListener?.didTap(event) }
title(
if (event.isEncrypted()) {
"${event.getClearType()} [encrypted]"
} else {
event.type
}?.toEpoxyCharSequence()
)
title(event.type.name.toEpoxyCharSequence())
description(
span {
+host.vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
span("\nfrom: ") {
span("\n${host.senderFromTo(event.type)}: ") {
textStyle = "bold"
}
+"${event.senderId}"
+"${event.info.userId}|${event.info.deviceId}"
span("\nroomId: ") {
textStyle = "bold"
}
+event.info.roomId
span("\nsessionId: ") {
textStyle = "bold"
}
+event.info.sessionId
apply {
if (event.getClearType() == EventType.ROOM_KEY_REQUEST) {
val content = event.getClearContent().toModel<RoomKeyShareRequest>()
span("\nreqId:") {
textStyle = "bold"
}
+" ${content?.requestId}"
span("\naction:") {
textStyle = "bold"
}
+" ${content?.action}"
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
span("\nsessionId:") {
when (event.type) {
TrailType.OutgoingKeyForward -> {
val fInfo = event.info as ForwardInfo
span("\nchainIndex: ") {
textStyle = "bold"
}
+" ${content.body?.sessionId}"
+"${fInfo.chainIndex}"
}
span("\nrequestedBy: ") {
textStyle = "bold"
}
+"${content?.requestingDeviceId}"
} else if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
val encryptedContent = event.content.toModel<OlmEventContent>()
val content = event.getClearContent().toModel<ForwardedRoomKeyContent>()
if (event.mxDecryptionResult == null) {
span("**Failed to Decrypt** ${event.mCryptoError}") {
textColor = host.colorProvider.getColorFromAttribute(R.attr.colorError)
}
}
span("\nsessionId:") {
textStyle = "bold"
}
+" ${content?.sessionId}"
span("\nFrom Device (sender key):") {
textStyle = "bold"
}
+" ${encryptedContent?.senderKey}"
} else if (event.getClearType() == EventType.ROOM_KEY) {
// it's a bit of a fake event for trail reasons
val content = event.getClearContent()
span("\nsessionId:") {
textStyle = "bold"
}
+" ${content?.get("session_id")}"
span("\nroomId:") {
textStyle = "bold"
}
+" ${content?.get("room_id")}"
span("\nTo :") {
textStyle = "bold"
}
+" ${content?.get("_dest") ?: "me"}"
} else if (event.getClearType() == EventType.SEND_SECRET) {
val content = event.getClearContent().toModel<SecretSendEventContent>()
span("\nrequestId:") {
textStyle = "bold"
}
+" ${content?.requestId}"
span("\nFrom Device:") {
textStyle = "bold"
}
+" ${event.mxDecryptionResult?.payload?.get("sender_device")}"
} else if (event.getClearType() == EventType.REQUEST_SECRET) {
val content = event.getClearContent().toModel<SecretShareRequest>()
span("\nreqId:") {
textStyle = "bold"
}
+" ${content?.requestId}"
span("\naction:") {
textStyle = "bold"
}
+" ${content?.action}"
if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
span("\nsecretName:") {
TrailType.OutgoingKeyWithheld -> {
val fInfo = event.info as WithheldInfo
span("\ncode: ") {
textStyle = "bold"
}
+" ${content.secretName}"
+"${fInfo.code}"
}
span("\nrequestedBy: ") {
textStyle = "bold"
TrailType.IncomingKeyRequest -> {
// no additional info
}
+"${content?.requestingDeviceId}"
} else if (event.getClearType() == EventType.ENCRYPTED) {
span("**Failed to Decrypt** ${event.mCryptoError}") {
textColor = host.colorProvider.getColorFromAttribute(R.attr.colorError)
TrailType.IncomingKeyForward -> {
val fInfo = event.info as ForwardInfo
span("\nchainIndex: ") {
textStyle = "bold"
}
+"${fInfo.chainIndex}"
}
TrailType.Unknown -> {
}
}
}
@ -164,4 +101,14 @@ class GossipingTrailPagedEpoxyController @Inject constructor(
)
}
}
private fun senderFromTo(type: TrailType): String {
return when (type) {
TrailType.OutgoingKeyWithheld,
TrailType.OutgoingKeyForward -> "to"
TrailType.IncomingKeyRequest,
TrailType.IncomingKeyForward -> "from"
TrailType.Unknown -> ""
}
}
}

View file

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

View file

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

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.api.session.crypto.OutgoingKeyRequest
import javax.inject.Inject
class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyController<OutgoingRoomKeyRequest>(
class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyController<OutgoingKeyRequest>(
// Important it must match the PageList builder notify Looper
modelBuildingHandler = createUIHandler()
) {
@ -36,7 +36,7 @@ class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyCo
var interactionListener: InteractionListener? = null
override fun buildItemModel(currentPosition: Int, item: OutgoingRoomKeyRequest?): EpoxyModel<*> {
override fun buildItemModel(currentPosition: Int, item: OutgoingKeyRequest?): EpoxyModel<*> {
val roomKeyRequest = item ?: return GenericItem_().apply { id(currentPosition) }
return GenericItem_().apply {

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