Merge branch 'main' into develop

This commit is contained in:
Valere 2022-09-28 18:16:39 +02:00
commit ce86e29122
45 changed files with 1018 additions and 243 deletions

View file

@ -1,3 +1,12 @@
Changes in Element v1.5.1 (2022-09-28)
======================================
Security ⚠️
----------
This update provides important security fixes, update now.
Ref: CVE-2022-39246 CVE-2022-39248
Changes in Element v1.5.0 (2022-09-23) Changes in Element v1.5.0 (2022-09-23)
====================================== ======================================

View file

@ -2617,6 +2617,7 @@
<string name="unencrypted">Unencrypted</string> <string name="unencrypted">Unencrypted</string>
<string name="encrypted_unverified">Encrypted by an unverified device</string> <string name="encrypted_unverified">Encrypted by an unverified device</string>
<string name="key_authenticity_not_guaranteed">The authenticity of this encrypted message can\'t be guaranteed on this device.</string>
<string name="review_logins">Review where youre logged in</string> <string name="review_logins">Review where youre logged in</string>
<string name="verify_other_sessions">Verify all your sessions to ensure your account &amp; messages are safe</string> <string name="verify_other_sessions">Verify all your sessions to ensure your account &amp; messages are safe</string>
<!-- Argument will be replaced by the other session name (e.g, Desktop, mobile) --> <!-- Argument will be replaced by the other session name (e.g, Desktop, mobile) -->

View file

@ -143,6 +143,7 @@
<color name="shield_color_trust">#0DBD8B</color> <color name="shield_color_trust">#0DBD8B</color>
<color name="shield_color_trust_background">#0F0DBD8B</color> <color name="shield_color_trust_background">#0F0DBD8B</color>
<color name="shield_color_black">#17191C</color> <color name="shield_color_black">#17191C</color>
<color name="shield_color_gray">#91A1C0</color>
<color name="shield_color_warning">#FF4B55</color> <color name="shield_color_warning">#FF4B55</color>
<color name="shield_color_warning_background">#0FFF4B55</color> <color name="shield_color_warning_background">#0FFF4B55</color>

View file

