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)
|
||||
======================================
|
||||
|
||||
|
|
|
@ -2617,6 +2617,7 @@
|
|||
|
||||
<string name="unencrypted">Unencrypted</string>
|
||||
<string name="encrypted_unverified">Encrypted by an unverified device</string>
|
||||
<string name="key_authenticity_not_guaranteed">The authenticity of this encrypted message can\'t be guaranteed on this device.</string>
|
||||
<string name="review_logins">Review where you’re logged in</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) -->
|
||||
|
|
|
@ -143,6 +143,7 @@
|
|||
<color name="shield_color_trust">#0DBD8B</color>
|
||||
<color name="shield_color_trust_background">#0F0DBD8B</color>
|
||||
<color name="shield_color_black">#17191C</color>
|
||||
<color name="shield_color_gray">#91A1C0</color>
|
||||
<color name="shield_color_warning">#FF4B55</color>
|
||||
<color name="shield_color_warning_background">#0FFF4B55</color>
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ import kotlin.coroutines.resume
|
|||
class DeactivateAccountTest : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun deactivateAccountTest() = runSessionTest(context(), false /* session will be deactivated */) { commonTestHelper ->
|
||||
fun deactivateAccountTest() = runSessionTest(context(), autoSignoutOnClose = false /* session will be deactivated */) { commonTestHelper ->
|
||||
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
|
||||
|
||||
// Deactivate the account
|
||||
|
|
|
@ -37,6 +37,7 @@ import org.matrix.android.sdk.api.MatrixConfiguration
|
|||
import org.matrix.android.sdk.api.SyncConfig
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
|
||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
|
@ -60,13 +61,13 @@ import kotlin.coroutines.suspendCoroutine
|
|||
* This class exposes methods to be used in common cases
|
||||
* Registration, login, Sync, Sending messages...
|
||||
*/
|
||||
class CommonTestHelper internal constructor(context: Context) {
|
||||
class CommonTestHelper internal constructor(context: Context, val cryptoConfig: MXCryptoConfig? = null) {
|
||||
|
||||
companion object {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CommonTestHelper) -> Unit) {
|
||||
val testHelper = CommonTestHelper(context)
|
||||
internal fun runSessionTest(context: Context, cryptoConfig: MXCryptoConfig? = null, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CommonTestHelper) -> Unit) {
|
||||
val testHelper = CommonTestHelper(context, cryptoConfig)
|
||||
return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) {
|
||||
try {
|
||||
withContext(Dispatchers.Default) {
|
||||
|
@ -81,8 +82,8 @@ class CommonTestHelper internal constructor(context: Context) {
|
|||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal fun runCryptoTest(context: Context, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CryptoTestHelper, CommonTestHelper) -> Unit) {
|
||||
val testHelper = CommonTestHelper(context)
|
||||
internal fun runCryptoTest(context: Context, cryptoConfig: MXCryptoConfig? = null, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CryptoTestHelper, CommonTestHelper) -> Unit) {
|
||||
val testHelper = CommonTestHelper(context, cryptoConfig)
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) {
|
||||
try {
|
||||
|
@ -114,6 +115,7 @@ class CommonTestHelper internal constructor(context: Context) {
|
|||
applicationFlavor = "TestFlavor",
|
||||
roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider(),
|
||||
syncConfig = SyncConfig(longPollTimeout = 5_000L),
|
||||
cryptoConfig = cryptoConfig ?: MXCryptoConfig()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -394,14 +394,16 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
|
|||
suspend fun ensureCanDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, messagesText: List<String>) {
|
||||
sentEventIds.forEachIndexed { index, sentEventId ->
|
||||
testHelper.retryPeriodically {
|
||||
val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root
|
||||
val event = session.getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(sentEventId)?.root
|
||||
?: return@retryPeriodically false
|
||||
try {
|
||||
session.cryptoService().decryptEvent(event, "").let { result ->
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
|
||||
isSafe = result.isSafe
|
||||
)
|
||||
}
|
||||
} catch (error: MXCryptoError) {
|
||||
|
|
|
@ -33,9 +33,9 @@ import org.junit.runner.RunWith
|
|||
import org.junit.runners.JUnit4
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.crypto.RequestResult
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
|
||||
|
@ -49,7 +49,6 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic
|
|||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
|
@ -130,7 +129,8 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
|
||||
timeLineEvent != null &&
|
||||
timeLineEvent.isEncrypted() &&
|
||||
timeLineEvent.root.getClearType() == EventType.MESSAGE
|
||||
timeLineEvent.root.getClearType() == EventType.MESSAGE &&
|
||||
timeLineEvent.root.mxDecryptionResult?.isSafe == true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -302,6 +302,13 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
|
||||
// ensure bob can now decrypt
|
||||
cryptoTestHelper.ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
|
||||
|
||||
// Check key trust
|
||||
sentEventIds.forEach { sentEventId ->
|
||||
val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)!!
|
||||
val result = newBobSession.cryptoService().decryptEvent(timelineEvent.root, "")
|
||||
assertEquals("Keys from history should be deniable", false, result.isSafe)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -348,10 +355,6 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
Log.v("#E2E TEST", "check that new bob can't currently decrypt")
|
||||
|
||||
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
|
||||
// newBobSession.cryptoService().getOutgoingRoomKeyRequests()
|
||||
// .firstOrNull {
|
||||
// it.sessionId ==
|
||||
// }
|
||||
|
||||
// Try to request
|
||||
sentEventIds.forEach { sentEventId ->
|
||||
|
@ -359,31 +362,27 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
newBobSession.cryptoService().requestRoomKeyForEvent(event)
|
||||
}
|
||||
|
||||
// wait a bit
|
||||
// we need to wait a couple of syncs to let sharing occurs
|
||||
// testHelper.waitFewSyncs(newBobSession, 6)
|
||||
|
||||
// Ensure that new bob still can't decrypt (keys must have been withheld)
|
||||
sentEventIds.forEach { sentEventId ->
|
||||
val megolmSessionId = newBobSession.getRoom(e2eRoomID)!!
|
||||
.getTimelineEvent(sentEventId)!!
|
||||
.root.content.toModel<EncryptedEventContent>()!!.sessionId
|
||||
testHelper.retryPeriodically {
|
||||
val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests()
|
||||
.first {
|
||||
it.sessionId == megolmSessionId &&
|
||||
it.roomId == e2eRoomID
|
||||
}
|
||||
.results.also {
|
||||
Log.w("##TEST", "result list is $it")
|
||||
}
|
||||
.firstOrNull { it.userId == aliceSession.myUserId }
|
||||
?.result
|
||||
aliceReply != null &&
|
||||
aliceReply is RequestResult.Failure &&
|
||||
WithHeldCode.UNAUTHORISED == aliceReply.code
|
||||
}
|
||||
}
|
||||
// sentEventIds.forEach { sentEventId ->
|
||||
// val megolmSessionId = newBobSession.getRoom(e2eRoomID)!!
|
||||
// .getTimelineEvent(sentEventId)!!
|
||||
// .root.content.toModel<EncryptedEventContent>()!!.sessionId
|
||||
// testHelper.retryPeriodically {
|
||||
// val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests()
|
||||
// .first {
|
||||
// it.sessionId == megolmSessionId &&
|
||||
// it.roomId == e2eRoomID
|
||||
// }
|
||||
// .results.also {
|
||||
// Log.w("##TEST", "result list is $it")
|
||||
// }
|
||||
// .firstOrNull { it.userId == aliceSession.myUserId }
|
||||
// ?.result
|
||||
// aliceReply != null &&
|
||||
// aliceReply is RequestResult.Failure &&
|
||||
// WithHeldCode.UNAUTHORISED == aliceReply.code
|
||||
// }
|
||||
// }
|
||||
|
||||
cryptoTestHelper.ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, null)
|
||||
|
||||
|
@ -405,7 +404,10 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
* Test that if a better key is forwarded (lower index, it is then used)
|
||||
*/
|
||||
@Test
|
||||
fun testForwardBetterKey() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
fun testForwardBetterKey() = runCryptoTest(
|
||||
context(),
|
||||
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
|
||||
) { cryptoTestHelper, testHelper ->
|
||||
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
|
|
@ -77,6 +77,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
|
|||
*/
|
||||
private fun testShareHistoryWithRoomVisibility(roomHistoryVisibility: RoomHistoryVisibility? = null) =
|
||||
runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val aliceMessageText = "Hello Bob, I am Alice!"
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, roomHistoryVisibility)
|
||||
|
||||
val e2eRoomID = cryptoTestData.roomId
|
||||
|
@ -96,20 +97,21 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
|
|||
assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2)
|
||||
Log.v("#E2E TEST", "Alice and Bob are in roomId: $e2eRoomID")
|
||||
|
||||
val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, "Hello Bob, I am Alice!", testHelper)
|
||||
val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, aliceMessageText, testHelper)
|
||||
Assert.assertTrue("Message should be sent", aliceMessageId != null)
|
||||
Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID")
|
||||
|
||||
// Bob should be able to decrypt the message
|
||||
testHelper.retryPeriodically {
|
||||
val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
|
||||
(timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE).also {
|
||||
if (it) {
|
||||
Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
|
||||
val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
|
||||
(timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE &&
|
||||
timelineEvent.root.mxDecryptionResult?.isSafe == true).also {
|
||||
if (it) {
|
||||
Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new user
|
||||
|
@ -134,15 +136,16 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
|
|||
-> {
|
||||
// Aris should be able to decrypt the message
|
||||
testHelper.retryPeriodically {
|
||||
val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
|
||||
(timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
).also {
|
||||
if (it) {
|
||||
Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
|
||||
val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
|
||||
(timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE &&
|
||||
timelineEvent.root.mxDecryptionResult?.isSafe == false
|
||||
).also {
|
||||
if (it) {
|
||||
Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RoomHistoryVisibility.INVITED,
|
||||
|
@ -354,7 +357,10 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
|
|||
}
|
||||
|
||||
private suspend fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? {
|
||||
return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.eventId
|
||||
return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.let {
|
||||
Log.v("#E2E TEST", "Message sent with session ${it.root.content?.get("session_id")}")
|
||||
return it.eventId
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String, testHelper: CommonTestHelper) {
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth
|
|||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
|
@ -82,7 +83,10 @@ class UnwedgingTest : InstrumentedTest {
|
|||
* -> This is automatically fixed after SDKs restarted the olm session
|
||||
*/
|
||||
@Test
|
||||
fun testUnwedging() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
fun testUnwedging() = runCryptoTest(
|
||||
context(),
|
||||
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
|
||||
) { cryptoTestHelper, testHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
|
|
|
@ -22,15 +22,16 @@ import androidx.test.filters.LargeTest
|
|||
import junit.framework.TestCase.assertNotNull
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import org.amshove.kluent.internal.assertEquals
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
|
||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
||||
import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
|
||||
import org.matrix.android.sdk.api.session.crypto.RequestResult
|
||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
|
||||
|
@ -43,7 +44,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
|
|||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import org.matrix.android.sdk.common.RetryTestRule
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
import org.matrix.android.sdk.mustFail
|
||||
|
@ -51,16 +51,15 @@ import org.matrix.android.sdk.mustFail
|
|||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
@LargeTest
|
||||
@Ignore
|
||||
class KeyShareTests : InstrumentedTest {
|
||||
|
||||
@get:Rule val rule = RetryTestRule(3)
|
||||
// @get:Rule val rule = RetryTestRule(3)
|
||||
|
||||
@Test
|
||||
fun test_DoNotSelfShareIfNotTrusted() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
|
||||
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||
Log.v("TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}")
|
||||
Log.v("#TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}")
|
||||
|
||||
// Create an encrypted room and add a message
|
||||
val roomId = aliceSession.roomService().createRoom(
|
||||
|
@ -84,7 +83,7 @@ class KeyShareTests : InstrumentedTest {
|
|||
aliceSession2.cryptoService().enableKeyGossiping(false)
|
||||
commonTestHelper.syncSession(aliceSession2)
|
||||
|
||||
Log.v("TEST", "=======> AliceSession 2 is ${aliceSession2.sessionParams.deviceId}")
|
||||
Log.v("#TEST", "=======> AliceSession 2 is ${aliceSession2.sessionParams.deviceId}")
|
||||
|
||||
val roomSecondSessionPOV = aliceSession2.getRoom(roomId)
|
||||
|
||||
|
@ -115,7 +114,7 @@ class KeyShareTests : InstrumentedTest {
|
|||
outgoing != null
|
||||
}
|
||||
}
|
||||
Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId")
|
||||
Log.v("#TEST", "=======> Outgoing requet Id is $outGoingRequestId")
|
||||
|
||||
val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
||||
|
||||
|
@ -127,14 +126,17 @@ class KeyShareTests : InstrumentedTest {
|
|||
// the request should be refused, because the device is not trusted
|
||||
commonTestHelper.retryPeriodically {
|
||||
// DEBUG LOGS
|
||||
// aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
|
||||
// Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
|
||||
// Log.v("TEST", "=========================")
|
||||
// it.forEach { keyRequest ->
|
||||
// Log.v("TEST", "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
|
||||
// }
|
||||
// Log.v("TEST", "=========================")
|
||||
// }
|
||||
aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
|
||||
Log.v("#TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
|
||||
Log.v("#TEST", "=========================")
|
||||
it.forEach { keyRequest ->
|
||||
Log.v(
|
||||
"#TEST",
|
||||
"[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}"
|
||||
)
|
||||
}
|
||||
Log.v("#TEST", "=========================")
|
||||
}
|
||||
|
||||
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
|
||||
incoming != null
|
||||
|
@ -143,10 +145,10 @@ class KeyShareTests : InstrumentedTest {
|
|||
commonTestHelper.retryPeriodically {
|
||||
// DEBUG LOGS
|
||||
aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest ->
|
||||
Log.v("TEST", "=========================")
|
||||
Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
|
||||
Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
|
||||
Log.v("TEST", "=========================")
|
||||
Log.v("#TEST", "=========================")
|
||||
Log.v("#TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
|
||||
Log.v("#TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
|
||||
Log.v("#TEST", "=========================")
|
||||
}
|
||||
|
||||
val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
|
||||
|
@ -160,11 +162,24 @@ class KeyShareTests : InstrumentedTest {
|
|||
}
|
||||
|
||||
// Mark the device as trusted
|
||||
|
||||
Log.v("#TEST", "=======> Alice device 1 is ${aliceSession.sessionParams.deviceId}|${aliceSession.cryptoService().getMyDevice().identityKey()}")
|
||||
val aliceSecondSession = aliceSession2.cryptoService().getMyDevice()
|
||||
Log.v("#TEST", "=======> Alice device 2 is ${aliceSession2.sessionParams.deviceId}|${aliceSecondSession.identityKey()}")
|
||||
|
||||
aliceSession.cryptoService().setDeviceVerification(
|
||||
DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId,
|
||||
aliceSession2.sessionParams.deviceId ?: ""
|
||||
)
|
||||
|
||||
// We only accept forwards from trusted session, so we need to trust on other side to
|
||||
aliceSession2.cryptoService().setDeviceVerification(
|
||||
DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId,
|
||||
aliceSession.sessionParams.deviceId ?: ""
|
||||
)
|
||||
|
||||
aliceSession.cryptoService().deviceWithIdentityKey(aliceSecondSession.identityKey()!!, MXCRYPTO_ALGORITHM_OLM)!!.isVerified shouldBeEqualTo true
|
||||
|
||||
// Re request
|
||||
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
|
||||
|
||||
|
@ -181,7 +196,10 @@ class KeyShareTests : InstrumentedTest {
|
|||
* if the key was originally shared with him
|
||||
*/
|
||||
@Test
|
||||
fun test_reShareIfWasIntendedToBeShared() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
fun test_reShareIfWasIntendedToBeShared() = runCryptoTest(
|
||||
context(),
|
||||
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
|
||||
) { cryptoTestHelper, commonTestHelper ->
|
||||
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = testData.firstSession
|
||||
|
@ -210,7 +228,10 @@ class KeyShareTests : InstrumentedTest {
|
|||
* if the key was originally shared with him
|
||||
*/
|
||||
@Test
|
||||
fun test_reShareToUnverifiedIfWasIntendedToBeShared() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
fun test_reShareToUnverifiedIfWasIntendedToBeShared() = runCryptoTest(
|
||||
context(),
|
||||
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
|
||||
) { cryptoTestHelper, commonTestHelper ->
|
||||
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceInARoom(true)
|
||||
val aliceSession = testData.firstSession
|
||||
|
@ -226,7 +247,6 @@ class KeyShareTests : InstrumentedTest {
|
|||
}
|
||||
val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first()
|
||||
val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||
|
||||
// Let's try to request any how.
|
||||
// As it was share previously alice should accept to reshare
|
||||
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
|
||||
|
@ -243,7 +263,10 @@ class KeyShareTests : InstrumentedTest {
|
|||
* Tests that keys reshared with own verified session are done from the earliest known index
|
||||
*/
|
||||
@Test
|
||||
fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
fun test_reShareFromTheEarliestKnownIndexWithOwnVerifiedSession() = runCryptoTest(
|
||||
context(),
|
||||
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
|
||||
) { cryptoTestHelper, commonTestHelper ->
|
||||
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = testData.firstSession
|
||||
|
@ -309,6 +332,9 @@ class KeyShareTests : InstrumentedTest {
|
|||
aliceSession.cryptoService()
|
||||
.verificationService()
|
||||
.markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!)
|
||||
aliceNewSession.cryptoService()
|
||||
.verificationService()
|
||||
.markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!)
|
||||
|
||||
// Let's now try to request
|
||||
aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root)
|
||||
|
@ -353,7 +379,10 @@ class KeyShareTests : InstrumentedTest {
|
|||
* Tests that we don't cancel a request to early on first forward if the index is not good enough
|
||||
*/
|
||||
@Test
|
||||
fun test_dontCancelToEarly() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
fun test_dontCancelToEarly() = runCryptoTest(
|
||||
context(),
|
||||
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
|
||||
) { cryptoTestHelper, commonTestHelper ->
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
|
||||
val aliceSession = testData.firstSession
|
||||
val bobSession = testData.secondSession!!
|
||||
|
@ -391,6 +420,9 @@ class KeyShareTests : InstrumentedTest {
|
|||
aliceSession.cryptoService()
|
||||
.verificationService()
|
||||
.markedLocallyAsManuallyVerified(aliceNewSession.myUserId, aliceNewSession.sessionParams.deviceId!!)
|
||||
aliceNewSession.cryptoService()
|
||||
.verificationService()
|
||||
.markedLocallyAsManuallyVerified(aliceSession.myUserId, aliceSession.sessionParams.deviceId!!)
|
||||
|
||||
// /!\ Stop initial alice session syncing so that it can't reply
|
||||
aliceSession.cryptoService().enableKeyGossiping(false)
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.junit.runner.RunWith
|
|||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.crypto.RequestResult
|
||||
|
@ -143,7 +144,10 @@ class WithHeldTests : InstrumentedTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun test_WithHeldNoOlm() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
fun test_WithHeldNoOlm() = runCryptoTest(
|
||||
context(),
|
||||
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
|
||||
) { cryptoTestHelper, testHelper ->
|
||||
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
val aliceSession = testData.firstSession
|
||||
|
@ -217,7 +221,10 @@ class WithHeldTests : InstrumentedTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun test_WithHeldKeyRequest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
fun test_WithHeldKeyRequest() = runCryptoTest(
|
||||
context(),
|
||||
cryptoConfig = MXCryptoConfig(limitRoomKeyRequestsToMyDevices = false)
|
||||
) { cryptoTestHelper, testHelper ->
|
||||
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
val aliceSession = testData.firstSession
|
||||
|
|
|
@ -35,8 +35,9 @@ data class MXCryptoConfig constructor(
|
|||
|
||||
/**
|
||||
* Currently megolm keys are requested to the sender device and to all of our devices.
|
||||
* You can limit request only to your sessions by turning this setting to `true`
|
||||
* You can limit request only to your sessions by turning this setting to `true`.
|
||||
* Forwarded keys coming from other users will also be ignored if set to true.
|
||||
*/
|
||||
val limitRoomKeyRequestsToMyDevices: Boolean = false,
|
||||
val limitRoomKeyRequestsToMyDevices: Boolean = true,
|
||||
|
||||
)
|
||||
|
|
|
@ -43,5 +43,7 @@ data class MXEventDecryptionResult(
|
|||
* List of curve25519 keys involved in telling us about the senderCurve25519Key and
|
||||
* claimedEd25519Key. See MXEvent.forwardingCurve25519KeyChain.
|
||||
*/
|
||||
val forwardingCurve25519KeyChain: List<String> = emptyList()
|
||||
val forwardingCurve25519KeyChain: List<String> = emptyList(),
|
||||
|
||||
val isSafe: Boolean = false
|
||||
)
|
||||
|
|
|
@ -44,5 +44,10 @@ data class OlmDecryptionResult(
|
|||
/**
|
||||
* Devices which forwarded this session to us (normally empty).
|
||||
*/
|
||||
@Json(name = "forwardingCurve25519KeyChain") val forwardingCurve25519KeyChain: List<String>? = null
|
||||
@Json(name = "forwardingCurve25519KeyChain") val forwardingCurve25519KeyChain: List<String>? = null,
|
||||
|
||||
/**
|
||||
* True if the key used to decrypt is considered safe (trusted).
|
||||
*/
|
||||
@Json(name = "key_safety") val isSafe: Boolean? = null,
|
||||
)
|
||||
|
|
|
@ -174,15 +174,29 @@ data class Event(
|
|||
* @return the event type
|
||||
*/
|
||||
fun getClearType(): String {
|
||||
return mxDecryptionResult?.payload?.get("type")?.toString() ?: type ?: EventType.MISSING_TYPE
|
||||
return getDecryptedType() ?: type ?: EventType.MISSING_TYPE
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The decrypted type, or null. Won't fallback to the wired type
|
||||
*/
|
||||
fun getDecryptedType(): String? {
|
||||
return mxDecryptionResult?.payload?.get("type")?.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the event content
|
||||
*/
|
||||
fun getClearContent(): Content? {
|
||||
return getDecryptedContent() ?: content
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the decrypted event content or null, Won't fallback to the wired content
|
||||
*/
|
||||
fun getDecryptedContent(): Content? {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return mxDecryptionResult?.payload?.get("content") as? Content ?: content
|
||||
return mxDecryptionResult?.payload?.get("content") as? Content
|
||||
}
|
||||
|
||||
fun toContentStringWithIndent(): String {
|
||||
|
|
|
@ -79,6 +79,7 @@ import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationActio
|
|||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
||||
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||
|
@ -183,7 +184,8 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
private val cryptoCoroutineScope: CoroutineScope,
|
||||
private val eventDecryptor: EventDecryptor,
|
||||
private val verificationMessageProcessor: VerificationMessageProcessor,
|
||||
private val liveEventManager: Lazy<StreamEventsManager>
|
||||
private val liveEventManager: Lazy<StreamEventsManager>,
|
||||
private val unrequestedForwardManager: UnRequestedForwardManager,
|
||||
) : CryptoService {
|
||||
|
||||
private val isStarting = AtomicBoolean(false)
|
||||
|
@ -399,6 +401,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
|
||||
incomingKeyRequestManager.close()
|
||||
outgoingKeyRequestManager.close()
|
||||
unrequestedForwardManager.close()
|
||||
olmDevice.release()
|
||||
cryptoStore.close()
|
||||
}
|
||||
|
@ -485,6 +488,14 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
// just for safety but should not throw
|
||||
Timber.tag(loggerTag.value).w("failed to process incoming room key requests")
|
||||
}
|
||||
|
||||
unrequestedForwardManager.postSyncProcessParkedKeysIfNeeded(clock.epochMillis()) { events ->
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
events.forEach {
|
||||
onRoomKeyEvent(it, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -844,10 +855,12 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
* Handle a key event.
|
||||
*
|
||||
* @param event the key event.
|
||||
* @param acceptUnrequested, if true it will force to accept unrequested keys.
|
||||
*/
|
||||
private fun onRoomKeyEvent(event: Event) {
|
||||
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
|
||||
Timber.tag(loggerTag.value).i("onRoomKeyEvent() from: ${event.senderId} type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>")
|
||||
private fun onRoomKeyEvent(event: Event, acceptUnrequested: Boolean = false) {
|
||||
val roomKeyContent = event.getDecryptedContent().toModel<RoomKeyContent>() ?: return
|
||||
Timber.tag(loggerTag.value)
|
||||
.i("onRoomKeyEvent(f:$acceptUnrequested) from: ${event.senderId} type<${event.getClearType()}> , session<${roomKeyContent.sessionId}>")
|
||||
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
|
||||
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : missing fields")
|
||||
return
|
||||
|
@ -857,7 +870,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
Timber.tag(loggerTag.value).e("GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
|
||||
return
|
||||
}
|
||||
alg.onRoomKeyEvent(event, keysBackupService)
|
||||
alg.onRoomKeyEvent(event, keysBackupService, acceptUnrequested)
|
||||
}
|
||||
|
||||
private fun onKeyWithHeldReceived(event: Event) {
|
||||
|
@ -950,6 +963,15 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
* @param event the membership event causing the change
|
||||
*/
|
||||
private fun onRoomMembershipEvent(roomId: String, event: Event) {
|
||||
// because the encryption event can be after the join/invite in the same batch
|
||||
event.stateKey?.let { _ ->
|
||||
val roomMember: RoomMemberContent? = event.content.toModel()
|
||||
val membership = roomMember?.membership
|
||||
if (membership == Membership.INVITE) {
|
||||
unrequestedForwardManager.onInviteReceived(roomId, event.senderId.orEmpty(), clock.epochMillis())
|
||||
}
|
||||
}
|
||||
|
||||
roomEncryptorsStore.get(roomId) ?: /* No encrypting in this room */ return
|
||||
|
||||
event.stateKey?.let { userId ->
|
||||
|
|
|
@ -91,6 +91,21 @@ internal class InboundGroupSessionStore @Inject constructor(
|
|||
internalStoreGroupSession(new, sessionId, senderKey)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateToSafe(old: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
|
||||
Timber.tag(loggerTag.value).v("## updateToSafe for session ${old.wrapper.roomId}-${old.wrapper.senderKey}")
|
||||
|
||||
store.storeInboundGroupSessions(
|
||||
listOf(
|
||||
old.wrapper.copy(
|
||||
sessionData = old.wrapper.sessionData.copy(trusted = true)
|
||||
)
|
||||
)
|
||||
)
|
||||
// will release it :/
|
||||
sessionCache.remove(CacheKey(sessionId, senderKey))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun storeInBoundGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
|
||||
internalStoreGroupSession(holder, sessionId, senderKey)
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto
|
|||
import androidx.annotation.VisibleForTesting
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
|
@ -602,6 +603,7 @@ internal class MXOlmDevice @Inject constructor(
|
|||
* @param keysClaimed Other keys the sender claims.
|
||||
* @param exportFormat true if the megolm keys are in export format
|
||||
* @param sharedHistory MSC3061, this key is sharable on invite
|
||||
* @param trusted True if the key is coming from a trusted source
|
||||
* @return true if the operation succeeds.
|
||||
*/
|
||||
fun addInboundGroupSession(
|
||||
|
@ -612,7 +614,8 @@ internal class MXOlmDevice @Inject constructor(
|
|||
forwardingCurve25519KeyChain: List<String>,
|
||||
keysClaimed: Map<String, String>,
|
||||
exportFormat: Boolean,
|
||||
sharedHistory: Boolean
|
||||
sharedHistory: Boolean,
|
||||
trusted: Boolean
|
||||
): AddSessionResult {
|
||||
val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") {
|
||||
if (exportFormat) {
|
||||
|
@ -620,6 +623,8 @@ internal class MXOlmDevice @Inject constructor(
|
|||
} else {
|
||||
OlmInboundGroupSession(sessionKey)
|
||||
}
|
||||
} ?: return AddSessionResult.NotImported.also {
|
||||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : failed to import key candidate $senderKey/$sessionId")
|
||||
}
|
||||
|
||||
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
|
||||
|
@ -631,31 +636,49 @@ internal class MXOlmDevice @Inject constructor(
|
|||
val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex } ?: return AddSessionResult.NotImported.also {
|
||||
// This is quite unexpected, could throw if native was released?
|
||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
|
||||
candidateSession?.releaseSession()
|
||||
candidateSession.releaseSession()
|
||||
// Probably should discard it?
|
||||
}
|
||||
val newKnownFirstIndex = tryOrNull("Failed to get candidate first known index") { candidateSession?.firstKnownIndex }
|
||||
// If our existing session is better we keep it
|
||||
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
|
||||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
|
||||
candidateSession?.releaseSession()
|
||||
return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt())
|
||||
val newKnownFirstIndex = tryOrNull("Failed to get candidate first known index") { candidateSession.firstKnownIndex }
|
||||
?: return AddSessionResult.NotImported.also {
|
||||
candidateSession.releaseSession()
|
||||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Failed to get new session index")
|
||||
}
|
||||
|
||||
val keyConnects = existingSession.session.connects(candidateSession)
|
||||
if (!keyConnects) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.e("## addInboundGroupSession() Unconnected key")
|
||||
if (!trusted) {
|
||||
// Ignore the not connecting unsafe, keep existing
|
||||
Timber.tag(loggerTag.value)
|
||||
.e("## addInboundGroupSession() Received unsafe unconnected key")
|
||||
return AddSessionResult.NotImported
|
||||
}
|
||||
// else if the new one is safe and does not connect with existing, import the new one
|
||||
} else {
|
||||
// If our existing session is better we keep it
|
||||
if (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) {
|
||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
|
||||
candidateSession?.releaseSession()
|
||||
candidateSession.releaseSession()
|
||||
return AddSessionResult.NotImported
|
||||
}
|
||||
}
|
||||
|
||||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId")
|
||||
|
||||
// sanity check on the new session
|
||||
if (null == candidateSession) {
|
||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>")
|
||||
return AddSessionResult.NotImported
|
||||
}
|
||||
|
||||
try {
|
||||
if (candidateSession.sessionIdentifier() != sessionId) {
|
||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
||||
|
@ -674,6 +697,7 @@ internal class MXOlmDevice @Inject constructor(
|
|||
keysClaimed = keysClaimed,
|
||||
forwardingCurve25519KeyChain = forwardingCurve25519KeyChain,
|
||||
sharedHistory = sharedHistory,
|
||||
trusted = trusted
|
||||
)
|
||||
|
||||
val wrapper = MXInboundMegolmSessionWrapper(
|
||||
|
@ -689,6 +713,16 @@ internal class MXOlmDevice @Inject constructor(
|
|||
return AddSessionResult.Imported(candidateSession.firstKnownIndex.toInt())
|
||||
}
|
||||
|
||||
fun OlmInboundGroupSession.connects(other: OlmInboundGroupSession): Boolean {
|
||||
return try {
|
||||
val lowestCommonIndex = this.firstKnownIndex.coerceAtLeast(other.firstKnownIndex)
|
||||
this.export(lowestCommonIndex) == other.export(lowestCommonIndex)
|
||||
} catch (failure: Throwable) {
|
||||
// native error? key disposed?
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import an inbound group sessions to the session store.
|
||||
*
|
||||
|
@ -821,7 +855,8 @@ internal class MXOlmDevice @Inject constructor(
|
|||
payload,
|
||||
wrapper.sessionData.keysClaimed,
|
||||
senderKey,
|
||||
wrapper.sessionData.forwardingCurve25519KeyChain
|
||||
wrapper.sessionData.forwardingCurve25519KeyChain,
|
||||
isSafe = sessionHolder.wrapper.sessionData.trusted.orFalse()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -267,13 +267,24 @@ internal class SecretShareManager @Inject constructor(
|
|||
Timber.tag(loggerTag.value).e("onSecretSend() :Received unencrypted secret send event")
|
||||
return
|
||||
}
|
||||
// no need to download keys, after a verification we already forced download
|
||||
val sendingDevice = toDevice.getSenderKey()?.let { cryptoStore.deviceWithIdentityKey(it) }
|
||||
if (sendingDevice == null) {
|
||||
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from unknown device ${toDevice.getSenderKey()}")
|
||||
return
|
||||
}
|
||||
|
||||
// Was that sent by us?
|
||||
if (toDevice.senderId != credentials.userId) {
|
||||
if (sendingDevice.userId != credentials.userId) {
|
||||
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from other user ${toDevice.senderId}")
|
||||
return
|
||||
}
|
||||
|
||||
if (!sendingDevice.isVerified) {
|
||||
Timber.tag(loggerTag.value).e("onSecretSend() : Ignore secret from untrusted device ${toDevice.getSenderKey()}")
|
||||
return
|
||||
}
|
||||
|
||||
val secretContent = toDevice.getClearContent().toModel<SecretSendEventContent>() ?: return
|
||||
|
||||
val existingRequest = verifMutex.withLock {
|
||||
|
|
|
@ -41,6 +41,7 @@ internal interface IMXDecrypting {
|
|||
*
|
||||
* @param event the key event.
|
||||
* @param defaultKeysBackupService the keys backup service
|
||||
* @param forceAccept the keys backup service
|
||||
*/
|
||||
fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {}
|
||||
fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean = false) {}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||
|
||||
import dagger.Lazy
|
||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
||||
|
@ -34,16 +35,20 @@ import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
|
|||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.session.StreamEventsManager
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
import timber.log.Timber
|
||||
|
||||
private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO)
|
||||
|
||||
internal class MXMegolmDecryption(
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val myUserId: String,
|
||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val matrixConfiguration: MatrixConfiguration,
|
||||
private val liveEventManager: Lazy<StreamEventsManager>
|
||||
private val liveEventManager: Lazy<StreamEventsManager>,
|
||||
private val unrequestedForwardManager: UnRequestedForwardManager,
|
||||
private val cryptoConfig: MXCryptoConfig,
|
||||
private val clock: Clock,
|
||||
) : IMXDecrypting {
|
||||
|
||||
var newSessionListener: NewSessionListener? = null
|
||||
|
@ -94,7 +99,8 @@ internal class MXMegolmDecryption(
|
|||
senderCurve25519Key = olmDecryptionResult.senderKey,
|
||||
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
|
||||
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
|
||||
.orEmpty()
|
||||
.orEmpty(),
|
||||
isSafe = olmDecryptionResult.isSafe.orFalse()
|
||||
).also {
|
||||
liveEventManager.get().dispatchLiveEventDecrypted(event, it)
|
||||
}
|
||||
|
@ -181,13 +187,23 @@ internal class MXMegolmDecryption(
|
|||
*
|
||||
* @param event the key event.
|
||||
* @param defaultKeysBackupService the keys backup service
|
||||
* @param forceAccept if true will force to accept the forwarded key
|
||||
*/
|
||||
override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {
|
||||
Timber.tag(loggerTag.value).v("onRoomKeyEvent()")
|
||||
override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean) {
|
||||
Timber.tag(loggerTag.value).v("onRoomKeyEvent(${event.getSenderKey()})")
|
||||
var exportFormat = false
|
||||
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
|
||||
val roomKeyContent = event.getDecryptedContent()?.toModel<RoomKeyContent>() ?: return
|
||||
|
||||
val eventSenderKey: String = event.getSenderKey() ?: return Unit.also {
|
||||
Timber.tag(loggerTag.value).e("onRoom Key/Forward Event() : event is missing sender_key field")
|
||||
}
|
||||
|
||||
// this device might not been downloaded now?
|
||||
val fromDevice = cryptoStore.deviceWithIdentityKey(eventSenderKey)
|
||||
|
||||
lateinit var sessionInitiatorSenderKey: String
|
||||
val trusted: Boolean
|
||||
|
||||
var senderKey: String? = event.getSenderKey()
|
||||
var keysClaimed: MutableMap<String, String> = HashMap()
|
||||
val forwardingCurve25519KeyChain: MutableList<String> = ArrayList()
|
||||
|
||||
|
@ -195,32 +211,25 @@ internal class MXMegolmDecryption(
|
|||
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : Key event is missing fields")
|
||||
return
|
||||
}
|
||||
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
||||
if (event.getDecryptedType() == EventType.FORWARDED_ROOM_KEY) {
|
||||
if (!cryptoStore.isKeyGossipingEnabled()) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.i("onRoomKeyEvent(), ignore forward adding as per crypto config : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
|
||||
return
|
||||
}
|
||||
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
|
||||
val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>()
|
||||
val forwardedRoomKeyContent = event.getDecryptedContent()?.toModel<ForwardedRoomKeyContent>()
|
||||
?: return
|
||||
|
||||
forwardedRoomKeyContent.forwardingCurve25519KeyChain?.let {
|
||||
forwardingCurve25519KeyChain.addAll(it)
|
||||
}
|
||||
|
||||
if (senderKey == null) {
|
||||
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : event is missing sender_key field")
|
||||
return
|
||||
}
|
||||
|
||||
forwardingCurve25519KeyChain.add(senderKey)
|
||||
forwardingCurve25519KeyChain.add(eventSenderKey)
|
||||
|
||||
exportFormat = true
|
||||
senderKey = forwardedRoomKeyContent.senderKey
|
||||
if (null == senderKey) {
|
||||
sessionInitiatorSenderKey = forwardedRoomKeyContent.senderKey ?: return Unit.also {
|
||||
Timber.tag(loggerTag.value).e("onRoomKeyEvent() : forwarded_room_key event is missing sender_key field")
|
||||
return
|
||||
}
|
||||
|
||||
if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) {
|
||||
|
@ -229,13 +238,52 @@ internal class MXMegolmDecryption(
|
|||
}
|
||||
|
||||
keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
|
||||
if (null == senderKey) {
|
||||
Timber.tag(loggerTag.value).e("## onRoomKeyEvent() : key event has no sender key (not encrypted?)")
|
||||
|
||||
// checking if was requested once.
|
||||
// should we check if the request is sort of active?
|
||||
val wasNotRequested = cryptoStore.getOutgoingRoomKeyRequest(
|
||||
roomId = forwardedRoomKeyContent.roomId.orEmpty(),
|
||||
sessionId = forwardedRoomKeyContent.sessionId.orEmpty(),
|
||||
algorithm = forwardedRoomKeyContent.algorithm.orEmpty(),
|
||||
senderKey = forwardedRoomKeyContent.senderKey.orEmpty(),
|
||||
).isEmpty()
|
||||
|
||||
trusted = false
|
||||
|
||||
if (!forceAccept && wasNotRequested) {
|
||||
// val senderId = cryptoStore.deviceWithIdentityKey(event.getSenderKey().orEmpty())?.userId.orEmpty()
|
||||
unrequestedForwardManager.onUnRequestedKeyForward(roomKeyContent.roomId, event, clock.epochMillis())
|
||||
// Ignore unsolicited
|
||||
Timber.tag(loggerTag.value).w("Ignoring forwarded_room_key_event for ${roomKeyContent.sessionId} that was not requested")
|
||||
return
|
||||
}
|
||||
|
||||
// Check who sent the request, as we requested we have the device keys (no need to download)
|
||||
val sessionThatIsSharing = cryptoStore.deviceWithIdentityKey(eventSenderKey)
|
||||
if (sessionThatIsSharing == null) {
|
||||
Timber.tag(loggerTag.value).w("Ignoring forwarded_room_key from unknown device with identity $eventSenderKey")
|
||||
return
|
||||
}
|
||||
val isOwnDevice = myUserId == sessionThatIsSharing.userId
|
||||
val isDeviceVerified = sessionThatIsSharing.isVerified
|
||||
val isFromSessionInitiator = sessionThatIsSharing.identityKey() == sessionInitiatorSenderKey
|
||||
|
||||
val isLegitForward = (isOwnDevice && isDeviceVerified) ||
|
||||
(!cryptoConfig.limitRoomKeyRequestsToMyDevices && isFromSessionInitiator)
|
||||
|
||||
val shouldAcceptForward = forceAccept || isLegitForward
|
||||
|
||||
if (!shouldAcceptForward) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.w("Ignoring forwarded_room_key device:$eventSenderKey, ownVerified:{$isOwnDevice&&$isDeviceVerified}," +
|
||||
" fromInitiator:$isFromSessionInitiator")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// It's a m.room_key so safe
|
||||
trusted = true
|
||||
sessionInitiatorSenderKey = eventSenderKey
|
||||
Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}")
|
||||
// inherit the claimed ed25519 key from the setup message
|
||||
keysClaimed = event.getKeysClaimed().toMutableMap()
|
||||
}
|
||||
|
@ -245,12 +293,15 @@ internal class MXMegolmDecryption(
|
|||
sessionId = roomKeyContent.sessionId,
|
||||
sessionKey = roomKeyContent.sessionKey,
|
||||
roomId = roomKeyContent.roomId,
|
||||
senderKey = senderKey,
|
||||
senderKey = sessionInitiatorSenderKey,
|
||||
forwardingCurve25519KeyChain = forwardingCurve25519KeyChain,
|
||||
keysClaimed = keysClaimed,
|
||||
exportFormat = exportFormat,
|
||||
sharedHistory = roomKeyContent.getSharedKey()
|
||||
)
|
||||
sharedHistory = roomKeyContent.getSharedKey(),
|
||||
trusted = trusted
|
||||
).also {
|
||||
Timber.tag(loggerTag.value).v(".. onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId} result: $it")
|
||||
}
|
||||
|
||||
when (addSessionResult) {
|
||||
is MXOlmDevice.AddSessionResult.Imported -> addSessionResult.ratchetIndex
|
||||
|
@ -258,35 +309,28 @@ internal class MXMegolmDecryption(
|
|||
else -> null
|
||||
}?.let { index ->
|
||||
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
||||
val fromDevice = (event.content?.get("sender_key") as? String)?.let { senderDeviceIdentityKey ->
|
||||
cryptoStore.getUserDeviceList(event.senderId ?: "")
|
||||
?.firstOrNull {
|
||||
it.identityKey() == senderDeviceIdentityKey
|
||||
}
|
||||
}?.deviceId
|
||||
|
||||
outgoingKeyRequestManager.onRoomKeyForwarded(
|
||||
sessionId = roomKeyContent.sessionId,
|
||||
algorithm = roomKeyContent.algorithm ?: "",
|
||||
roomId = roomKeyContent.roomId,
|
||||
senderKey = senderKey,
|
||||
senderKey = sessionInitiatorSenderKey,
|
||||
fromIndex = index,
|
||||
fromDevice = fromDevice,
|
||||
fromDevice = fromDevice?.deviceId,
|
||||
event = event
|
||||
)
|
||||
|
||||
cryptoStore.saveIncomingForwardKeyAuditTrail(
|
||||
roomId = roomKeyContent.roomId,
|
||||
sessionId = roomKeyContent.sessionId,
|
||||
senderKey = senderKey,
|
||||
senderKey = sessionInitiatorSenderKey,
|
||||
algorithm = roomKeyContent.algorithm ?: "",
|
||||
userId = event.senderId ?: "",
|
||||
deviceId = fromDevice ?: "",
|
||||
userId = event.senderId.orEmpty(),
|
||||
deviceId = fromDevice?.deviceId.orEmpty(),
|
||||
chainIndex = index.toLong()
|
||||
)
|
||||
|
||||
// The index is used to decide if we cancel sent request or if we wait for a better key
|
||||
outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, senderKey, index)
|
||||
outgoingKeyRequestManager.postCancelRequestForSessionIfNeeded(roomKeyContent.sessionId, roomKeyContent.roomId, sessionInitiatorSenderKey, index)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -295,7 +339,7 @@ internal class MXMegolmDecryption(
|
|||
.d("onRoomKeyEvent(${event.getClearType()}) : Added megolm session ${roomKeyContent.sessionId} in ${roomKeyContent.roomId}")
|
||||
defaultKeysBackupService.maybeBackupKeys()
|
||||
|
||||
onNewSession(roomKeyContent.roomId, senderKey, roomKeyContent.sessionId)
|
||||
onNewSession(roomKeyContent.roomId, sessionInitiatorSenderKey, roomKeyContent.sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,28 +17,36 @@
|
|||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||
|
||||
import dagger.Lazy
|
||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||
import org.matrix.android.sdk.api.crypto.MXCryptoConfig
|
||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.session.StreamEventsManager
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class MXMegolmDecryptionFactory @Inject constructor(
|
||||
private val olmDevice: MXOlmDevice,
|
||||
@UserId private val myUserId: String,
|
||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val matrixConfiguration: MatrixConfiguration,
|
||||
private val eventsManager: Lazy<StreamEventsManager>
|
||||
private val eventsManager: Lazy<StreamEventsManager>,
|
||||
private val unrequestedForwardManager: UnRequestedForwardManager,
|
||||
private val mxCryptoConfig: MXCryptoConfig,
|
||||
private val clock: Clock,
|
||||
) {
|
||||
|
||||
fun create(): MXMegolmDecryption {
|
||||
return MXMegolmDecryption(
|
||||
olmDevice,
|
||||
outgoingKeyRequestManager,
|
||||
cryptoStore,
|
||||
matrixConfiguration,
|
||||
eventsManager
|
||||
olmDevice = olmDevice,
|
||||
myUserId = myUserId,
|
||||
outgoingKeyRequestManager = outgoingKeyRequestManager,
|
||||
cryptoStore = cryptoStore,
|
||||
liveEventManager = eventsManager,
|
||||
unrequestedForwardManager = unrequestedForwardManager,
|
||||
cryptoConfig = mxCryptoConfig,
|
||||
clock = clock,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -162,7 +162,8 @@ internal class MXMegolmEncryption(
|
|||
forwardingCurve25519KeyChain = emptyList(),
|
||||
keysClaimed = keysClaimedMap,
|
||||
exportFormat = false,
|
||||
sharedHistory = sharedHistory
|
||||
sharedHistory = sharedHistory,
|
||||
trusted = true
|
||||
)
|
||||
|
||||
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())
|
||||
if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) {
|
||||
awaitCallback<Unit> {
|
||||
trustKeysBackupVersion(keysBackupVersion, true, it)
|
||||
}
|
||||
// we don't want to start immediately downloading all as it can take very long
|
||||
|
||||
// val importResult = awaitCallback<ImportRoomKeysResult> {
|
||||
// restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it)
|
||||
// }
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
|
||||
}
|
||||
|
|
|
@ -38,9 +38,6 @@ data class InboundGroupSessionData(
|
|||
@Json(name = "forwarding_curve25519_key_chain")
|
||||
var forwardingCurve25519KeyChain: List<String>? = emptyList(),
|
||||
|
||||
/** Not yet used, will be in backup v2
|
||||
val untrusted?: Boolean = false */
|
||||
|
||||
/**
|
||||
* Flag that indicates whether or not the current inboundSession will be shared to
|
||||
* invited users to decrypt past messages.
|
||||
|
@ -48,4 +45,10 @@ data class InboundGroupSessionData(
|
|||
@Json(name = "shared_history")
|
||||
val sharedHistory: Boolean = false,
|
||||
|
||||
/**
|
||||
* Flag indicating that this key is trusted.
|
||||
*/
|
||||
@Json(name = "trusted")
|
||||
val trusted: Boolean? = null,
|
||||
|
||||
)
|
||||
|
|
|
@ -86,6 +86,7 @@ data class MXInboundMegolmSessionWrapper(
|
|||
keysClaimed = megolmSessionData.senderClaimedKeys,
|
||||
forwardingCurve25519KeyChain = megolmSessionData.forwardingCurve25519KeyChain,
|
||||
sharedHistory = megolmSessionData.sharedHistory,
|
||||
trusted = false
|
||||
)
|
||||
|
||||
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.MigrateCryptoTo016
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo018
|
||||
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
import javax.inject.Inject
|
||||
|
@ -48,7 +49,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(
|
|||
private val clock: Clock,
|
||||
) : MatrixRealmMigration(
|
||||
dbName = "Crypto",
|
||||
schemaVersion = 17L,
|
||||
schemaVersion = 18L,
|
||||
) {
|
||||
/**
|
||||
* Forces all RealmCryptoStoreMigration instances to be equal.
|
||||
|
@ -75,5 +76,6 @@ internal class RealmCryptoStoreMigration @Inject constructor(
|
|||
if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
|
||||
if (oldVersion < 16) MigrateCryptoTo016(realm).perform()
|
||||
if (oldVersion < 17) MigrateCryptoTo017(realm).perform()
|
||||
if (oldVersion < 18) MigrateCryptoTo018(realm).perform()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
forwardingCurve25519KeyChain = emptyList(),
|
||||
senderCurve25519Key = result.eventContent["sender_key"] as? String,
|
||||
claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint()
|
||||
claimedEd25519Key = cryptoService.get().getMyDevice().fingerprint(),
|
||||
isSafe = true
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
|
|
@ -228,7 +228,8 @@ private fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEnt
|
|||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
|
||||
isSafe = result.isSafe
|
||||
)
|
||||
// Save decryption result, to not decrypt every time we enter the thread list
|
||||
eventEntity.setDecryptionResult(result)
|
||||
|
|
|
@ -87,7 +87,8 @@ internal open class EventEntity(
|
|||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
|
||||
isSafe = result.isSafe
|
||||
)
|
||||
val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java)
|
||||
decryptionResultJson = adapter.toJson(decryptionResult)
|
||||
|
|
|
@ -225,7 +225,8 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
|
|||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
|
||||
isSafe = result.isSafe
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
if (e is MXCryptoError.Base) {
|
||||
|
|
|
@ -56,7 +56,8 @@ internal class DefaultGetEventTask @Inject constructor(
|
|||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
|
||||
isSafe = result.isSafe
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.sync.handler
|
||||
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
|
||||
|
@ -42,17 +43,41 @@ internal class CryptoSyncHandler @Inject constructor(
|
|||
|
||||
suspend fun handleToDevice(toDevice: ToDeviceSyncResponse, progressReporter: ProgressReporter? = null) {
|
||||
val total = toDevice.events?.size ?: 0
|
||||
toDevice.events?.forEachIndexed { index, event ->
|
||||
progressReporter?.reportProgress(index * 100F / total)
|
||||
// Decrypt event if necessary
|
||||
Timber.tag(loggerTag.value).i("To device event from ${event.senderId} of type:${event.type}")
|
||||
decryptToDeviceEvent(event, null)
|
||||
if (event.getClearType() == EventType.MESSAGE &&
|
||||
event.getClearContent()?.toModel<MessageContent>()?.msgType == "m.bad.encrypted") {
|
||||
Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}")
|
||||
} else {
|
||||
verificationService.onToDeviceEvent(event)
|
||||
cryptoService.onToDeviceEvent(event)
|
||||
toDevice.events
|
||||
?.filter { isSupportedToDevice(it) }
|
||||
?.forEachIndexed { index, event ->
|
||||
progressReporter?.reportProgress(index * 100F / total)
|
||||
// Decrypt event if necessary
|
||||
Timber.tag(loggerTag.value).i("To device event from ${event.senderId} of type:${event.type}")
|
||||
decryptToDeviceEvent(event, null)
|
||||
if (event.getClearType() == EventType.MESSAGE &&
|
||||
event.getClearContent()?.toModel<MessageContent>()?.msgType == "m.bad.encrypted") {
|
||||
Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}")
|
||||
} 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,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
|
||||
isSafe = result.isSafe
|
||||
)
|
||||
return true
|
||||
} 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.settings.LightweightSettingsStorage
|
||||
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.UnRequestedForwardManager
|
||||
import org.matrix.android.sdk.internal.database.helper.addIfNecessary
|
||||
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
|
||||
import org.matrix.android.sdk.internal.database.helper.createOrUpdate
|
||||
|
@ -99,6 +100,7 @@ internal class RoomSyncHandler @Inject constructor(
|
|||
private val timelineInput: TimelineInput,
|
||||
private val liveEventService: Lazy<StreamEventsManager>,
|
||||
private val clock: Clock,
|
||||
private val unRequestedForwardManager: UnRequestedForwardManager,
|
||||
) {
|
||||
|
||||
sealed class HandlingStrategy {
|
||||
|
@ -322,6 +324,7 @@ internal class RoomSyncHandler @Inject constructor(
|
|||
}
|
||||
roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.INVITE)
|
||||
roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = true, inviterId = inviterEvent?.senderId, aggregator = aggregator)
|
||||
unRequestedForwardManager.onInviteReceived(roomId, inviterEvent?.senderId.orEmpty(), clock.epochMillis())
|
||||
return roomEntity
|
||||
}
|
||||
|
||||
|
@ -551,7 +554,8 @@ internal class RoomSyncHandler @Inject constructor(
|
|||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
|
||||
isSafe = result.isSafe
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
if (e is MXCryptoError.Base) {
|
||||
|
|
|
@ -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.core.view.isVisible
|
||||
import im.vector.app.R
|
||||
import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
||||
|
||||
class ShieldImageView @JvmOverloads constructor(
|
||||
|
@ -68,6 +69,39 @@ class ShieldImageView @JvmOverloads constructor(
|
|||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
fun renderE2EDecoration(decoration: E2EDecoration?) {
|
||||
isVisible = true
|
||||
when (decoration) {
|
||||
E2EDecoration.WARN_IN_CLEAR -> {
|
||||
contentDescription = context.getString(R.string.unencrypted)
|
||||
setImageResource(R.drawable.ic_shield_warning)
|
||||
}
|
||||
E2EDecoration.WARN_SENT_BY_UNVERIFIED -> {
|
||||
contentDescription = context.getString(R.string.encrypted_unverified)
|
||||
setImageResource(R.drawable.ic_shield_warning)
|
||||
}
|
||||
E2EDecoration.WARN_SENT_BY_UNKNOWN -> {
|
||||
contentDescription = context.getString(R.string.encrypted_unverified)
|
||||
setImageResource(R.drawable.ic_shield_warning)
|
||||
}
|
||||
E2EDecoration.WARN_SENT_BY_DELETED_SESSION -> {
|
||||
contentDescription = context.getString(R.string.encrypted_unverified)
|
||||
setImageResource(R.drawable.ic_shield_warning)
|
||||
}
|
||||
E2EDecoration.WARN_UNSAFE_KEY -> {
|
||||
contentDescription = context.getString(R.string.key_authenticity_not_guaranteed)
|
||||
setImageResource(
|
||||
R.drawable.ic_shield_gray
|
||||
)
|
||||
}
|
||||
E2EDecoration.NONE,
|
||||
null -> {
|
||||
contentDescription = null
|
||||
isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
|
|
|
@ -143,6 +143,14 @@ class MessageActionsEpoxyController @Inject constructor(
|
|||
drawableStart(R.drawable.ic_shield_warning_small)
|
||||
}
|
||||
}
|
||||
E2EDecoration.WARN_UNSAFE_KEY -> {
|
||||
bottomSheetSendStateItem {
|
||||
id("e2e_unsafe")
|
||||
showProgress(false)
|
||||
text(host.stringProvider.getString(R.string.key_authenticity_not_guaranteed))
|
||||
drawableStart(R.drawable.ic_shield_gray)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// nothing
|
||||
}
|
||||
|
|
|
@ -83,7 +83,8 @@ class ViewEditHistoryViewModel @AssistedInject constructor(
|
|||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
|
||||
isSafe = result.isSafe
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.w("Failed to decrypt event in history")
|
||||
|
|
|
@ -31,7 +31,6 @@ import org.matrix.android.sdk.api.extensions.orFalse
|
|||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.verification.VerificationState
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.getMsgType
|
||||
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
|
||||
import org.matrix.android.sdk.api.session.events.model.isSticker
|
||||
|
@ -146,55 +145,82 @@ class MessageInformationDataFactory @Inject constructor(
|
|||
}
|
||||
|
||||
private fun getE2EDecoration(roomSummary: RoomSummary?, event: TimelineEvent): E2EDecoration {
|
||||
return if (
|
||||
event.root.sendState == SendState.SYNCED &&
|
||||
roomSummary?.isEncrypted.orFalse() &&
|
||||
// is user verified
|
||||
session.cryptoService().crossSigningService().getUserCrossSigningKeys(event.root.senderId ?: "")?.isTrusted() == true) {
|
||||
val ts = roomSummary?.encryptionEventTs ?: 0
|
||||
val eventTs = event.root.originServerTs ?: 0
|
||||
if (event.isEncrypted()) {
|
||||
if (roomSummary?.isEncrypted != true) {
|
||||
// No decoration for clear room
|
||||
// Questionable? what if the event is E2E?
|
||||
return E2EDecoration.NONE
|
||||
}
|
||||
if (event.root.sendState != SendState.SYNCED) {
|
||||
// we don't display e2e decoration if event not synced back
|
||||
return E2EDecoration.NONE
|
||||
}
|
||||
val userCrossSigningInfo = session.cryptoService()
|
||||
.crossSigningService()
|
||||
.getUserCrossSigningKeys(event.root.senderId.orEmpty())
|
||||
|
||||
if (userCrossSigningInfo?.isTrusted() == true) {
|
||||
return if (event.isEncrypted()) {
|
||||
// Do not decorate failed to decrypt, or redaction (we lost sender device info)
|
||||
if (event.root.getClearType() == EventType.ENCRYPTED || event.root.isRedacted()) {
|
||||
E2EDecoration.NONE
|
||||
} else {
|
||||
val sendingDevice = event.root.content
|
||||
.toModel<EncryptedEventContent>()
|
||||
?.deviceId
|
||||
?.let { deviceId ->
|
||||
session.cryptoService().getCryptoDeviceInfo(event.root.senderId ?: "", deviceId)
|
||||
val sendingDevice = event.root.getSenderKey()
|
||||
?.let {
|
||||
session.cryptoService().deviceWithIdentityKey(
|
||||
it,
|
||||
event.root.content?.get("algorithm") as? String ?: ""
|
||||
)
|
||||
}
|
||||
if (event.root.mxDecryptionResult?.isSafe == false) {
|
||||
E2EDecoration.WARN_UNSAFE_KEY
|
||||
} else {
|
||||
when {
|
||||
sendingDevice == null -> {
|
||||
// For now do not decorate this with warning
|
||||
// maybe it's a deleted session
|
||||
E2EDecoration.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 {
|
||||
if (event.root.isStateEvent()) {
|
||||
// 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
|
||||
}
|
||||
e2EDecorationForClearEventInE2ERoom(event, roomSummary)
|
||||
}
|
||||
} 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
|
||||
* 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.reactions.widget.ReactionButton
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
|
||||
private const val MAX_REACTIONS_TO_SHOW = 8
|
||||
|
@ -80,17 +79,7 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder>(@LayoutRes layo
|
|||
override fun bind(holder: H) {
|
||||
super.bind(holder)
|
||||
renderReactions(holder, baseAttributes.informationData.reactionsSummary)
|
||||
when (baseAttributes.informationData.e2eDecoration) {
|
||||
E2EDecoration.NONE -> {
|
||||
holder.e2EDecorationView.render(null)
|
||||
}
|
||||
E2EDecoration.WARN_IN_CLEAR,
|
||||
E2EDecoration.WARN_SENT_BY_UNVERIFIED,
|
||||
E2EDecoration.WARN_SENT_BY_UNKNOWN -> {
|
||||
holder.e2EDecorationView.render(RoomEncryptionTrustLevel.Warning)
|
||||
}
|
||||
}
|
||||
|
||||
holder.e2EDecorationView.renderE2EDecoration(baseAttributes.informationData.e2eDecoration)
|
||||
holder.view.onClick(baseAttributes.itemClickListener)
|
||||
holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener)
|
||||
(holder.view as? TimelineMessageLayoutRenderer)?.renderMessageLayout(baseAttributes.informationData.messageLayout)
|
||||
|
|
|
@ -106,7 +106,9 @@ enum class E2EDecoration {
|
|||
NONE,
|
||||
WARN_IN_CLEAR,
|
||||
WARN_SENT_BY_UNVERIFIED,
|
||||
WARN_SENT_BY_UNKNOWN
|
||||
WARN_SENT_BY_UNKNOWN,
|
||||
WARN_SENT_BY_DELETED_SESSION,
|
||||
WARN_UNSAFE_KEY
|
||||
}
|
||||
|
||||
enum class SendStateDecoration {
|
||||
|
|
|
@ -28,7 +28,6 @@ import im.vector.app.core.ui.views.ShieldImageView
|
|||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
|
||||
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
|
||||
|
||||
@EpoxyModelClass
|
||||
abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>(R.layout.item_timeline_event_base_noinfo) {
|
||||
|
@ -43,16 +42,7 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>(R.layout.item_timel
|
|||
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
holder.avatarImageView.onClick(attributes.avatarClickListener)
|
||||
|
||||
when (attributes.informationData.e2eDecoration) {
|
||||
E2EDecoration.NONE -> {
|
||||
holder.e2EDecorationView.render(null)
|
||||
}
|
||||
E2EDecoration.WARN_IN_CLEAR,
|
||||
E2EDecoration.WARN_SENT_BY_UNVERIFIED,
|
||||
E2EDecoration.WARN_SENT_BY_UNKNOWN -> {
|
||||
holder.e2EDecorationView.render(RoomEncryptionTrustLevel.Warning)
|
||||
}
|
||||
}
|
||||
holder.e2EDecorationView.renderE2EDecoration(attributes.informationData.e2eDecoration)
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
|
|
|
@ -213,7 +213,8 @@ class NotifiableEventResolver @Inject constructor(
|
|||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
|
||||
isSafe = result.isSafe
|
||||
)
|
||||
} catch (ignore: MXCryptoError) {
|
||||
}
|
||||
|
|
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