Merge pull request #7248 from vector-im/feature/bca/hotfix_1.5.1_merge

Feature/bca/hotfix 1.5.1 merge back from main
This commit is contained in:
Benoit Marty 2022-09-29 11:58:36 +02:00 committed by GitHub
commit 214867ad0e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1027 additions and 254 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)
======================================

View file

@ -2617,6 +2617,7 @@
<string name="unencrypted">Unencrypted</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="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) -->

View file

@ -143,6 +143,7 @@
<color name="shield_color_trust">#0DBD8B</color>
<color name="shield_color_trust_background">#0F0DBD8B</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_background">#0FFF4B55</color>

View file

@ -40,7 +40,7 @@ import kotlin.coroutines.resume
class DeactivateAccountTest : InstrumentedTest {
@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))
// 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.auth.data.HomeServerConnectionConfig
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.events.model.EventType
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
* Registration, login, Sync, Sending messages...
*/
class CommonTestHelper internal constructor(context: Context) {
class CommonTestHelper internal constructor(context: Context, val cryptoConfig: MXCryptoConfig? = null) {
companion object {
@OptIn(ExperimentalCoroutinesApi::class)
internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CommonTestHelper) -> Unit) {
val testHelper = CommonTestHelper(context)
internal fun runSessionTest(context: Context, cryptoConfig: MXCryptoConfig? = null, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CommonTestHelper) -> Unit) {
val testHelper = CommonTestHelper(context, cryptoConfig)
return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) {
try {
withContext(Dispatchers.Default) {
@ -81,8 +82,8 @@ class CommonTestHelper internal constructor(context: Context) {
}
@OptIn(ExperimentalCoroutinesApi::class)
internal fun runCryptoTest(context: Context, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CryptoTestHelper, CommonTestHelper) -> Unit) {
val testHelper = CommonTestHelper(context)
internal fun runCryptoTest(context: Context, cryptoConfig: MXCryptoConfig? = null, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CryptoTestHelper, CommonTestHelper) -> Unit) {
val testHelper = CommonTestHelper(context, cryptoConfig)
val cryptoTestHelper = CryptoTestHelper(testHelper)
return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) {
try {
@ -114,6 +115,7 @@ class CommonTestHelper internal constructor(context: Context) {
applicationFlavor = "TestFlavor",
roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider(),
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>) {
sentEventIds.forEachIndexed { index, sentEventId ->
testHelper.retryPeriodically {
val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root
val event = session.getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(sentEventId)?.root
?: return@retryPeriodically false
try {
session.cryptoService().decryptEvent(event, "").let { result ->
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
)
}
} catch (error: MXCryptoError) {

View file

@ -33,9 +33,9 @@ import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
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.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.RequestResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
@ -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.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room
@ -130,7 +129,8 @@ class E2eeSanityTests : InstrumentedTest {
val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
timeLineEvent != null &&
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
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")
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
// newBobSession.cryptoService().getOutgoingRoomKeyRequests()
// .firstOrNull {
// it.sessionId ==
// }
// Try to request
sentEventIds.forEach { sentEventId ->
@ -359,31 +362,27 @@ class E2eeSanityTests : InstrumentedTest {
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)
sentEventIds.forEach { sentEventId ->
val megolmSessionId = newBobSession.getRoom(e2eRoomID)!!
.getTimelineEvent(sentEventId)!!
.root.content.toModel<EncryptedEventContent>()!!.sessionId
testHelper.retryPeriodically {
val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests()
.first {
it.sessionId == megolmSessionId &&
it.roomId == e2eRoomID
}
.results.also {
Log.w("##TEST", "result list is $it")
}
.firstOrNull { it.userId == aliceSession.myUserId }
?.result
aliceReply != null &&
aliceReply is RequestResult.Failure &&
WithHeldCode.UNAUTHORISED == aliceReply.code
}
}
// sentEventIds.forEach { sentEventId ->
// val megolmSessionId = newBobSession.getRoom(e2eRoomID)!!
// .getTimelineEvent(sentEventId)!!
// .root.content.toModel<EncryptedEventContent>()!!.sessionId
// testHelper.retryPeriodically {
// val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests()
// .first {
// it.sessionId == megolmSessionId &&
// it.roomId == e2eRoomID
// }
// .results.also {
// Log.w("##TEST", "result list is $it")
// }
// .firstOrNull { it.userId == aliceSession.myUserId }
// ?.result
// aliceReply != null &&
// aliceReply is RequestResult.Failure &&
// WithHeldCode.UNAUTHORISED == aliceReply.code
// }
// }
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
@ -405,7 +404,10 @@ class E2eeSanityTests : InstrumentedTest {
* Test that if a better key is forwarded (lower index, it is then used)
*/
@Test
fun testForwardBetterKey() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
fun testForwardBetterKey() = runCryptoTest(
context(),
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
val aliceSession = cryptoTestData.firstSession

View file

@ -77,6 +77,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
*/
private fun testShareHistoryWithRoomVisibility(roomHistoryVisibility: RoomHistoryVisibility? = null) =
runCryptoTest(context()) { cryptoTestHelper, testHelper ->
val aliceMessageText = "Hello Bob, I am Alice!"
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, roomHistoryVisibility)
val e2eRoomID = cryptoTestData.roomId
@ -96,7 +97,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2)
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)
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!!)
(timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE).also {
timelineEvent.root.getClearType() == EventType.MESSAGE &&
timelineEvent.root.mxDecryptionResult?.isSafe == true).also {
if (it) {
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!!)
(timelineEvent != null &&
timelineEvent.isEncrypted() &&
timelineEvent.root.getClearType() == EventType.MESSAGE
timelineEvent.root.getClearType() == EventType.MESSAGE &&
timelineEvent.root.mxDecryptionResult?.isSafe == false
).also {
if (it) {
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? {
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) {

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.UserPasswordAuth
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.session.crypto.MXCryptoError
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
*/
@Test
fun testUnwedging() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
fun testUnwedging() = runCryptoTest(
context(),
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
) { cryptoTestHelper, testHelper ->
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession

View file

@ -22,15 +22,16 @@ import androidx.test.filters.LargeTest
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import org.amshove.kluent.internal.assertEquals
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Assert
import org.junit.Assert.assertNull
import org.junit.FixMethodOrder
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.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.RequestResult
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.timeline.getLastMessageContent
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.TestConstants
import org.matrix.android.sdk.mustFail
@ -51,16 +51,15 @@ import org.matrix.android.sdk.mustFail
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@LargeTest
@Ignore
class KeyShareTests : InstrumentedTest {
@get:Rule val rule = RetryTestRule(3)
// @get:Rule val rule = RetryTestRule(3)
@Test
fun test_DoNotSelfShareIfNotTrusted() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
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
val roomId = aliceSession.roomService().createRoom(
@ -84,7 +83,7 @@ class KeyShareTests : InstrumentedTest {
aliceSession2.cryptoService().enableKeyGossiping(false)
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)
@ -115,7 +114,7 @@ class KeyShareTests : InstrumentedTest {
outgoing != null
}
}
Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId")
Log.v("#TEST", "=======> Outgoing requet Id is $outGoingRequestId")
val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
@ -127,14 +126,17 @@ class KeyShareTests : InstrumentedTest {
// the request should be refused, because the device is not trusted
commonTestHelper.retryPeriodically {
// DEBUG LOGS
// aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
// Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
// Log.v("TEST", "=========================")
// it.forEach { keyRequest ->
// Log.v("TEST", "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
// }
// Log.v("TEST", "=========================")
// }
aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
Log.v("#TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
Log.v("#TEST", "=========================")
it.forEach { keyRequest ->
Log.v(
"#TEST",
"[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}"
)
}
Log.v("#TEST", "=========================")
}
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
incoming != null
@ -143,10 +145,10 @@ class KeyShareTests : InstrumentedTest {
commonTestHelper.retryPeriodically {
// DEBUG LOGS
aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest ->
Log.v("TEST", "=========================")
Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
Log.v("TEST", "=========================")
Log.v("#TEST", "=========================")
Log.v("#TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
Log.v("#TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
Log.v("#TEST", "=========================")
}
val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
@ -160,11 +162,24 @@ class KeyShareTests : InstrumentedTest {
}
// 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(
DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId,
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
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
@ -181,7 +196,10 @@ class KeyShareTests : InstrumentedTest {
* if the key was originally shared with him
*/
@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 aliceSession = testData.firstSession
@ -210,7 +228,10 @@ class KeyShareTests : InstrumentedTest {
* if the key was originally shared with him
*/
@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 aliceSession = testData.firstSession
@ -226,7 +247,6 @@ class KeyShareTests : InstrumentedTest {
}
val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first()
val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
// Let's try to request any how.
// As it was share previously alice should accept to reshare
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
*/
@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 aliceSession = testData.firstSession
@ -309,6 +332,9 @@ class KeyShareTests : InstrumentedTest {
aliceSession.cryptoService()
.verificationService()
.markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!)
aliceNewSession.cryptoService()
.verificationService()
.markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!)
// Let's now try to request
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
*/
@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 aliceSession = testData.firstSession
val bobSession = testData.secondSession!!
@ -391,6 +420,9 @@ class KeyShareTests : InstrumentedTest {
aliceSession.cryptoService()
.verificationService()
.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
aliceSession.cryptoService().enableKeyGossiping(false)

View file

@ -27,6 +27,7 @@ import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
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.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.RequestResult
@ -143,7 +144,10 @@ class WithHeldTests : InstrumentedTest {
}
@Test
fun test_WithHeldNoOlm() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
fun test_WithHeldNoOlm() = runCryptoTest(
context(),
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
) { cryptoTestHelper, testHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = testData.firstSession
@ -217,7 +221,10 @@ class WithHeldTests : InstrumentedTest {
}
@Test
fun test_WithHeldKeyRequest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
fun test_WithHeldKeyRequest() = runCryptoTest(
context(),
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
) { cryptoTestHelper, testHelper ->
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
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.
* 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
* 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).
*/
@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
*/
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
*/
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")
return mxDecryptionResult?.payload?.get("content") as? Content ?: content
return mxDecryptionResult?.payload?.get("content") as? Content
}
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.IMXGroupEncryption
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.crosssigning.DefaultCrossSigningService
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
@ -183,7 +184,8 @@ internal class DefaultCryptoService @Inject constructor(
private val cryptoCoroutineScope: CoroutineScope,
private val eventDecryptor: EventDecryptor,
private val verificationMessageProcessor: VerificationMessageProcessor,
private val liveEventManager: Lazy<StreamEventsManager>
private val liveEventManager: Lazy<StreamEventsManager>,
private val unrequestedForwardManager: UnRequestedForwardManager,
) : CryptoService {
private val isStarting = AtomicBoolean(false)
@ -399,6 +401,7 @@ internal class DefaultCryptoService @Inject constructor(
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
incomingKeyRequestManager.close()
outgoingKeyRequestManager.close()
unrequestedForwardManager.close()
olmDevice.release()
cryptoStore.close()
}
@ -485,6 +488,14 @@ internal class DefaultCryptoService @Inject constructor(
// just for safety but should not throw
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)
}
}
}
}
}
}
@ -844,10 +855,12 @@ internal class DefaultCryptoService @Inject constructor(
* Handle a key event.
*
* @param event the key event.
* @param acceptUnrequested, if true it will force to accept unrequested keys.
*/
private fun onRoomKeyEvent(event: Event) {
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
Timber.tag(loggerTag.value).i("onRoomKeyEvent() from: ${event.senderId} type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>")
private fun onRoomKeyEvent(event: Event, acceptUnrequested: Boolean = false) {
val roomKeyContent = event.getDecryptedContent().toModel<RoomKeyContent>() ?: return
Timber.tag(loggerTag.value)
.i("onRoomKeyEvent(f:$acceptUnrequested) from: ${event.senderId} type<${event.getClearType()}> , session<${roomKeyContent.sessionId}>")
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : missing fields")
return
@ -857,7 +870,7 @@ internal class DefaultCryptoService @Inject constructor(
Timber.tag(loggerTag.value).e("GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
return
}
alg.onRoomKeyEvent(event, keysBackupService)
alg.onRoomKeyEvent(event, keysBackupService, acceptUnrequested)
}
private fun onKeyWithHeldReceived(event: Event) {
@ -950,6 +963,15 @@ internal class DefaultCryptoService @Inject constructor(
* @param event the membership event causing the change
*/
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
event.stateKey?.let { userId ->

View file

@ -91,6 +91,21 @@ internal class InboundGroupSessionStore @Inject constructor(
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
fun storeInBoundGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
internalStoreGroupSession(holder, sessionId, senderKey)

View file

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.sync.Mutex
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.logger.LoggerTag
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
@ -602,6 +603,7 @@ internal class MXOlmDevice @Inject constructor(
* @param keysClaimed Other keys the sender claims.
* @param exportFormat true if the megolm keys are in export format
* @param sharedHistory MSC3061, this key is sharable on invite
* @param trusted True if the key is coming from a trusted source
* @return true if the operation succeeds.
*/
fun addInboundGroupSession(
@ -612,7 +614,8 @@ internal class MXOlmDevice @Inject constructor(
forwardingCurve25519KeyChain: List<String>,
keysClaimed: Map<String, String>,
exportFormat: Boolean,
sharedHistory: Boolean
sharedHistory: Boolean,
trusted: Boolean
): AddSessionResult {
val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") {
if (exportFormat) {
@ -620,6 +623,8 @@ internal class MXOlmDevice @Inject constructor(
} else {
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) }
@ -631,31 +636,49 @@ internal class MXOlmDevice @Inject constructor(
val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex } ?: return AddSessionResult.NotImported.also {
// This is quite unexpected, could throw if native was released?
Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
candidateSession?.releaseSession()
candidateSession.releaseSession()
// 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 (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")
candidateSession?.releaseSession()
candidateSession.releaseSession()
return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt())
}
}
} catch (failure: Throwable) {
Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
candidateSession?.releaseSession()
candidateSession.releaseSession()
return AddSessionResult.NotImported
}
}
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 {
if (candidateSession.sessionIdentifier() != sessionId) {
Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
@ -674,6 +697,7 @@ internal class MXOlmDevice @Inject constructor(
keysClaimed = keysClaimed,
forwardingCurve25519KeyChain = forwardingCurve25519KeyChain,
sharedHistory = sharedHistory,
trusted = trusted
)
val wrapper = MXInboundMegolmSessionWrapper(
@ -689,6 +713,16 @@ internal class MXOlmDevice @Inject constructor(
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.
*
@ -821,7 +855,8 @@ internal class MXOlmDevice @Inject constructor(
payload,
wrapper.sessionData.keysClaimed,
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")
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?
if (toDevice.senderId != credentials.userId) {
if (sendingDevice.userId != credentials.userId) {
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}")
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 existingRequest = verifMutex.withLock {

View file

@ -41,6 +41,7 @@ internal interface IMXDecrypting {
*
* @param event the key event.
* @param defaultKeysBackupService the keys backup service
* @param forceAccept 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
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.session.crypto.MXCryptoError
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.store.IMXCryptoStore
import org.matrix.android.sdk.internal.session.StreamEventsManager
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO)
internal class MXMegolmDecryption(
private val olmDevice: MXOlmDevice,
private val myUserId: String,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
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 {
var newSessionListener: NewSessionListener? = null
@ -94,7 +99,8 @@ internal class MXMegolmDecryption(
senderCurve25519Key = olmDecryptionResult.senderKey,
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
.orEmpty()
.orEmpty(),
isSafe = olmDecryptionResult.isSafe.orFalse()
).also {
liveEventManager.get().dispatchLiveEventDecrypted(event, it)
}
@ -181,13 +187,23 @@ internal class MXMegolmDecryption(
*
* @param event the key event.
* @param defaultKeysBackupService the keys backup service
* @param forceAccept if true will force to accept the forwarded key
*/
override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {
Timber.tag(loggerTag.value).v("onRoomKeyEvent()")
override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean) {
Timber.tag(loggerTag.value).v("onRoomKeyEvent(${event.getSenderKey()})")
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()
val forwardingCurve25519KeyChain: MutableList<String> = ArrayList()
@ -195,32 +211,25 @@ internal class MXMegolmDecryption(
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : Key event is missing fields")
return
}
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
if (event.getDecryptedType() == EventType.FORWARDED_ROOM_KEY) {
if (!cryptoStore.isKeyGossipingEnabled()) {
Timber.tag(loggerTag.value)
.i("onRoomKeyEvent(), ignore forward adding as per crypto config : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
return
}
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>()
val forwardedRoomKeyContent = event.getDecryptedContent()?.toModel<ForwardedRoomKeyContent>()
?: return
forwardedRoomKeyContent.forwardingCurve25519KeyChain?.let {
forwardingCurve25519KeyChain.addAll(it)
}
if (senderKey == null) {
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : event is missing sender_key field")
return
}
forwardingCurve25519KeyChain.add(senderKey)
forwardingCurve25519KeyChain.add(eventSenderKey)
exportFormat = true
senderKey = forwardedRoomKeyContent.senderKey
if (null == senderKey) {
sessionInitiatorSenderKey = forwardedRoomKeyContent.senderKey ?: return Unit.also {
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : forwarded_room_key event is missing sender_key field")
return
}
if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) {
@ -229,13 +238,52 @@ internal class MXMegolmDecryption(
}
keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key
} else {
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
if (null == senderKey) {
Timber.tag(loggerTag.value).e("## onRoomKeyEvent() : key event has no sender key (not encrypted?)")
// checking if was requested once.
// should we check if the request is sort of active?
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
}
// 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
keysClaimed = event.getKeysClaimed().toMutableMap()
}
@ -245,12 +293,15 @@ internal class MXMegolmDecryption(
sessionId = roomKeyContent.sessionId,
sessionKey = roomKeyContent.sessionKey,
roomId = roomKeyContent.roomId,
senderKey = senderKey,
senderKey = sessionInitiatorSenderKey,
forwardingCurve25519KeyChain = forwardingCurve25519KeyChain,
keysClaimed = keysClaimed,
exportFormat = exportFormat,
sharedHistory = roomKeyContent.getSharedKey()
)
sharedHistory = roomKeyContent.getSharedKey(),
trusted = trusted
).also {
Timber.tag(loggerTag.value).v(".. onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId} result: $it")
}
when (addSessionResult) {
is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex
@ -258,35 +309,28 @@ internal class MXMegolmDecryption(
else -> null
}?.let { index ->
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
val fromDevice = (event.content?.get("sender_key") as? String)?.let { senderDeviceIdentityKey ->
cryptoStore.getUserDeviceList(event.senderId ?: "")
?.firstOrNull {
it.identityKey() == senderDeviceIdentityKey
}
}?.deviceId
outgoingKeyRequestManager.onRoomKeyForwarded(
sessionId = roomKeyContent.sessionId,
algorithm = roomKeyContent.algorithm ?: "",
roomId = roomKeyContent.roomId,
senderKey = senderKey,
senderKey = sessionInitiatorSenderKey,
fromIndex = index,
fromDevice = fromDevice,
fromDevice = fromDevice?.deviceId,
event = event
)
cryptoStore.saveIncomingForwardKeyAuditTrail(
roomId = roomKeyContent.roomId,
sessionId = roomKeyContent.sessionId,
senderKey = senderKey,
senderKey = sessionInitiatorSenderKey,
algorithm = roomKeyContent.algorithm ?: "",
userId = event.senderId ?: "",
deviceId = fromDevice ?: "",
userId = event.senderId.orEmpty(),
deviceId = fromDevice?.deviceId.orEmpty(),
chainIndex = index.toLong()
)
// The index is used to decide if we cancel sent request or if we wait for a better key
outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, senderKey, index)
outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, sessionInitiatorSenderKey, index)
}
}
@ -295,7 +339,7 @@ internal class MXMegolmDecryption(
.d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}")
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
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.OutgoingKeyRequestManager
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.util.time.Clock
import javax.inject.Inject
internal class MXMegolmDecryptionFactory @Inject constructor(
private val olmDevice: MXOlmDevice,
@UserId private val myUserId: String,
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
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 {
return MXMegolmDecryption(
olmDevice,
outgoingKeyRequestManager,
cryptoStore,
matrixConfiguration,
eventsManager
olmDevice = olmDevice,
myUserId = myUserId,
outgoingKeyRequestManager = outgoingKeyRequestManager,
cryptoStore = cryptoStore,
liveEventManager = eventsManager,
unrequestedForwardManager = unrequestedForwardManager,
cryptoConfig = mxCryptoConfig,
clock = clock,
)
}
}

View file

@ -162,7 +162,8 @@ internal class MXMegolmEncryption(
forwardingCurve25519KeyChain = emptyList(),
keysClaimed = keysClaimedMap,
exportFormat = false,
sharedHistory = sharedHistory
sharedHistory = sharedHistory,
trusted = true
)
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 const 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())
if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) {
awaitCallback<Unit> {
trustKeysBackupVersion(keysBackupVersion, true, it)
}
// we don't want to start immediately downloading all as it can take very long
// val importResult = awaitCallback<ImportRoomKeysResult> {
// restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it)
// }
withContext(coroutineDispatchers.crypto) {
cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
}

View file

@ -38,9 +38,6 @@ data class InboundGroupSessionData(
@Json(name = "forwarding_curve25519_key_chain")
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
* invited users to decrypt past messages.
@ -48,4 +45,10 @@ data class InboundGroupSessionData(
@Json(name = "shared_history")
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,
forwardingCurve25519KeyChain = megolmSessionData.forwardingCurve25519KeyChain,
sharedHistory = megolmSessionData.sharedHistory,
trusted = false
)
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.MigrateCryptoTo016
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.time.Clock
import javax.inject.Inject
@ -48,7 +49,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(
private val clock: Clock,
) : MatrixRealmMigration(
dbName = "Crypto",
schemaVersion = 17L,
schemaVersion = 18L,
) {
/**
* Forces all RealmCryptoStoreMigration instances to be equal.
@ -75,5 +76,6 @@ internal class RealmCryptoStoreMigration @Inject constructor(
if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
if (oldVersion < 16) MigrateCryptoTo016(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(),
forwardingCurve25519KeyChain = emptyList(),
senderCurve25519Key = result.eventContent["sender_key"] as? String,
claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint()
claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint(),
isSafe = true
)
} else {
null

View file

@ -228,7 +228,8 @@ private fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEnt
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
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
eventEntity.setDecryptionResult(result)

View file

@ -87,7 +87,8 @@ internal open class EventEntity(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
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)
decryptionResultJson = adapter.toJson(decryptionResult)

View file

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

View file

@ -56,7 +56,8 @@ internal class DefaultGetEventTask @Inject constructor(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
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
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
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.model.MXEventDecryptionResult
@ -42,7 +43,9 @@ internal class CryptoSyncHandler @Inject constructor(
suspend fun handleToDevice(toDevice: ToDeviceSyncResponse, progressReporter: ProgressReporter? = null) {
val total = toDevice.events?.size ?: 0
toDevice.events?.forEachIndexed { index, event ->
toDevice.events
?.filter { isSupportedToDevice(it) }
?.forEachIndexed { index, event ->
progressReporter?.reportProgress(index * 100F / total)
// Decrypt event if necessary
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) {
cryptoService.onSyncCompleted(syncResponse)
}
@ -91,7 +116,8 @@ internal class CryptoSyncHandler @Inject constructor(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
)
return true
} 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.settings.LightweightSettingsStorage
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.addTimelineEvent
import org.matrix.android.sdk.internal.database.helper.createOrUpdate
@ -99,6 +100,7 @@ internal class RoomSyncHandler @Inject constructor(
private val timelineInput: TimelineInput,
private val liveEventService: Lazy<StreamEventsManager>,
private val clock: Clock,
private val unRequestedForwardManager: UnRequestedForwardManager,
) {
sealed class HandlingStrategy {
@ -322,6 +324,7 @@ internal class RoomSyncHandler @Inject constructor(
}
roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.INVITE)
roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = true, inviterId = inviterEvent?.senderId, aggregator = aggregator)
unRequestedForwardManager.onInviteReceived(roomId, inviterEvent?.senderId.orEmpty(), clock.epochMillis())
return roomEntity
}
@ -551,7 +554,8 @@ internal class RoomSyncHandler @Inject constructor(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
)
} catch (e: MXCryptoError) {
if (e is MXCryptoError.Base) {

View file

@ -0,0 +1,250 @@
/*
* 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.core.view.isVisible
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
class ShieldImageView @JvmOverloads constructor(
@ -68,6 +69,39 @@ class ShieldImageView @JvmOverloads constructor(
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

View file

@ -143,6 +143,14 @@ class MessageActionsEpoxyController @Inject constructor(
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 -> {
// nothing
}

View file

@ -83,7 +83,8 @@ class ViewEditHistoryViewModel @AssistedInject constructor(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
)
} catch (e: MXCryptoError) {
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.crypto.verification.VerificationState
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.isAttachmentMessage
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 {
return if (
event.root.sendState == SendState.SYNCED &&
roomSummary?.isEncrypted.orFalse() &&
// is user verified
session.cryptoService().crossSigningService().getUserCrossSigningKeys(event.root.senderId ?: "")?.isTrusted() == true) {
val ts = roomSummary?.encryptionEventTs ?: 0
val eventTs = event.root.originServerTs ?: 0
if (event.isEncrypted()) {
if (roomSummary?.isEncrypted != true) {
// No decoration for clear room
// Questionable? what if the event is E2E?
return E2EDecoration.NONE
}
if (event.root.sendState != SendState.SYNCED) {
// we don't display e2e decoration if event not synced back
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)
if (event.root.getClearType() == EventType.ENCRYPTED || event.root.isRedacted()) {
E2EDecoration.NONE
} else {
val sendingDevice = event.root.content
.toModel<EncryptedEventContent>()
?.deviceId
?.let { deviceId ->
session.cryptoService().getCryptoDeviceInfo(event.root.senderId ?: "", deviceId)
val sendingDevice = event.root.getSenderKey()
?.let {
session.cryptoService().deviceWithIdentityKey(
it,
event.root.content?.get("algorithm") as? String ?: ""
)
}
if (event.root.mxDecryptionResult?.isSafe == false) {
E2EDecoration.WARN_UNSAFE_KEY
} else {
when {
sendingDevice == null -> {
// For now do not decorate this with warning
// maybe it's a deleted session
E2EDecoration.NONE
E2EDecoration.WARN_SENT_BY_DELETED_SESSION
}
sendingDevice.trustLevel == null -> {
E2EDecoration.WARN_SENT_BY_UNKNOWN
@ -181,19 +191,35 @@ class MessageInformationDataFactory @Inject constructor(
}
}
}
}
} 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()) {
// Do not warn for state event, they are always in clear
E2EDecoration.NONE
} 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 (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

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.reactions.widget.ReactionButton
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
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) {
super.bind(holder)
renderReactions(holder, baseAttributes.informationData.reactionsSummary)
when (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.e2EDecorationView.renderE2EDecoration(baseAttributes.informationData.e2eDecoration)
holder.view.onClick(baseAttributes.itemClickListener)
holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener)
(holder.view as? TimelineMessageLayoutRenderer)?.renderMessageLayout(baseAttributes.informationData.messageLayout)

View file

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

View file

@ -28,7 +28,6 @@ import im.vector.app.core.ui.views.ShieldImageView
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
@EpoxyModelClass
abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>(R.layout.item_timeline_event_base_noinfo) {
@ -43,16 +42,7 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>(R.layout.item_timel
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
holder.avatarImageView.onClick(attributes.avatarClickListener)
when (attributes.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.e2EDecorationView.renderE2EDecoration(attributes.informationData.e2eDecoration)
}
override fun unbind(holder: Holder) {

View file

@ -213,7 +213,8 @@ class NotifiableEventResolver @Inject constructor(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
isSafe = result.isSafe
)
} 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>