@ -40,7 +40,7 @@ import kotlin.coroutines.resume
class DeactivateAccountTest : InstrumentedTest { class DeactivateAccountTest : InstrumentedTest {
@Test @Test
fun deactivateAccountTest() = runSessionTest(context(), false /* session will be deactivated */) { commonTestHelper -> fun deactivateAccountTest() = runSessionTest(context(), autoSignoutOnClose = false /* session will be deactivated */) { commonTestHelper ->
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
// Deactivate the account // Deactivate the account

View file

@ -37,6 +37,7 @@ import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.SyncConfig import org.matrix.android.sdk.api.SyncConfig
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.registration.RegistrationResult import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType 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.session.events.model.toModel
@ -60,13 +61,13 @@ import kotlin.coroutines.suspendCoroutine
* This class exposes methods to be used in common cases * This class exposes methods to be used in common cases
* Registration, login, Sync, Sending messages... * Registration, login, Sync, Sending messages...
*/ */
class CommonTestHelper internal constructor(context: Context) { class CommonTestHelper internal constructor(context: Context, val cryptoConfig: MXCryptoConfig? = null) {
companion object { companion object {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CommonTestHelper) -> Unit) { internal fun runSessionTest(context: Context, cryptoConfig: MXCryptoConfig? = null, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CommonTestHelper) -> Unit) {
val testHelper = CommonTestHelper(context) val testHelper = CommonTestHelper(context, cryptoConfig)
return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) { return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) {
try { try {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
@ -81,8 +82,8 @@ class CommonTestHelper internal constructor(context: Context) {
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
internal fun runCryptoTest(context: Context, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CryptoTestHelper, CommonTestHelper) -> Unit) { internal fun runCryptoTest(context: Context, cryptoConfig: MXCryptoConfig? = null, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CryptoTestHelper, CommonTestHelper) -> Unit) {
val testHelper = CommonTestHelper(context) val testHelper = CommonTestHelper(context, cryptoConfig)
val cryptoTestHelper = CryptoTestHelper(testHelper) val cryptoTestHelper = CryptoTestHelper(testHelper)
return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) { return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) {
try { try {
@ -114,6 +115,7 @@ class CommonTestHelper internal constructor(context: Context) {
applicationFlavor = "TestFlavor", applicationFlavor = "TestFlavor",
roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider(), roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider(),
syncConfig = SyncConfig(longPollTimeout = 5_000L), syncConfig = SyncConfig(longPollTimeout = 5_000L),
cryptoConfig = cryptoConfig ?: MXCryptoConfig()
) )
) )
} }

View file

@ -394,14 +394,16 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
suspend fun ensureCanDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, messagesText: List<String>) { suspend fun ensureCanDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, messagesText: List<String>) {
sentEventIds.forEachIndexed { index, sentEventId -> sentEventIds.forEachIndexed { index, sentEventId ->
testHelper.retryPeriodically { testHelper.retryPeriodically {
val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root val event = session.getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(sentEventId)?.root
?: return@retryPeriodically false
try { try {
session.cryptoService().decryptEvent(event, "").let { result -> session.cryptoService().decryptEvent(event, "").let { result ->
event.mxDecryptionResult = OlmDecryptionResult( event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent, payload = result.clearEvent,
senderKey = result.senderCurve25519Key, senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
) )
} }
} catch (error: MXCryptoError) { } catch (error: MXCryptoError) {

View file

@ -33,9 +33,9 @@ import org.junit.runner.RunWith
import org.junit.runners.JUnit4 import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.RequestResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
@ -49,7 +49,6 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.Room
@ -130,7 +129,8 @@ class E2eeSanityTests : InstrumentedTest {
val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!) val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
timeLineEvent != null && timeLineEvent != null &&
timeLineEvent.isEncrypted() && timeLineEvent.isEncrypted() &&
timeLineEvent.root.getClearType() == EventType.MESSAGE timeLineEvent.root.getClearType() == EventType.MESSAGE &&
timeLineEvent.root.mxDecryptionResult?.isSafe == true
} }
} }
@ -302,6 +302,13 @@ class E2eeSanityTests : InstrumentedTest {
// ensure bob can now decrypt // ensure bob can now decrypt
cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText) cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
// Check key trust
sentEventIds.forEach { sentEventId ->
val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)!!
val result = newBobSession.cryptoService().decryptEvent(timelineEvent.root, "")
assertEquals("Keys from history should be deniable", false, result.isSafe)
}
} }
/** /**
@ -348,10 +355,6 @@ class E2eeSanityTests : InstrumentedTest {
Log.v("#E2E TEST", "check that new bob can't currently decrypt") Log.v("#E2E TEST", "check that new bob can't currently decrypt")
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
// newBobSession.cryptoService().getOutgoingRoomKeyRequests()
// .firstOrNull {
// it.sessionId ==
// }
// Try to request // Try to request
sentEventIds.forEach { sentEventId -> sentEventIds.forEach { sentEventId ->
@ -359,31 +362,27 @@ class E2eeSanityTests : InstrumentedTest {
newBobSession.cryptoService().requestRoomKeyForEvent(event) newBobSession.cryptoService().requestRoomKeyForEvent(event)
} }
// wait a bit
// 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) // Ensure that new bob still can't decrypt (keys must have been withheld)
sentEventIds.forEach { sentEventId -> // sentEventIds.forEach { sentEventId ->
val megolmSessionId = newBobSession.getRoom(e2eRoomID)!! // val megolmSessionId = newBobSession.getRoom(e2eRoomID)!!
.getTimelineEvent(sentEventId)!! // .getTimelineEvent(sentEventId)!!
.root.content.toModel<EncryptedEventContent>()!!.sessionId // .root.content.toModel<EncryptedEventContent>()!!.sessionId
testHelper.retryPeriodically { // testHelper.retryPeriodically {
val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests() // val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests()
.first { // .first {
it.sessionId == megolmSessionId && // it.sessionId == megolmSessionId &&
it.roomId == e2eRoomID // it.roomId == e2eRoomID
} // }
.results.also { // .results.also {
Log.w("##TEST", "result list is $it") // Log.w("##TEST", "result list is $it")
} // }
.firstOrNull { it.userId == aliceSession.myUserId } // .firstOrNull { it.userId == aliceSession.myUserId }
?.result // ?.result
aliceReply != null && // aliceReply != null &&
aliceReply is RequestResult.Failure && // aliceReply is RequestResult.Failure &&
WithHeldCode.UNAUTHORISED == aliceReply.code // WithHeldCode.UNAUTHORISED == aliceReply.code
} // }
} // }
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null) cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
@ -405,7 +404,10 @@ class E2eeSanityTests : InstrumentedTest {
* Test that if a better key is forwarded (lower index, it is then used) * Test that if a better key is forwarded (lower index, it is then used)
*/ */
@Test @Test
fun testForwardBetterKey() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> fun testForwardBetterKey() = runCryptoTest(
context(),
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession val aliceSession = cryptoTestData.firstSession

View file

@ -77,6 +77,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
*/ */
private fun testShareHistoryWithRoomVisibility(roomHistoryVisibility: RoomHistoryVisibility? = null) = private fun testShareHistoryWithRoomVisibility(roomHistoryVisibility: RoomHistoryVisibility? = null) =
runCryptoTest(context()) { cryptoTestHelper, testHelper -> runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val aliceMessageText = "Hello Bob, I am Alice!"
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, roomHistoryVisibility) val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, roomHistoryVisibility)
val e2eRoomID = cryptoTestData.roomId val e2eRoomID = cryptoTestData.roomId
@ -96,7 +97,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2) assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2)
Log.v("#E2E TEST", "Alice and Bob are in roomId: $e2eRoomID") Log.v("#E2E TEST", "Alice and Bob are in roomId: $e2eRoomID")
val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, "Hello Bob, I am Alice!", testHelper) val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, aliceMessageText, testHelper)
Assert.assertTrue("Message should be sent", aliceMessageId != null) Assert.assertTrue("Message should be sent", aliceMessageId != null)
Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID") Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID")
@ -105,7 +106,8 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
(timelineEvent != null && (timelineEvent != null &&
timelineEvent.isEncrypted() && timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE).also { timelineEvent.root.getClearType() == EventType.MESSAGE &&
timelineEvent.root.mxDecryptionResult?.isSafe == true).also {
if (it) { if (it) {
Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
} }
@ -137,7 +139,8 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!) val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
(timelineEvent != null && (timelineEvent != null &&
timelineEvent.isEncrypted() && timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE timelineEvent.root.getClearType() == EventType.MESSAGE &&
timelineEvent.root.mxDecryptionResult?.isSafe == false
).also { ).also {
if (it) { if (it) {
Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}") Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
@ -354,7 +357,10 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
} }
private suspend fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? { private suspend fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? {
return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.eventId return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.let {
Log.v("#E2E TEST", "Message sent with session ${it.root.content?.get("session_id")}")
return it.eventId
}
} }
private suspend fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String, testHelper: CommonTestHelper) { private suspend fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String, testHelper: CommonTestHelper) {

View file

@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
@ -82,7 +83,10 @@ class UnwedgingTest : InstrumentedTest {
* -> This is automatically fixed after SDKs restarted the olm session * -> This is automatically fixed after SDKs restarted the olm session
*/ */
@Test @Test
fun testUnwedging() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> fun testUnwedging() = runCryptoTest(
context(),
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession val aliceSession = cryptoTestData.firstSession

View file

@ -22,15 +22,16 @@ import androidx.test.filters.LargeTest
import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue import junit.framework.TestCase.assertTrue
import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.internal.assertEquals
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Assert import org.junit.Assert
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState 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.RequestResult
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
@ -43,7 +44,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
import org.matrix.android.sdk.common.RetryTestRule
import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.mustFail import org.matrix.android.sdk.mustFail
@ -51,16 +51,15 @@ import org.matrix.android.sdk.mustFail
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@LargeTest @LargeTest
@Ignore
class KeyShareTests : InstrumentedTest { class KeyShareTests : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3) // @get:Rule val rule = RetryTestRule(3)
@Test @Test
fun test_DoNotSelfShareIfNotTrusted() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> fun test_DoNotSelfShareIfNotTrusted() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true)) val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}") Log.v("#TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}")
// Create an encrypted room and add a message // Create an encrypted room and add a message
val roomId = aliceSession.roomService().createRoom( val roomId = aliceSession.roomService().createRoom(
@ -84,7 +83,7 @@ class KeyShareTests : InstrumentedTest {
aliceSession2.cryptoService().enableKeyGossiping(false) aliceSession2.cryptoService().enableKeyGossiping(false)
commonTestHelper.syncSession(aliceSession2) commonTestHelper.syncSession(aliceSession2)
Log.v("TEST", "=======> AliceSession 2 is ${aliceSession2.sessionParams.deviceId}") Log.v("#TEST", "=======> AliceSession 2 is ${aliceSession2.sessionParams.deviceId}")
val roomSecondSessionPOV = aliceSession2.getRoom(roomId) val roomSecondSessionPOV = aliceSession2.getRoom(roomId)
@ -115,7 +114,7 @@ class KeyShareTests : InstrumentedTest {
outgoing != null outgoing != null
} }
} }
Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId") Log.v("#TEST", "=======> Outgoing requet Id is $outGoingRequestId")
val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequests() val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
@ -127,14 +126,17 @@ class KeyShareTests : InstrumentedTest {
// the request should be refused, because the device is not trusted // the request should be refused, because the device is not trusted
commonTestHelper.retryPeriodically { commonTestHelper.retryPeriodically {
// DEBUG LOGS // DEBUG LOGS
// aliceSession.cryptoService().getIncomingRoomKeyRequests().let { aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
// Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)") Log.v("#TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
// Log.v("TEST", "=========================") Log.v("#TEST", "=========================")
// it.forEach { keyRequest -> it.forEach { keyRequest ->
// Log.v("TEST", "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") Log.v(
// } "#TEST",
// 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 } val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
incoming != null incoming != null
@ -143,10 +145,10 @@ class KeyShareTests : InstrumentedTest {
commonTestHelper.retryPeriodically { commonTestHelper.retryPeriodically {
// DEBUG LOGS // DEBUG LOGS
aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest -> aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest ->
Log.v("TEST", "=========================") Log.v("#TEST", "=========================")
Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}") Log.v("#TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}") Log.v("#TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
Log.v("TEST", "=========================") Log.v("#TEST", "=========================")
} }
val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId } val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
@ -160,11 +162,24 @@ class KeyShareTests : InstrumentedTest {
} }
// Mark the device as trusted // Mark the device as trusted
Log.v("#TEST", "=======> Alice device 1 is ${aliceSession.sessionParams.deviceId}|${aliceSession.cryptoService().getMyDevice().identityKey()}")
val aliceSecondSession = aliceSession2.cryptoService().getMyDevice()
Log.v("#TEST", "=======> Alice device 2 is ${aliceSession2.sessionParams.deviceId}|${aliceSecondSession.identityKey()}")
aliceSession.cryptoService().setDeviceVerification( aliceSession.cryptoService().setDeviceVerification(
DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId, DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId,
aliceSession2.sessionParams.deviceId ?: "" aliceSession2.sessionParams.deviceId ?: ""
) )
// We only accept forwards from trusted session, so we need to trust on other side to
aliceSession2.cryptoService().setDeviceVerification(
DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId,
aliceSession.sessionParams.deviceId ?: ""
)
aliceSession.cryptoService().deviceWithIdentityKey(aliceSecondSession.identityKey()!!, MXCRYPTO_ALGORITHM_OLM)!!.isVerified shouldBeEqualTo true
// Re request // Re request
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root) aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
@ -181,7 +196,10 @@ class KeyShareTests : InstrumentedTest {
* if the key was originally shared with him * if the key was originally shared with him
*/ */
@Test @Test
fun test_reShareIfWasIntendedToBeShared() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> fun test_reShareIfWasIntendedToBeShared() = runCryptoTest(
context(),
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = testData.firstSession val aliceSession = testData.firstSession
@ -210,7 +228,10 @@ class KeyShareTests : InstrumentedTest {
* if the key was originally shared with him * if the key was originally shared with him
*/ */
@Test @Test
fun test_reShareToUnverifiedIfWasIntendedToBeShared() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> fun test_reShareToUnverifiedIfWasIntendedToBeShared() = runCryptoTest(
context(),
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true) val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true)
val aliceSession = testData.firstSession val aliceSession = testData.firstSession
@ -226,7 +247,6 @@ class KeyShareTests : InstrumentedTest {
} }
val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first() val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first()
val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!! val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
// Let's try to request any how. // Let's try to request any how.
// As it was share previously alice should accept to reshare // As it was share previously alice should accept to reshare
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root) aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
@ -243,7 +263,10 @@ class KeyShareTests : InstrumentedTest {
* Tests that keys reshared with own verified session are done from the earliest known index * Tests that keys reshared with own verified session are done from the earliest known index
*/ */
@Test @Test
fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() = runCryptoTest(
context(),
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = testData.firstSession val aliceSession = testData.firstSession
@ -309,6 +332,9 @@ class KeyShareTests : InstrumentedTest {
aliceSession.cryptoService() aliceSession.cryptoService()
.verificationService() .verificationService()
.markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!) .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!)
aliceNewSession.cryptoService()
.verificationService()
.markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!)
// Let's now try to request // Let's now try to request
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root) aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root)
@ -353,7 +379,10 @@ class KeyShareTests : InstrumentedTest {
* Tests that we don't cancel a request to early on first forward if the index is not good enough * Tests that we don't cancel a request to early on first forward if the index is not good enough
*/ */
@Test @Test
fun test_dontCancelToEarly() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper -> fun test_dontCancelToEarly() = runCryptoTest(
context(),
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
) { cryptoTestHelper, commonTestHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true) val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = testData.firstSession val aliceSession = testData.firstSession
val bobSession = testData.secondSession!! val bobSession = testData.secondSession!!
@ -391,6 +420,9 @@ class KeyShareTests : InstrumentedTest {
aliceSession.cryptoService() aliceSession.cryptoService()
.verificationService() .verificationService()
.markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!) .markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!)
aliceNewSession.cryptoService()
.verificationService()
.markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!)
// /!\ Stop initial alice session syncing so that it can't reply // /!\ Stop initial alice session syncing so that it can't reply
aliceSession.cryptoService().enableKeyGossiping(false) aliceSession.cryptoService().enableKeyGossiping(false)

View file

