mirror of
https://github.com/element-hq/element-android
synced 2024-11-23 18:05:36 +03:00
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:
commit
214867ad0e
46 changed files with 1027 additions and 254 deletions
|
@ -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)
|
||||||
======================================
|
======================================
|
||||||
|
|
||||||
|
|
|
@ -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 you’re logged in</string>
|
<string name="review_logins">Review where you’re logged in</string>
|
||||||
<string name="verify_other_sessions">Verify all your sessions to ensure your account & messages are safe</string>
|
<string name="verify_other_sessions">Verify all your sessions to ensure your account & 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) -->
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,20 +97,21 @@ 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")
|
||||||
|
|
||||||
// Bob should be able to decrypt the message
|
// Bob should be able to decrypt the message
|
||||||
testHelper.retryPeriodically {
|
testHelper.retryPeriodically {
|
||||||
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 &&
|
||||||
if (it) {
|
timelineEvent.root.mxDecryptionResult?.isSafe == true).also {
|
||||||
Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
|
if (it) {
|
||||||
|
Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new user
|
// Create a new user
|
||||||
|
@ -134,15 +136,16 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
|
||||||
-> {
|
-> {
|
||||||
// Aris should be able to decrypt the message
|
// Aris should be able to decrypt the message
|
||||||
testHelper.retryPeriodically {
|
testHelper.retryPeriodically {
|
||||||
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 &&
|
||||||
).also {
|
timelineEvent.root.mxDecryptionResult?.isSafe == false
|
||||||
if (it) {
|
).also {
|
||||||
Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
|
if (it) {
|
||||||
|
Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RoomHistoryVisibility.INVITED,
|
RoomHistoryVisibility.INVITED,
|
||||||
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -844,10 +855,12 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
* Handle a key event.
|
* Handle a key event.
|
||||||
*
|
*
|
||||||
* @param event the key event.
|
* @param event the key event.
|
||||||
|
* @param acceptUnrequested, if true it will force to accept unrequested keys.
|
||||||
*/
|
*/
|
||||||
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(f:$acceptUnrequested) from: ${event.senderId} type<${event.getClearType()}> , session<${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 +870,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 +963,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 ->
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
@ -602,6 +603,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
* @param keysClaimed Other keys the sender claims.
|
* @param keysClaimed Other keys the sender claims.
|
||||||
* @param exportFormat true if the megolm keys are in export format
|
* @param exportFormat true if the megolm keys are in export format
|
||||||
* @param sharedHistory MSC3061, this key is sharable on invite
|
* @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.
|
* @return true if the operation succeeds.
|
||||||
*/
|
*/
|
||||||
fun addInboundGroupSession(
|
fun addInboundGroupSession(
|
||||||
|
@ -612,7 +614,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 +623,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 +636,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 }
|
||||||
// If our existing session is better we keep it
|
?: return AddSessionResult.NotImported.also {
|
||||||
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
|
candidateSession.releaseSession()
|
||||||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
|
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Failed to get new session index")
|
||||||
candidateSession?.releaseSession()
|
}
|
||||||
return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt())
|
|
||||||
|
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 (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()
|
||||||
|
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 +697,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 +713,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 +855,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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -41,6 +41,7 @@ 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
|
||||||
|
* @param forceAccept the keys backup service
|
||||||
*/
|
*/
|
||||||
fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {}
|
fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean = false) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -181,13 +187,23 @@ 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
|
||||||
|
* @param forceAccept if true will force to accept the forwarded key
|
||||||
*/
|
*/
|
||||||
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 +211,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 +238,52 @@ 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 +293,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 +309,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 +339,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,17 +43,41 @@ 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
|
||||||
progressReporter?.reportProgress(index * 100F / total)
|
?.filter { isSupportedToDevice(it) }
|
||||||
// Decrypt event if necessary
|
?.forEachIndexed { index, event ->
|
||||||
Timber.tag(loggerTag.value).i("To device event from ${event.senderId} of type:${event.type}")
|
progressReporter?.reportProgress(index * 100F / total)
|
||||||
decryptToDeviceEvent(event, null)
|
// Decrypt event if necessary
|
||||||
if (event.getClearType() == EventType.MESSAGE &&
|
Timber.tag(loggerTag.value).i("To device event from ${event.senderId} of type:${event.type}")
|
||||||
event.getClearContent()?.toModel<MessageContent>()?.msgType == "m.bad.encrypted") {
|
decryptToDeviceEvent(event, null)
|
||||||
Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}")
|
if (event.getClearType() == EventType.MESSAGE &&
|
||||||
} else {
|
event.getClearContent()?.toModel<MessageContent>()?.msgType == "m.bad.encrypted") {
|
||||||
verificationService.onToDeviceEvent(event)
|
Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}")
|
||||||
cryptoService.onToDeviceEvent(event)
|
} else {
|
||||||
|
verificationService.onToDeviceEvent(event)
|
||||||
|
cryptoService.onToDeviceEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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,55 +145,82 @@ 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 {
|
||||||
?.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 {
|
||||||
|
sendingDevice == null -> {
|
||||||
|
// For now do not decorate this with warning
|
||||||
|
// maybe it's a deleted session
|
||||||
|
E2EDecoration.WARN_SENT_BY_DELETED_SESSION
|
||||||
|
}
|
||||||
|
sendingDevice.trustLevel == null -> {
|
||||||
|
E2EDecoration.WARN_SENT_BY_UNKNOWN
|
||||||
|
}
|
||||||
|
sendingDevice.trustLevel?.isVerified().orFalse() -> {
|
||||||
|
E2EDecoration.NONE
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
E2EDecoration.WARN_SENT_BY_UNVERIFIED
|
||||||
}
|
}
|
||||||
when {
|
|
||||||
sendingDevice == null -> {
|
|
||||||
// For now do not decorate this with warning
|
|
||||||
// maybe it's a deleted session
|
|
||||||
E2EDecoration.NONE
|
|
||||||
}
|
|
||||||
sendingDevice.trustLevel == null -> {
|
|
||||||
E2EDecoration.WARN_SENT_BY_UNKNOWN
|
|
||||||
}
|
|
||||||
sendingDevice.trustLevel?.isVerified().orFalse() -> {
|
|
||||||
E2EDecoration.NONE
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
E2EDecoration.WARN_SENT_BY_UNVERIFIED
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (event.root.isStateEvent()) {
|
e2EDecorationForClearEventInE2ERoom(event, roomSummary)
|
||||||
// Do not warn for state event, they are always in clear
|
|
||||||
E2EDecoration.NONE
|
|
||||||
} else {
|
|
||||||
// If event is in clear after the room enabled encryption we should warn
|
|
||||||
if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
E2EDecoration.NONE
|
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
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
* even if same sender.
|
* even if same sender.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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.AvatarRenderer
|
||||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
|
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
|
||||||
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
|
||||||
|
|
||||||
@EpoxyModelClass
|
@EpoxyModelClass
|
||||||
abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>(R.layout.item_timeline_event_base_noinfo) {
|
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.view.setOnLongClickListener(attributes.itemLongClickListener)
|
||||||
holder.avatarImageView.onClick(attributes.avatarClickListener)
|
holder.avatarImageView.onClick(attributes.avatarClickListener)
|
||||||
|
|
||||||
when (attributes.informationData.e2eDecoration) {
|
holder.e2EDecorationView.renderE2EDecoration(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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unbind(holder: Holder) {
|
override fun unbind(holder: Holder) {
|
||||||
|
|
|
@ -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) {
|
||||||
}
|
}
|
||||||
|
|
11
vector/src/main/res/drawable/ic_shield_gray.xml
Normal file
11
vector/src/main/res/drawable/ic_shield_gray.xml
Normal 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>
|
Loading…
Reference in a new issue