@ -27,6 +27,7 @@ import org.junit.runner.RunWith
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.RequestResult import org.matrix.android.sdk.api.session.crypto.RequestResult
@ -143,7 +144,10 @@ class WithHeldTests : InstrumentedTest {
} }
@Test @Test
fun test_WithHeldNoOlm() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> fun test_WithHeldNoOlm() = runCryptoTest(
context(),
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
) { cryptoTestHelper, testHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = testData.firstSession val aliceSession = testData.firstSession
@ -217,7 +221,10 @@ class WithHeldTests : InstrumentedTest {
} }
@Test @Test
fun test_WithHeldKeyRequest() = runCryptoTest(context()) { cryptoTestHelper, testHelper -> fun test_WithHeldKeyRequest() = runCryptoTest(
context(),
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
) { cryptoTestHelper, testHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom() val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = testData.firstSession val aliceSession = testData.firstSession

View file

@ -35,8 +35,9 @@ data class MXCryptoConfig constructor(
/** /**
* Currently megolm keys are requested to the sender device and to all of our devices. * 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` * You can limit request only to your sessions by turning this setting to `true`.
* Forwarded keys coming from other users will also be ignored if set to true.
*/ */
val limitRoomKeyRequestsToMyDevices: Boolean = false, val limitRoomKeyRequestsToMyDevices: Boolean = true,
) )

View file

@ -43,5 +43,7 @@ data class MXEventDecryptionResult(
* List of curve25519 keys involved in telling us about the senderCurve25519Key and * List of curve25519 keys involved in telling us about the senderCurve25519Key and
* claimedEd25519Key. See MXEvent.forwardingCurve25519KeyChain. * claimedEd25519Key. See MXEvent.forwardingCurve25519KeyChain.
*/ */
val forwardingCurve25519KeyChain: List<String> = emptyList() val forwardingCurve25519KeyChain: List<String> = emptyList(),
val isSafe: Boolean = false
) )

View file

@ -44,5 +44,10 @@ data class OlmDecryptionResult(
/** /**
* Devices which forwarded this session to us (normally empty). * Devices which forwarded this session to us (normally empty).
*/ */
@Json(name = "forwardingCurve25519KeyChain") val forwardingCurve25519KeyChain: List<String>? = null @Json(name = "forwardingCurve25519KeyChain") val forwardingCurve25519KeyChain: List<String>? = null,
/**
* True if the key used to decrypt is considered safe (trusted).
*/
@Json(name = "key_safety") val isSafe: Boolean? = null,
) )

View file

@ -174,15 +174,29 @@ data class Event(
* @return the event type * @return the event type
*/ */
fun getClearType(): String { fun getClearType(): String {
return mxDecryptionResult?.payload?.get("type")?.toString() ?: type ?: EventType.MISSING_TYPE return getDecryptedType() ?: type ?: EventType.MISSING_TYPE
}
/**
* @return The decrypted type, or null. Won't fallback to the wired type
*/
fun getDecryptedType(): String? {
return mxDecryptionResult?.payload?.get("type")?.toString()
} }
/** /**
* @return the event content * @return the event content
*/ */
fun getClearContent(): Content? { fun getClearContent(): Content? {
return getDecryptedContent() ?: content
}
/**
* @return the decrypted event content or null, Won't fallback to the wired content
*/
fun getDecryptedContent(): Content? {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return mxDecryptionResult?.payload?.get("content") as? Content ?: content return mxDecryptionResult?.payload?.get("content") as? Content
} }
fun toContentStringWithIndent(): String { fun toContentStringWithIndent(): String {

View file

@ -79,6 +79,7 @@ import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationActio
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
@ -183,7 +184,8 @@ internal class DefaultCryptoService @Inject constructor(
private val cryptoCoroutineScope: CoroutineScope, private val cryptoCoroutineScope: CoroutineScope,
private val eventDecryptor: EventDecryptor, private val eventDecryptor: EventDecryptor,
private val verificationMessageProcessor: VerificationMessageProcessor, private val verificationMessageProcessor: VerificationMessageProcessor,
private val liveEventManager: Lazy<StreamEventsManager> private val liveEventManager: Lazy<StreamEventsManager>,
private val unrequestedForwardManager: UnRequestedForwardManager,
) : CryptoService { ) : CryptoService {
private val isStarting = AtomicBoolean(false) private val isStarting = AtomicBoolean(false)
@ -399,6 +401,7 @@ internal class DefaultCryptoService @Inject constructor(
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
incomingKeyRequestManager.close() incomingKeyRequestManager.close()
outgoingKeyRequestManager.close() outgoingKeyRequestManager.close()
unrequestedForwardManager.close()
olmDevice.release() olmDevice.release()
cryptoStore.close() cryptoStore.close()
} }
@ -485,6 +488,14 @@ internal class DefaultCryptoService @Inject constructor(
// just for safety but should not throw // just for safety but should not throw
Timber.tag(loggerTag.value).w("failed to process incoming room key requests") Timber.tag(loggerTag.value).w("failed to process incoming room key requests")
} }
unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(clock.epochMillis()) { events ->
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
events.forEach {
onRoomKeyEvent(it, true)
}
}
}
} }
} }
} }
@ -845,9 +856,9 @@ internal class DefaultCryptoService @Inject constructor(
* *
* @param event the key event. * @param event the key event.
*/ */
private fun onRoomKeyEvent(event: Event) { private fun onRoomKeyEvent(event: Event, acceptUnrequested: Boolean = false) {
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return val roomKeyContent = event.getDecryptedContent().toModel<RoomKeyContent>() ?: return
Timber.tag(loggerTag.value).i("onRoomKeyEvent() from: ${event.senderId} type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>") Timber.tag(loggerTag.value).i("onRoomKeyEvent(forceAccept:$acceptUnrequested) from: ${event.senderId} type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>")
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : missing fields") Timber.tag(loggerTag.value).e("onRoomKeyEvent() : missing fields")
return return
@ -857,7 +868,7 @@ internal class DefaultCryptoService @Inject constructor(
Timber.tag(loggerTag.value).e("GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") Timber.tag(loggerTag.value).e("GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
return return
} }
alg.onRoomKeyEvent(event, keysBackupService) alg.onRoomKeyEvent(event, keysBackupService, acceptUnrequested)
} }
private fun onKeyWithHeldReceived(event: Event) { private fun onKeyWithHeldReceived(event: Event) {
@ -950,6 +961,15 @@ internal class DefaultCryptoService @Inject constructor(
* @param event the membership event causing the change * @param event the membership event causing the change
*/ */
private fun onRoomMembershipEvent(roomId: String, event: Event) { private fun onRoomMembershipEvent(roomId: String, event: Event) {
// because the encryption event can be after the join/invite in the same batch
event.stateKey?.let { _ ->
val roomMember: RoomMemberContent? = event.content.toModel()
val membership = roomMember?.membership
if (membership == Membership.INVITE) {
unrequestedForwardManager.onInviteReceived(roomId, event.senderId.orEmpty(), clock.epochMillis())
}
}
roomEncryptorsStore.get(roomId) ?: /* No encrypting in this room */ return roomEncryptorsStore.get(roomId) ?: /* No encrypting in this room */ return
event.stateKey?.let { userId -> event.stateKey?.let { userId ->

View file

@ -91,6 +91,21 @@ internal class InboundGroupSessionStore @Inject constructor(
internalStoreGroupSession(new, sessionId, senderKey) internalStoreGroupSession(new, sessionId, senderKey)
} }
@Synchronized
fun updateToSafe(old: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
Timber.tag(loggerTag.value).v("## updateToSafe for session ${old.wrapper.roomId}-${old.wrapper.senderKey}")
store.storeInboundGroupSessions(
listOf(
old.wrapper.copy(
sessionData = old.wrapper.sessionData.copy(trusted = true)
)
)
)
// will release it :/
sessionCache.remove(CacheKey(sessionId, senderKey))
}
@Synchronized @Synchronized
fun storeInBoundGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) { fun storeInBoundGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
internalStoreGroupSession(holder, sessionId, senderKey) internalStoreGroupSession(holder, sessionId, senderKey)

View file

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
@ -612,7 +613,8 @@ internal class MXOlmDevice @Inject constructor(
forwardingCurve25519KeyChain: List<String>, forwardingCurve25519KeyChain: List<String>,
keysClaimed: Map<String, String>, keysClaimed: Map<String, String>,
exportFormat: Boolean, exportFormat: Boolean,
sharedHistory: Boolean sharedHistory: Boolean,
trusted: Boolean
): AddSessionResult { ): AddSessionResult {
val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") { val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") {
if (exportFormat) { if (exportFormat) {
@ -620,6 +622,8 @@ internal class MXOlmDevice @Inject constructor(
} else { } else {
OlmInboundGroupSession(sessionKey) OlmInboundGroupSession(sessionKey)
} }
} ?: return AddSessionResult.NotImported.also {
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : failed to import key candidate $senderKey/$sessionId")
} }
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) } val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
@ -631,31 +635,49 @@ internal class MXOlmDevice @Inject constructor(
val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex } ?: return AddSessionResult.NotImported.also { val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex } ?: return AddSessionResult.NotImported.also {
// This is quite unexpected, could throw if native was released? // This is quite unexpected, could throw if native was released?
Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session") Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
candidateSession?.releaseSession() candidateSession.releaseSession()
// Probably should discard it? // Probably should discard it?
} }
val newKnownFirstIndex = tryOrNull("Failed to get candidate first known index") { candidateSession?.firstKnownIndex } val newKnownFirstIndex = tryOrNull("Failed to get candidate first known index") { candidateSession.firstKnownIndex }
?: return AddSessionResult.NotImported.also {
candidateSession.releaseSession()
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Failed to get new session index")
}
val keyConnects = existingSession.session.connects(candidateSession)
if (!keyConnects) {
Timber.tag(loggerTag.value)
.e("## addInboundGroupSession() Unconnected key")
if (!trusted) {
// Ignore the not connecting unsafe, keep existing
Timber.tag(loggerTag.value)
.e("## addInboundGroupSession() Received unsafe unconnected key")
return AddSessionResult.NotImported
}
// else if the new one is safe and does not connect with existing, import the new one
} else {
// If our existing session is better we keep it // If our existing session is better we keep it
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { if (existingFirstKnown <= newKnownFirstIndex) {
val shouldUpdateTrust = trusted && (existingSession.sessionData.trusted != true)
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : updateTrust for $sessionId")
if (shouldUpdateTrust) {
// the existing as a better index but the new one is trusted so update trust
inboundGroupSessionStore.updateToSafe(existingSessionHolder, sessionId, senderKey)
}
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId") Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
candidateSession?.releaseSession() candidateSession.releaseSession()
return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt()) return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt())
} }
}
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}") Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
candidateSession?.releaseSession() candidateSession.releaseSession()
return AddSessionResult.NotImported return AddSessionResult.NotImported
} }
} }
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId") Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId")
// sanity check on the new session
if (null == candidateSession) {
Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>")
return AddSessionResult.NotImported
}
try { try {
if (candidateSession.sessionIdentifier() != sessionId) { if (candidateSession.sessionIdentifier() != sessionId) {
Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
@ -674,6 +696,7 @@ internal class MXOlmDevice @Inject constructor(
keysClaimed = keysClaimed, keysClaimed = keysClaimed,
forwardingCurve25519KeyChain = forwardingCurve25519KeyChain, forwardingCurve25519KeyChain = forwardingCurve25519KeyChain,
sharedHistory = sharedHistory, sharedHistory = sharedHistory,
trusted = trusted
) )
val wrapper = MXInboundMegolmSessionWrapper( val wrapper = MXInboundMegolmSessionWrapper(
@ -689,6 +712,16 @@ internal class MXOlmDevice @Inject constructor(
return AddSessionResult.Imported(candidateSession.firstKnownIndex.toInt()) return AddSessionResult.Imported(candidateSession.firstKnownIndex.toInt())
} }
fun OlmInboundGroupSession.connects(other: OlmInboundGroupSession): Boolean {
return try {
val lowestCommonIndex = this.firstKnownIndex.coerceAtLeast(other.firstKnownIndex)
this.export(lowestCommonIndex) == other.export(lowestCommonIndex)
} catch (failure: Throwable) {
// native error? key disposed?
false
}
}
/** /**
* Import an inbound group sessions to the session store. * Import an inbound group sessions to the session store.
* *
@ -821,7 +854,8 @@ internal class MXOlmDevice @Inject constructor(
payload, payload,
wrapper.sessionData.keysClaimed, wrapper.sessionData.keysClaimed,
senderKey, senderKey,
wrapper.sessionData.forwardingCurve25519KeyChain wrapper.sessionData.forwardingCurve25519KeyChain,
isSafe = sessionHolder.wrapper.sessionData.trusted.orFalse()
) )
} }

View file

@ -267,13 +267,24 @@ internal class SecretShareManager @Inject constructor(
Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event") Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event")
return return
} }
// no need to download keys, after a verification we already forced download
val sendingDevice = toDevice.getSenderKey()?.let { cryptoStore.deviceWithIdentityKey(it) }
if (sendingDevice == null) {
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from unknown device ${toDevice.getSenderKey()}")
return
}
// Was that sent by us? // Was that sent by us?
if (toDevice.senderId != credentials.userId) { if (sendingDevice.userId != credentials.userId) {
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}") Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}")
return return
} }
if (!sendingDevice.isVerified) {
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from untrusted device ${toDevice.getSenderKey()}")
return
}
val secretContent = toDevice.getClearContent().toModel<SecretSendEventContent>() ?: return val secretContent = toDevice.getClearContent().toModel<SecretSendEventContent>() ?: return
val existingRequest = verifMutex.withLock { val existingRequest = verifMutex.withLock {

View file

@ -42,5 +42,5 @@ internal interface IMXDecrypting {
* @param event the key event. * @param event the key event.
* @param defaultKeysBackupService the keys backup service * @param defaultKeysBackupService the keys backup service
*/ */
fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {} fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean = false) {}
} }

View file

@ -17,7 +17,8 @@
package org.matrix.android.sdk.internal.crypto.algorithms.megolm package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import dagger.Lazy import dagger.Lazy
import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.NewSessionListener import org.matrix.android.sdk.api.session.crypto.NewSessionListener
@ -34,16 +35,20 @@ import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.session.StreamEventsManager import org.matrix.android.sdk.internal.session.StreamEventsManager
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber import timber.log.Timber
private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO) private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO)
internal class MXMegolmDecryption( internal class MXMegolmDecryption(
private val olmDevice: MXOlmDevice, private val olmDevice: MXOlmDevice,
private val myUserId: String,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager, private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
private val matrixConfiguration: MatrixConfiguration, private val liveEventManager: Lazy<StreamEventsManager>,
private val liveEventManager: Lazy<StreamEventsManager> private val unrequestedForwardManager: UnRequestedForwardManager,
private val cryptoConfig: MXCryptoConfig,
private val clock: Clock,
) : IMXDecrypting { ) : IMXDecrypting {
var newSessionListener: NewSessionListener? = null var newSessionListener: NewSessionListener? = null
@ -94,7 +99,8 @@ internal class MXMegolmDecryption(
senderCurve25519Key = olmDecryptionResult.senderKey, senderCurve25519Key = olmDecryptionResult.senderKey,
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"), claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
.orEmpty() .orEmpty(),
isSafe = olmDecryptionResult.isSafe.orFalse()
).also { ).also {
liveEventManager.get().dispatchLiveEventDecrypted(event, it) liveEventManager.get().dispatchLiveEventDecrypted(event, it)
} }
@ -182,12 +188,21 @@ internal class MXMegolmDecryption(
* @param event the key event. * @param event the key event.
* @param defaultKeysBackupService the keys backup service * @param defaultKeysBackupService the keys backup service
*/ */
override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) { override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean) {
Timber.tag(loggerTag.value).v("onRoomKeyEvent()") Timber.tag(loggerTag.value).v("onRoomKeyEvent(${event.getSenderKey()})")
var exportFormat = false var exportFormat = false
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return val roomKeyContent = event.getDecryptedContent()?.toModel<RoomKeyContent>() ?: return
val eventSenderKey: String = event.getSenderKey() ?: return Unit.also {
Timber.tag(loggerTag.value).e("onRoom Key/Forward Event() : event is missing sender_key field")
}
// this device might not been downloaded now?
val fromDevice = cryptoStore.deviceWithIdentityKey(eventSenderKey)
lateinit var sessionInitiatorSenderKey: String
val trusted: Boolean
var senderKey: String? = event.getSenderKey()
var keysClaimed: MutableMap<String, String> = HashMap() var keysClaimed: MutableMap<String, String> = HashMap()
val forwardingCurve25519KeyChain: MutableList<String> = ArrayList() val forwardingCurve25519KeyChain: MutableList<String> = ArrayList()
@ -195,32 +210,25 @@ internal class MXMegolmDecryption(
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : Key event is missing fields") Timber.tag(loggerTag.value).e("onRoomKeyEvent() : Key event is missing fields")
return return
} }
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { if (event.getDecryptedType() == EventType.FORWARDED_ROOM_KEY) {
if (!cryptoStore.isKeyGossipingEnabled()) { if (!cryptoStore.isKeyGossipingEnabled()) {
Timber.tag(loggerTag.value) Timber.tag(loggerTag.value)
.i("onRoomKeyEvent(), ignore forward adding as per crypto config : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") .i("onRoomKeyEvent(), ignore forward adding as per crypto config : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
return return
} }
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>() val forwardedRoomKeyContent = event.getDecryptedContent()?.toModel<ForwardedRoomKeyContent>()
?: return ?: return
forwardedRoomKeyContent.forwardingCurve25519KeyChain?.let { forwardedRoomKeyContent.forwardingCurve25519KeyChain?.let {
forwardingCurve25519KeyChain.addAll(it) forwardingCurve25519KeyChain.addAll(it)
} }
if (senderKey == null) { forwardingCurve25519KeyChain.add(eventSenderKey)
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : event is missing sender_key field")
return
}
forwardingCurve25519KeyChain.add(senderKey)
exportFormat = true exportFormat = true
senderKey = forwardedRoomKeyContent.senderKey sessionInitiatorSenderKey = forwardedRoomKeyContent.senderKey ?: return Unit.also {
if (null == senderKey) {
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : forwarded_room_key event is missing sender_key field") Timber.tag(loggerTag.value).e("onRoomKeyEvent() : forwarded_room_key event is missing sender_key field")
return
} }
if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) { if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) {
@ -229,13 +237,51 @@ internal class MXMegolmDecryption(
} }
keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key
} else {
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") // checking if was requested once.
if (null == senderKey) { // should we check if the request is sort of active?
Timber.tag(loggerTag.value).e("## onRoomKeyEvent() : key event has no sender key (not encrypted?)") val wasNotRequested = cryptoStore.getOutgoingRoomKeyRequest(
roomId = forwardedRoomKeyContent.roomId.orEmpty(),
sessionId = forwardedRoomKeyContent.sessionId.orEmpty(),
algorithm = forwardedRoomKeyContent.algorithm.orEmpty(),
senderKey = forwardedRoomKeyContent.senderKey.orEmpty(),
).isEmpty()
trusted = false
if (!forceAccept && wasNotRequested) {
// val senderId = cryptoStore.deviceWithIdentityKey(event.getSenderKey().orEmpty())?.userId.orEmpty()
unrequestedForwardManager.onUnRequestedKeyForward(roomKeyContent.roomId, event, clock.epochMillis())
// Ignore unsolicited
Timber.tag(loggerTag.value).w("Ignoring forwarded_room_key_event for ${roomKeyContent.sessionId} that was not requested")
return return
} }
// Check who sent the request, as we requested we have the device keys (no need to download)
val sessionThatIsSharing = cryptoStore.deviceWithIdentityKey(eventSenderKey)
if (sessionThatIsSharing == null) {
Timber.tag(loggerTag.value).w("Ignoring forwarded_room_key from unknown device with identity $eventSenderKey")
return
}
val isOwnDevice = myUserId == sessionThatIsSharing.userId
val isDeviceVerified = sessionThatIsSharing.isVerified
val isFromSessionInitiator = sessionThatIsSharing.identityKey() == sessionInitiatorSenderKey
val isLegitForward = (isOwnDevice && isDeviceVerified) ||
(!cryptoConfig.limitRoomKeyRequestsToMyDevices && isFromSessionInitiator)
val shouldAcceptForward = forceAccept || isLegitForward
if (!shouldAcceptForward) {
Timber.tag(loggerTag.value)
.w("Ignoring forwarded_room_key device:$eventSenderKey, ownVerified:{$isOwnDevice&&$isDeviceVerified}, fromInitiator:$isFromSessionInitiator")
return
}
} else {
// It's a m.room_key so safe
trusted = true
sessionInitiatorSenderKey = eventSenderKey
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
// inherit the claimed ed25519 key from the setup message // inherit the claimed ed25519 key from the setup message
keysClaimed = event.getKeysClaimed().toMutableMap() keysClaimed = event.getKeysClaimed().toMutableMap()
} }
@ -245,12 +291,15 @@ internal class MXMegolmDecryption(
sessionId = roomKeyContent.sessionId, sessionId = roomKeyContent.sessionId,
sessionKey = roomKeyContent.sessionKey, sessionKey = roomKeyContent.sessionKey,
roomId = roomKeyContent.roomId, roomId = roomKeyContent.roomId,
senderKey = senderKey, senderKey = sessionInitiatorSenderKey,
forwardingCurve25519KeyChain = forwardingCurve25519KeyChain, forwardingCurve25519KeyChain = forwardingCurve25519KeyChain,
keysClaimed = keysClaimed, keysClaimed = keysClaimed,
exportFormat = exportFormat, exportFormat = exportFormat,
sharedHistory = roomKeyContent.getSharedKey() sharedHistory = roomKeyContent.getSharedKey(),
) trusted = trusted
).also {
Timber.tag(loggerTag.value).v(".. onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId} result: $it")
}
when (addSessionResult) { when (addSessionResult) {
is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex
@ -258,35 +307,28 @@ internal class MXMegolmDecryption(
else -> null else -> null
}?.let { index -> }?.let { index ->
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { 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( outgoingKeyRequestManager.onRoomKeyForwarded(
sessionId = roomKeyContent.sessionId, sessionId = roomKeyContent.sessionId,
algorithm = roomKeyContent.algorithm ?: "", algorithm = roomKeyContent.algorithm ?: "",
roomId = roomKeyContent.roomId, roomId = roomKeyContent.roomId,
senderKey = senderKey, senderKey = sessionInitiatorSenderKey,
fromIndex = index, fromIndex = index,
fromDevice = fromDevice, fromDevice = fromDevice?.deviceId,
event = event event = event
) )
cryptoStore.saveIncomingForwardKeyAuditTrail( cryptoStore.saveIncomingForwardKeyAuditTrail(
roomId = roomKeyContent.roomId, roomId = roomKeyContent.roomId,
sessionId = roomKeyContent.sessionId, sessionId = roomKeyContent.sessionId,
senderKey = senderKey, senderKey = sessionInitiatorSenderKey,
algorithm = roomKeyContent.algorithm ?: "", algorithm = roomKeyContent.algorithm ?: "",
userId = event.senderId ?: "", userId = event.senderId.orEmpty(),
deviceId = fromDevice ?: "", deviceId = fromDevice?.deviceId.orEmpty(),
chainIndex = index.toLong() chainIndex = index.toLong()
) )
// The index is used to decide if we cancel sent request or if we wait for a better key // 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) outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, sessionInitiatorSenderKey, index)
} }
} }
@ -295,7 +337,7 @@ internal class MXMegolmDecryption(
.d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}") .d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}")
defaultKeysBackupService.maybeBackupKeys() defaultKeysBackupService.maybeBackupKeys()
onNewSession(roomKeyContent.roomId, senderKey, roomKeyContent.sessionId) onNewSession(roomKeyContent.roomId, sessionInitiatorSenderKey, roomKeyContent.sessionId)
} }
} }

View file

@ -17,28 +17,36 @@
package org.matrix.android.sdk.internal.crypto.algorithms.megolm package org.matrix.android.sdk.internal.crypto.algorithms.megolm
import dagger.Lazy import dagger.Lazy
import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.StreamEventsManager import org.matrix.android.sdk.internal.session.StreamEventsManager
import org.matrix.android.sdk.internal.util.time.Clock
import javax.inject.Inject import javax.inject.Inject
internal class MXMegolmDecryptionFactory @Inject constructor( internal class MXMegolmDecryptionFactory @Inject constructor(
private val olmDevice: MXOlmDevice, private val olmDevice: MXOlmDevice,
@UserId private val myUserId: String,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager, private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
private val matrixConfiguration: MatrixConfiguration, private val eventsManager: Lazy<StreamEventsManager>,
private val eventsManager: Lazy<StreamEventsManager> private val unrequestedForwardManager: UnRequestedForwardManager,
private val mxCryptoConfig: MXCryptoConfig,
private val clock: Clock,
) { ) {
fun create(): MXMegolmDecryption { fun create(): MXMegolmDecryption {
return MXMegolmDecryption( return MXMegolmDecryption(
olmDevice, olmDevice = olmDevice,
outgoingKeyRequestManager, myUserId = myUserId,
cryptoStore, outgoingKeyRequestManager = outgoingKeyRequestManager,
matrixConfiguration, cryptoStore = cryptoStore,
eventsManager liveEventManager = eventsManager,
unrequestedForwardManager = unrequestedForwardManager,
cryptoConfig = mxCryptoConfig,
clock = clock,
) )
} }
} }

View file

@ -162,7 +162,8 @@ internal class MXMegolmEncryption(
forwardingCurve25519KeyChain = emptyList(), forwardingCurve25519KeyChain = emptyList(),
keysClaimed = keysClaimedMap, keysClaimed = keysClaimedMap,
exportFormat = false, exportFormat = false,
sharedHistory = sharedHistory sharedHistory = sharedHistory,
trusted = true
) )
defaultKeysBackupService.maybeBackupKeys() defaultKeysBackupService.maybeBackupKeys()

View file

@ -0,0 +1,150 @@
/*
* 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.algorithms.megolm
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
import timber.log.Timber
import java.util.concurrent.Executors
import javax.inject.Inject
import kotlin.math.abs
private val INVITE_VALIDITY_TIME_WINDOW_MILLIS = 10 * 60_000
@SessionScope
internal class UnRequestedForwardManager @Inject constructor(
private val deviceListManager: DeviceListManager,
) {
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val scope = CoroutineScope(SupervisorJob() + dispatcher)
private val sequencer = SemaphoreCoroutineSequencer()
// For now only in memory storage. Maybe we should persist? in case of gappy sync and long catchups?
private val forwardedKeysPerRoom = mutableMapOf<String, MutableMap<String, MutableList<ForwardInfo>>>()
data class InviteInfo(
val roomId: String,
val fromMxId: String,
val timestamp: Long
)
data class ForwardInfo(
val event: Event,
val timestamp: Long
)
// roomId, local timestamp of invite
private val recentInvites = mutableListOf<InviteInfo>()
fun close() {
try {
scope.cancel("User Terminate")
} catch (failure: Throwable) {
Timber.w(failure, "Failed to shutDown UnrequestedForwardManager")
}
}
fun onInviteReceived(roomId: String, fromUserId: String, localTimeStamp: Long) {
Timber.w("Invite received in room:$roomId from:$fromUserId at $localTimeStamp")
scope.launch {
sequencer.post {
if (!recentInvites.any { it.roomId == roomId && it.fromMxId == fromUserId }) {
recentInvites.add(
InviteInfo(
roomId,
fromUserId,
localTimeStamp
)
)
}
}
}
}
fun onUnRequestedKeyForward(roomId: String, event: Event, localTimeStamp: Long) {
Timber.w("Received unrequested forward in room:$roomId from:${event.senderId} at $localTimeStamp")
scope.launch {
sequencer.post {
val claimSenderId = event.senderId.orEmpty()
val senderKey = event.getSenderKey()
// we might want to download keys, as this user might not be known yet, cache is ok
val ownerMxId =
tryOrNull {
deviceListManager.downloadKeys(listOf(claimSenderId), false)
.map[claimSenderId]
?.values
?.firstOrNull { it.identityKey() == senderKey }
?.userId
}
// Not sure what to do if the device has been deleted? I can't proove the mxid
if (ownerMxId == null || claimSenderId != ownerMxId) {
Timber.w("Mismatch senderId between event and olm owner")
return@post
}
forwardedKeysPerRoom
.getOrPut(roomId) { mutableMapOf() }
.getOrPut(ownerMxId) { mutableListOf() }
.add(ForwardInfo(event, localTimeStamp))
}
}
}
fun postSyncProcessParkedKeysIfNeeded(currentTimestamp: Long, handleForwards: suspend (List<Event>) -> Unit) {
scope.launch {
sequencer.post {
// Prune outdated invites
recentInvites.removeAll { currentTimestamp - it.timestamp > INVITE_VALIDITY_TIME_WINDOW_MILLIS }
val cleanUpEvents = mutableListOf<Pair<String, String>>()
forwardedKeysPerRoom.forEach { (roomId, senderIdToForwardMap) ->
senderIdToForwardMap.forEach { (senderId, eventList) ->
// is there a matching invite in a valid timewindow?
val matchingInvite = recentInvites.firstOrNull { it.fromMxId == senderId && it.roomId == roomId }
if (matchingInvite != null) {
Timber.v("match for room:$roomId from sender:$senderId -> count =${eventList.size}")
eventList.filter {
abs(matchingInvite.timestamp - it.timestamp) <= INVITE_VALIDITY_TIME_WINDOW_MILLIS
}.map {
it.event
}.takeIf { it.isNotEmpty() }?.let {
Timber.w("Re-processing forwarded_room_key_event that was not requested after invite")
scope.launch {
handleForwards.invoke(it)
}
}
cleanUpEvents.add(roomId to senderId)
}
}
}
cleanUpEvents.forEach { roomIdToSenderPair ->
forwardedKeysPerRoom[roomIdToSenderPair.first]?.get(roomIdToSenderPair.second)?.clear()
}
}
}
}
}

View file

@ -652,14 +652,7 @@ internal class DefaultKeysBackupService @Inject constructor(
} }
val recoveryKey = computeRecoveryKey(secret.fromBase64()) val recoveryKey = computeRecoveryKey(secret.fromBase64())
if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) { 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 // 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) { withContext(coroutineDispatchers.crypto) {
cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version) cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
} }

View file

@ -38,9 +38,6 @@ data class InboundGroupSessionData(
@Json(name = "forwarding_curve25519_key_chain") @Json(name = "forwarding_curve25519_key_chain")
var forwardingCurve25519KeyChain: List<String>? = emptyList(), var forwardingCurve25519KeyChain: List<String>? = emptyList(),
/** Not yet used, will be in backup v2
val untrusted?: Boolean = false */
/** /**
* Flag that indicates whether or not the current inboundSession will be shared to * Flag that indicates whether or not the current inboundSession will be shared to
* invited users to decrypt past messages. * invited users to decrypt past messages.
@ -48,4 +45,10 @@ data class InboundGroupSessionData(
@Json(name = "shared_history") @Json(name = "shared_history")
val sharedHistory: Boolean = false, val sharedHistory: Boolean = false,
/**
* Flag indicating that this key is trusted.
*/
@Json(name = "trusted")
val trusted: Boolean? = null,
) )

View file

@ -86,6 +86,7 @@ data class MXInboundMegolmSessionWrapper(
keysClaimed = megolmSessionData.senderClaimedKeys, keysClaimed = megolmSessionData.senderClaimedKeys,
forwardingCurve25519KeyChain = megolmSessionData.forwardingCurve25519KeyChain, forwardingCurve25519KeyChain = megolmSessionData.forwardingCurve25519KeyChain,
sharedHistory = megolmSessionData.sharedHistory, sharedHistory = megolmSessionData.sharedHistory,
trusted = false
) )
return MXInboundMegolmSessionWrapper( return MXInboundMegolmSessionWrapper(

View file

@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo018
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.time.Clock
import javax.inject.Inject import javax.inject.Inject
@ -48,7 +49,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(
private val clock: Clock, private val clock: Clock,
) : MatrixRealmMigration( ) : MatrixRealmMigration(
dbName = "Crypto", dbName = "Crypto",
schemaVersion = 17L, schemaVersion = 18L,
) { ) {
/** /**
* Forces all RealmCryptoStoreMigration instances to be equal. * Forces all RealmCryptoStoreMigration instances to be equal.
@ -75,5 +76,6 @@ internal class RealmCryptoStoreMigration @Inject constructor(
if (oldVersion < 15) MigrateCryptoTo015(realm).perform() if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
if (oldVersion < 16) MigrateCryptoTo016(realm).perform() if (oldVersion < 16) MigrateCryptoTo016(realm).perform()
if (oldVersion < 17) MigrateCryptoTo017(realm).perform() if (oldVersion < 17) MigrateCryptoTo017(realm).perform()
if (oldVersion < 18) MigrateCryptoTo018(realm).perform()
} }
} }

View file

@ -0,0 +1,52 @@
/*
* 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.model.InboundGroupSessionData
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.util.database.RealmMigrator
import timber.log.Timber
/**
* This migration is adding support for trusted flags on megolm sessions.
* We can't really assert the trust of existing keys, so for the sake of simplicity we are going to
* mark existing keys as safe.
* This migration can take long depending on the account
*/
internal class MigrateCryptoTo018(realm: DynamicRealm) : RealmMigrator(realm, 18) {
private val moshiAdapter = MoshiProvider.providesMoshi().adapter(InboundGroupSessionData::class.java)
override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("OlmInboundGroupSessionEntity")
?.transform { dynamicObject ->
try {
dynamicObject.getString(OlmInboundGroupSessionEntityFields.INBOUND_GROUP_SESSION_DATA_JSON)?.let { oldData ->
moshiAdapter.fromJson(oldData)?.let { dataToMigrate ->
dataToMigrate.copy(trusted = true).let {
dynamicObject.setString(OlmInboundGroupSessionEntityFields.INBOUND_GROUP_SESSION_DATA_JSON, moshiAdapter.toJson(it))
}
}
}
} catch (failure: Throwable) {
Timber.e(failure, "Failed to migrate megolm session")
}
}
}
}

View file

@ -82,7 +82,8 @@ internal class DefaultEncryptEventTask @Inject constructor(
).toContent(), ).toContent(),
forwardingCurve25519KeyChain = emptyList(), forwardingCurve25519KeyChain = emptyList(),
senderCurve25519Key = result.eventContent["sender_key"] as? String, senderCurve25519Key = result.eventContent["sender_key"] as? String,
claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint() claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint(),
isSafe = true
) )
} else { } else {
null null

View file

@ -228,7 +228,8 @@ private fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEnt
payload = result.clearEvent, payload = result.clearEvent,
senderKey = result.senderCurve25519Key, senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
) )
// Save decryption result, to not decrypt every time we enter the thread list // Save decryption result, to not decrypt every time we enter the thread list
eventEntity.setDecryptionResult(result) eventEntity.setDecryptionResult(result)

View file

@ -87,7 +87,8 @@ internal open class EventEntity(
payload = result.clearEvent, payload = result.clearEvent,
senderKey = result.senderCurve25519Key, senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
) )
val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java)
decryptionResultJson = adapter.toJson(decryptionResult) decryptionResultJson = adapter.toJson(decryptionResult)

View file

@ -225,7 +225,8 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
payload = result.clearEvent, payload = result.clearEvent,
senderKey = result.senderCurve25519Key, senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
) )
} catch (e: MXCryptoError) { } catch (e: MXCryptoError) {
if (e is MXCryptoError.Base) { if (e is MXCryptoError.Base) {

View file

@ -56,7 +56,8 @@ internal class DefaultGetEventTask @Inject constructor(
payload = result.clearEvent, payload = result.clearEvent,
senderKey = result.senderCurve25519Key, senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
) )
} }
} }

View file

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.sync.handler package org.matrix.android.sdk.internal.session.sync.handler
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
@ -42,7 +43,9 @@ internal class CryptoSyncHandler @Inject constructor(
suspend fun handleToDevice(toDevice: ToDeviceSyncResponse, progressReporter: ProgressReporter? = null) { suspend fun handleToDevice(toDevice: ToDeviceSyncResponse, progressReporter: ProgressReporter? = null) {
val total = toDevice.events?.size ?: 0 val total = toDevice.events?.size ?: 0
toDevice.events?.forEachIndexed { index, event -> toDevice.events
?.filter { isSupportedToDevice(it) }
?.forEachIndexed { index, event ->
progressReporter?.reportProgress(index * 100F / total) progressReporter?.reportProgress(index * 100F / total)
// Decrypt event if necessary // Decrypt event if necessary
Timber.tag(loggerTag.value).i("To device event from ${event.senderId} of type:${event.type}") Timber.tag(loggerTag.value).i("To device event from ${event.senderId} of type:${event.type}")
@ -57,6 +60,28 @@ internal class CryptoSyncHandler @Inject constructor(
} }
} }
private val unsupportedPlainToDeviceEventTypes = listOf(
EventType.ROOM_KEY,
EventType.FORWARDED_ROOM_KEY,
EventType.SEND_SECRET
)
private fun isSupportedToDevice(event: Event): Boolean {
val algorithm = event.content?.get("algorithm") as? String
val type = event.type.orEmpty()
return if (event.isEncrypted()) {
algorithm == MXCRYPTO_ALGORITHM_OLM
} else {
// some clear events are not allowed
type !in unsupportedPlainToDeviceEventTypes
}.also {
if (!it) {
Timber.tag(loggerTag.value)
.w("Ignoring unsupported to device event ${event.type} alg:${algorithm}")
}
}
}
fun onSyncCompleted(syncResponse: SyncResponse) { fun onSyncCompleted(syncResponse: SyncResponse) {
cryptoService.onSyncCompleted(syncResponse) cryptoService.onSyncCompleted(syncResponse)
} }
@ -91,7 +116,8 @@ internal class CryptoSyncHandler @Inject constructor(
payload = result.clearEvent, payload = result.clearEvent,
senderKey = result.senderCurve25519Key, senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
) )
return true return true
} else { } else {

View file

@ -40,6 +40,7 @@ import org.matrix.android.sdk.api.session.sync.model.RoomSync
import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager
import org.matrix.android.sdk.internal.database.helper.addIfNecessary import org.matrix.android.sdk.internal.database.helper.addIfNecessary
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
import org.matrix.android.sdk.internal.database.helper.createOrUpdate import org.matrix.android.sdk.internal.database.helper.createOrUpdate
@ -99,6 +100,7 @@ internal class RoomSyncHandler @Inject constructor(
private val timelineInput: TimelineInput, private val timelineInput: TimelineInput,
private val liveEventService: Lazy<StreamEventsManager>, private val liveEventService: Lazy<StreamEventsManager>,
private val clock: Clock, private val clock: Clock,
private val unRequestedForwardManager: UnRequestedForwardManager,
) { ) {
sealed class HandlingStrategy { sealed class HandlingStrategy {
@ -322,6 +324,7 @@ internal class RoomSyncHandler @Inject constructor(
} }
roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.INVITE) roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.INVITE)
roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = true, inviterId = inviterEvent?.senderId, aggregator = aggregator) roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = true, inviterId = inviterEvent?.senderId, aggregator = aggregator)
unRequestedForwardManager.onInviteReceived(roomId, inviterEvent?.senderId.orEmpty(), clock.epochMillis())
return roomEntity return roomEntity
} }
@ -551,7 +554,8 @@ internal class RoomSyncHandler @Inject constructor(
payload = result.clearEvent, payload = result.clearEvent,
senderKey = result.senderCurve25519Key, senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
) )
} catch (e: MXCryptoError) { } catch (e: MXCryptoError) {
if (e is MXCryptoError.Base) { if (e is MXCryptoError.Base) {

View file

@ -0,0 +1,248 @@
/*
* 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 io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import org.amshove.kluent.fail
import org.amshove.kluent.shouldBe
import org.junit.Test
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent
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.UnsignedDeviceInfo
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.toContent
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager
class UnRequestedKeysManagerTest {
private val aliceMxId = "alice@example.com"
private val bobMxId = "bob@example.com"
private val bobDeviceId = "MKRJDSLYGA"
private val device1Id = "MGDAADVDMG"
private val aliceFirstDevice = CryptoDeviceInfo(
deviceId = device1Id,
userId = aliceMxId,
algorithms = MXCryptoAlgorithms.supportedAlgorithms(),
keys = mapOf(
"curve25519:$device1Id" to "yDa6cWOZ/WGBqm/JMUfTUCdEbAIzKHhuIcdDbnPEhDU",
"ed25519:$device1Id" to "XTge+TDwfm+WW10IGnaqEyLTSukPPzg3R1J1YvO1SBI",
),
signatures = mapOf(
aliceMxId to mapOf(
"ed25519:$device1Id" to "bPOAqM40+QSMgeEzUbYbPSZZccDDMUG00lCNdSXCoaS1gKKBGkSEaHO1OcibISIabjLYzmhp9mgtivz32fbABQ",
"ed25519:Ru4ni66dbQ6FZgUoHyyBtmjKecOHMvMSsSBZ2SABtt0" to "owzUsQ4Pvn35uEIc5FdVnXVRPzsVYBV8uJRUSqr4y8r5tp0DvrMArtJukKETgYEAivcZMT1lwNihHIN9xh06DA"
)
),
unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Web"),
trustLevel = DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true)
)
private val aBobDevice = CryptoDeviceInfo(
deviceId = bobDeviceId,
userId = bobMxId,
algorithms = MXCryptoAlgorithms.supportedAlgorithms(),
keys = mapOf(
"curve25519:$bobDeviceId" to "tWwg63Yfn//61Ylhir6z4QGejvo193E6MVHmURtYVn0",
"ed25519:$bobDeviceId" to "pS5NJ1LiVksQFX+p58NlphqMxE705laRVtUtZpYIAfs",
),
signatures = mapOf(
bobMxId to mapOf(
"ed25519:$bobDeviceId" to "zAJqsmOSzkx8EWXcrynCsWtbgWZifN7A6DLyEBs+ZPPLnNuPN5Jwzc1Rg+oZWZaRPvOPcSL0cgcxRegSBU0NBA",
)
),
unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Ios")
)
@Test
fun `test process key request if invite received`() {
val fakeDeviceListManager = mockk<DeviceListManager> {
coEvery { downloadKeys(any(), any()) } returns MXUsersDevicesMap<CryptoDeviceInfo>().apply {
setObject(bobMxId, bobDeviceId, aBobDevice)
}
}
val unrequestedForwardManager = UnRequestedForwardManager(fakeDeviceListManager)
val roomId = "someRoomId"
unrequestedForwardManager.onUnRequestedKeyForward(
roomId,
createFakeSuccessfullyDecryptedForwardToDevice(
aBobDevice,
aliceFirstDevice,
aBobDevice,
megolmSessionId = "megolmId1"
),
1_000
)
unrequestedForwardManager.onUnRequestedKeyForward(
roomId,
createFakeSuccessfullyDecryptedForwardToDevice(
aBobDevice,
aliceFirstDevice,
aBobDevice,
megolmSessionId = "megolmId2"
),
1_000
)
// for now no reason to accept
runBlocking {
unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(1000) {
fail("There should be no key to process")
}
}
// ACT
// suppose an invite is received but from another user
val inviteTime = 1_000L
unrequestedForwardManager.onInviteReceived(roomId, "@jhon:example.com", inviteTime)
// we shouldn't process the requests!
// runBlocking {
unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(inviteTime) {
fail("There should be no key to process")
}
// }
// ACT
// suppose an invite is received from correct user
unrequestedForwardManager.onInviteReceived(roomId, aBobDevice.userId, inviteTime)
runBlocking {
unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(inviteTime) {
it.size shouldBe 2
}
}
}
@Test
fun `test invite before keys`() {
val fakeDeviceListManager = mockk<DeviceListManager> {
coEvery { downloadKeys(any(), any()) } returns MXUsersDevicesMap<CryptoDeviceInfo>().apply {
setObject(bobMxId, bobDeviceId, aBobDevice)
}
}
val unrequestedForwardManager = UnRequestedForwardManager(fakeDeviceListManager)
val roomId = "someRoomId"
unrequestedForwardManager.onInviteReceived(roomId, aBobDevice.userId, 1_000)
unrequestedForwardManager.onUnRequestedKeyForward(
roomId,
createFakeSuccessfullyDecryptedForwardToDevice(
aBobDevice,
aliceFirstDevice,
aBobDevice,
megolmSessionId = "megolmId1"
),
1_000
)
runBlocking {
unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(1000) {
it.size shouldBe 1
}
}
}
@Test
fun `test validity window`() {
val fakeDeviceListManager = mockk<DeviceListManager> {
coEvery { downloadKeys(any(), any()) } returns MXUsersDevicesMap<CryptoDeviceInfo>().apply {
setObject(bobMxId, bobDeviceId, aBobDevice)
}
}
val unrequestedForwardManager = UnRequestedForwardManager(fakeDeviceListManager)
val roomId = "someRoomId"
val timeOfKeyReception = 1_000L
unrequestedForwardManager.onUnRequestedKeyForward(
roomId,
createFakeSuccessfullyDecryptedForwardToDevice(
aBobDevice,
aliceFirstDevice,
aBobDevice,
megolmSessionId = "megolmId1"
),
timeOfKeyReception
)
val currentTimeWindow = 10 * 60_000
// simulate very late invite
val inviteTime = timeOfKeyReception + currentTimeWindow + 1_000
unrequestedForwardManager.onInviteReceived(roomId, aBobDevice.userId, inviteTime)
runBlocking {
unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(inviteTime) {
fail("There should be no key to process")
}
}
}
private fun createFakeSuccessfullyDecryptedForwardToDevice(
sentBy: CryptoDeviceInfo,
dest: CryptoDeviceInfo,
sessionInitiator: CryptoDeviceInfo,
algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM,
roomId: String = "!zzgDlIhbWOevcdFBXr:example.com",
megolmSessionId: String = "Z/FSE8wDYheouGjGP9pezC4S1i39RtAXM3q9VXrBVZw"
): Event {
return Event(
type = EventType.ENCRYPTED,
eventId = "!fake",
senderId = sentBy.userId,
content = OlmEventContent(
ciphertext = mapOf(
dest.identityKey()!! to mapOf(
"type" to 0,
"body" to "AwogcziNF/tv60X0elsBmnKPN3+LTXr4K3vXw+1ZJ6jpTxESIJCmMMDvOA+"
)
),
senderKey = sentBy.identityKey()
).toContent(),
).apply {
mxDecryptionResult = OlmDecryptionResult(
payload = mapOf(
"type" to EventType.FORWARDED_ROOM_KEY,
"content" to ForwardedRoomKeyContent(
algorithm = algorithm,
roomId = roomId,
senderKey = sessionInitiator.identityKey(),
sessionId = megolmSessionId,
sessionKey = "AQAAAAAc4dK+lXxXyaFbckSxwjIEoIGDLKYovONJ7viWpwevhfvoBh+Q..."
).toContent()
),
senderKey = sentBy.identityKey()
)
}
}
}

View file

@ -22,6 +22,7 @@ import androidx.annotation.DrawableRes
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import im.vector.app.R import im.vector.app.R
import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
class ShieldImageView @JvmOverloads constructor( class ShieldImageView @JvmOverloads constructor(
@ -68,6 +69,39 @@ class ShieldImageView @JvmOverloads constructor(
null -> Unit null -> Unit
} }
} }
fun renderE2EDecoration(decoration: E2EDecoration?) {
isVisible = true
when (decoration) {
E2EDecoration.WARN_IN_CLEAR -> {
contentDescription = context.getString(R.string.unencrypted)
setImageResource(R.drawable.ic_shield_warning)
}
E2EDecoration.WARN_SENT_BY_UNVERIFIED -> {
contentDescription = context.getString(R.string.encrypted_unverified)
setImageResource(R.drawable.ic_shield_warning)
}
E2EDecoration.WARN_SENT_BY_UNKNOWN -> {
contentDescription = context.getString(R.string.encrypted_unverified)
setImageResource(R.drawable.ic_shield_warning)
}
E2EDecoration.WARN_SENT_BY_DELETED_SESSION -> {
contentDescription = context.getString(R.string.encrypted_unverified)
setImageResource(R.drawable.ic_shield_warning)
}
E2EDecoration.WARN_UNSAFE_KEY -> {
contentDescription = context.getString(R.string.key_authenticity_not_guaranteed)
setImageResource(
R.drawable.ic_shield_gray
)
}
E2EDecoration.NONE,
null -> {
contentDescription = null
isVisible = false
}
}
}
} }
@DrawableRes @DrawableRes

View file

@ -143,6 +143,14 @@ class MessageActionsEpoxyController @Inject constructor(
drawableStart(R.drawable.ic_shield_warning_small) drawableStart(R.drawable.ic_shield_warning_small)
} }
} }
E2EDecoration.WARN_UNSAFE_KEY -> {
bottomSheetSendStateItem {
id("e2e_unsafe")
showProgress(false)
text(host.stringProvider.getString(R.string.key_authenticity_not_guaranteed))
drawableStart(R.drawable.ic_shield_gray)
}
}
else -> { else -> {
// nothing // nothing
} }

View file

@ -83,7 +83,8 @@ class ViewEditHistoryViewModel @AssistedInject constructor(
payload = result.clearEvent, payload = result.clearEvent,
senderKey = result.senderCurve25519Key, senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
) )
} catch (e: MXCryptoError) { } catch (e: MXCryptoError) {
Timber.w("Failed to decrypt event in history") Timber.w("Failed to decrypt event in history")

View file

@ -31,7 +31,6 @@ import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.verification.VerificationState import org.matrix.android.sdk.api.session.crypto.verification.VerificationState
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.getMsgType import org.matrix.android.sdk.api.session.events.model.getMsgType
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.isSticker import org.matrix.android.sdk.api.session.events.model.isSticker
@ -146,29 +145,40 @@ class MessageInformationDataFactory @Inject constructor(
} }
private fun getE2EDecoration(roomSummary: RoomSummary?, event: TimelineEvent): E2EDecoration { private fun getE2EDecoration(roomSummary: RoomSummary?, event: TimelineEvent): E2EDecoration {
return if ( if (roomSummary?.isEncrypted != true) {
event.root.sendState == SendState.SYNCED && // No decoration for clear room
roomSummary?.isEncrypted.orFalse() && // Questionable? what if the event is E2E?
// is user verified return E2EDecoration.NONE
session.cryptoService().crossSigningService().getUserCrossSigningKeys(event.root.senderId ?: "")?.isTrusted() == true) { }
val ts = roomSummary?.encryptionEventTs ?: 0 if (event.root.sendState != SendState.SYNCED) {
val eventTs = event.root.originServerTs ?: 0 // we don't display e2e decoration if event not synced back
if (event.isEncrypted()) { return E2EDecoration.NONE
}
val userCrossSigningInfo = session.cryptoService()
.crossSigningService()
.getUserCrossSigningKeys(event.root.senderId.orEmpty())
if (userCrossSigningInfo?.isTrusted() == true) {
return if (event.isEncrypted()) {
// Do not decorate failed to decrypt, or redaction (we lost sender device info) // Do not decorate failed to decrypt, or redaction (we lost sender device info)
if (event.root.getClearType() == EventType.ENCRYPTED || event.root.isRedacted()) { if (event.root.getClearType() == EventType.ENCRYPTED || event.root.isRedacted()) {
E2EDecoration.NONE E2EDecoration.NONE
} else { } else {
val sendingDevice = event.root.content val sendingDevice = event.root.getSenderKey()
.toModel<EncryptedEventContent>() ?.let { it ->
?.deviceId session.cryptoService().deviceWithIdentityKey(
?.let { deviceId -> it,
session.cryptoService().getCryptoDeviceInfo(event.root.senderId ?: "", deviceId) event.root.content?.get("algorithm") as? String ?: ""
)
} }
if (event.root.mxDecryptionResult?.isSafe == false) {
E2EDecoration.WARN_UNSAFE_KEY
} else {
when { when {
sendingDevice == null -> { sendingDevice == null -> {
// For now do not decorate this with warning // For now do not decorate this with warning
// maybe it's a deleted session // maybe it's a deleted session
E2EDecoration.NONE E2EDecoration.WARN_SENT_BY_DELETED_SESSION
} }
sendingDevice.trustLevel == null -> { sendingDevice.trustLevel == null -> {
E2EDecoration.WARN_SENT_BY_UNKNOWN E2EDecoration.WARN_SENT_BY_UNKNOWN
@ -181,19 +191,35 @@ class MessageInformationDataFactory @Inject constructor(
} }
} }
} }
}
} else { } else {
e2EDecorationForClearEventInE2ERoom(event, roomSummary)
}
} else {
return if (!event.isEncrypted()) {
e2EDecorationForClearEventInE2ERoom(event, roomSummary)
} else if (event.root.mxDecryptionResult != null) {
if (event.root.mxDecryptionResult?.isSafe == true) {
E2EDecoration.NONE
} else {
E2EDecoration.WARN_UNSAFE_KEY
}
} else {
E2EDecoration.NONE
}
}
}
private fun e2EDecorationForClearEventInE2ERoom(event: TimelineEvent, roomSummary: RoomSummary) =
if (event.root.isStateEvent()) { if (event.root.isStateEvent()) {
// Do not warn for state event, they are always in clear // Do not warn for state event, they are always in clear
E2EDecoration.NONE E2EDecoration.NONE
} else { } else {
val ts = roomSummary.encryptionEventTs ?: 0
val eventTs = event.root.originServerTs ?: 0
// If event is in clear after the room enabled encryption we should warn // If event is in clear after the room enabled encryption we should warn
if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE
} }
}
} else {
E2EDecoration.NONE
}
}
/** /**
* Tiles type message never show the sender information (like verification request), so we should repeat it for next message * Tiles type message never show the sender information (like verification request), so we should repeat it for next message

View file

@ -40,7 +40,6 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
import im.vector.app.features.reactions.widget.ReactionButton import im.vector.app.features.reactions.widget.ReactionButton
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
private const val MAX_REACTIONS_TO_SHOW = 8 private const val MAX_REACTIONS_TO_SHOW = 8
@ -80,17 +79,7 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder>(@LayoutRes layo
override fun bind(holder: H) { override fun bind(holder: H) {
super.bind(holder) super.bind(holder)
renderReactions(holder, baseAttributes.informationData.reactionsSummary) renderReactions(holder, baseAttributes.informationData.reactionsSummary)
when (baseAttributes.informationData.e2eDecoration) { holder.e2EDecorationView.renderE2EDecoration(baseAttributes.informationData.e2eDecoration)
E2EDecoration.NONE -> {
holder.e2EDecorationView.render(null)
}
E2EDecoration.WARN_IN_CLEAR,
E2EDecoration.WARN_SENT_BY_UNVERIFIED,
E2EDecoration.WARN_SENT_BY_UNKNOWN -> {
holder.e2EDecorationView.render(RoomEncryptionTrustLevel.Warning)
}
}
holder.view.onClick(baseAttributes.itemClickListener) holder.view.onClick(baseAttributes.itemClickListener)
holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener) holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener)
(holder.view as? TimelineMessageLayoutRenderer)?.renderMessageLayout(baseAttributes.informationData.messageLayout) (holder.view as? TimelineMessageLayoutRenderer)?.renderMessageLayout(baseAttributes.informationData.messageLayout)

View file

@ -106,7 +106,9 @@ enum class E2EDecoration {
NONE, NONE,
WARN_IN_CLEAR, WARN_IN_CLEAR,
WARN_SENT_BY_UNVERIFIED, WARN_SENT_BY_UNVERIFIED,
WARN_SENT_BY_UNKNOWN WARN_SENT_BY_UNKNOWN,
WARN_SENT_BY_DELETED_SESSION,
WARN_UNSAFE_KEY
} }
enum class SendStateDecoration { enum class SendStateDecoration {

View file

@ -213,7 +213,8 @@ class NotifiableEventResolver @Inject constructor(
payload = result.clearEvent, payload = result.clearEvent,
senderKey = result.senderCurve25519Key, senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
) )
} catch (ignore: MXCryptoError) { } catch (ignore: MXCryptoError) {
} }

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:strokeWidth="1"
android:pathData="M12.0077,23.4869C12.0051,23.4875 12.0025,23.4881 12,23.4886C11.9975,23.4881 11.9949,23.4875 11.9923,23.4869C11.9204,23.4706 11.8129,23.4452 11.6749,23.4092C11.3989,23.3373 11.0015,23.2235 10.5233,23.0575C9.5654,22.725 8.2921,22.186 7.0225,21.3608C4.4897,19.7145 2,16.954 2,12.405V3.4496L12,0.521L22,3.4496V12.405C22,16.954 19.5103,19.7145 16.9775,21.3608C15.7079,22.186 14.4346,22.725 13.4767,23.0575C12.9985,23.2235 12.6011,23.3373 12.3251,23.4092C12.1871,23.4452 12.0796,23.4706 12.0077,23.4869Z"
android:fillColor="@color/shield_color_gray"
android:strokeColor="#ffffff"/>
</vector>