Merge branch 'develop' into feature/misleading_url_target

This commit is contained in:
Onuray Sahin 2020-04-23 20:18:44 +03:00 committed by GitHub
commit a80181da9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
74 changed files with 1976 additions and 572 deletions

View file

@ -25,6 +25,7 @@
<w>signup</w> <w>signup</w>
<w>ssss</w> <w>ssss</w>
<w>threepid</w> <w>threepid</w>
<w>unwedging</w>
</words> </words>
</dictionary> </dictionary>
</component> </component>

View file

@ -7,6 +7,7 @@ Features ✨:
- Cross-Signing | Verify new session from existing session (#1134) - Cross-Signing | Verify new session from existing session (#1134)
- Cross-Signing | Bootstraping cross signing with 4S from mobile (#985) - Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
- Save media files to Gallery (#973) - Save media files to Gallery (#973)
- Account deactivation (with password only) (#35)
Improvements 🙌: Improvements 🙌:
- Verification DM / Handle concurrent .start after .ready (#794) - Verification DM / Handle concurrent .start after .ready (#794)
@ -23,6 +24,8 @@ Improvements 🙌:
- Cross-Signing | Composer decoration: shields (#1077) - Cross-Signing | Composer decoration: shields (#1077)
- Cross-Signing | Migrate existing keybackup to cross signing with 4S from mobile (#1197) - Cross-Signing | Migrate existing keybackup to cross signing with 4S from mobile (#1197)
- Show a warning dialog if the text of the clicked link does not match the link target (#922) - Show a warning dialog if the text of the clicked link does not match the link target (#922)
- Restart broken Olm sessions ([MSC1719](https://github.com/matrix-org/matrix-doc/pull/1719))
- Cross-Signing | Hide Use recovery key when 4S is not setup (#1007)
Bugfix 🐛: Bugfix 🐛:
- Fix summary notification staying after "mark as read" - Fix summary notification staying after "mark as read"
@ -37,6 +40,7 @@ Bugfix 🐛:
- Render image event even if thumbnail_info does not have mimetype defined (#1209) - Render image event even if thumbnail_info does not have mimetype defined (#1209)
- RiotX now uses as many threads as it needs to do work and send messages (#1221) - RiotX now uses as many threads as it needs to do work and send messages (#1221)
- Fix issue with media path (#1227) - Fix issue with media path (#1227)
- Add user to direct chat by user id (#1065)
Translations 🗣: Translations 🗣:
- -

View file

@ -38,10 +38,10 @@ When the client receives the new information, it immediately sends another reque
This effectively emulates a server push feature. This effectively emulates a server push feature.
The HTTP long Polling can be fine tuned in the **SDK** using two parameters: The HTTP long Polling can be fine tuned in the **SDK** using two parameters:
* timout (Sync request timeout) * timeout (Sync request timeout)
* delay (Delay between each sync) * delay (Delay between each sync)
**timeout** is a server paramter, defined by: **timeout** is a server parameter, defined by:
``` ```
The maximum time to wait, in milliseconds, before returning this request.` The maximum time to wait, in milliseconds, before returning this request.`
If no events (or other data) become available before this time elapses, the server will return a response with empty fields. If no events (or other data) become available before this time elapses, the server will return a response with empty fields.

View file

@ -57,7 +57,7 @@ We get credential (200)
```json ```json
{ {
"user_id": "@benoit0816:matrix.org", "user_id": "@alice:matrix.org",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gfnYrSypfdTtkNXIuNWx1KgowMDJmc2lnbmF0dXJlIOsh1XqeAkXexh4qcofl_aR4kHJoSOWYGOhE7-ubX-DZCg", "access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gfnYrSypfdTtkNXIuNWx1KgowMDJmc2lnbmF0dXJlIOsh1XqeAkXexh4qcofl_aR4kHJoSOWYGOhE7-ubX-DZCg",
"home_server": "matrix.org", "home_server": "matrix.org",
"device_id": "GTVREDALBF", "device_id": "GTVREDALBF",
@ -128,6 +128,8 @@ We get the credentials (200)
} }
``` ```
It's worth noting that the response from the homeserver contains the userId of Alice.
### Login with Msisdn ### Login with Msisdn
Not supported yet in RiotX Not supported yet in RiotX

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.account
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.failure.isInvalidPassword
import im.vector.matrix.android.common.CommonTestHelper
import im.vector.matrix.android.common.SessionTestParams
import im.vector.matrix.android.common.TestConstants
import org.amshove.kluent.shouldBeTrue
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class ChangePasswordTest : InstrumentedTest {
private val commonTestHelper = CommonTestHelper(context())
companion object {
private const val NEW_PASSWORD = "this is a new password"
}
@Test
fun changePasswordTest() {
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false))
// Change password
commonTestHelper.doSync<Unit> {
session.changePassword(TestConstants.PASSWORD, NEW_PASSWORD, it)
}
// Try to login with the previous password, it will fail
val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD)
throwable.isInvalidPassword().shouldBeTrue()
// Try to login with the new password, should work
val session2 = commonTestHelper.logIntoAccount(session.myUserId, NEW_PASSWORD, SessionTestParams(withInitialSync = false))
commonTestHelper.signOutAndClose(session)
commonTestHelper.signOutAndClose(session2)
}
}

View file

@ -0,0 +1,88 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.account
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.auth.data.LoginFlowResult
import im.vector.matrix.android.api.auth.registration.RegistrationResult
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.common.CommonTestHelper
import im.vector.matrix.android.common.SessionTestParams
import im.vector.matrix.android.common.TestConstants
import im.vector.matrix.android.common.TestMatrixCallback
import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class DeactivateAccountTest : InstrumentedTest {
private val commonTestHelper = CommonTestHelper(context())
@Test
fun deactivateAccountTest() {
val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false))
// Deactivate the account
commonTestHelper.doSync<Unit> {
session.deactivateAccount(TestConstants.PASSWORD, false, it)
}
// Try to login on the previous account, it will fail (M_USER_DEACTIVATED)
val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD)
// Test the error
assertTrue(throwable is Failure.ServerError
&& throwable.error.code == MatrixError.M_USER_DEACTIVATED
&& throwable.error.message == "This account has been deactivated")
// Try to create an account with the deactivate account user id, it will fail (M_USER_IN_USE)
val hs = commonTestHelper.createHomeServerConfig()
commonTestHelper.doSync<LoginFlowResult> {
commonTestHelper.matrix.authenticationService.getLoginFlow(hs, it)
}
var accountCreationError: Throwable? = null
commonTestHelper.waitWithLatch {
commonTestHelper.matrix.authenticationService
.getRegistrationWizard()
.createAccount(session.myUserId.substringAfter("@").substringBefore(":"),
TestConstants.PASSWORD,
null,
object : TestMatrixCallback<RegistrationResult>(it, false) {
override fun onFailure(failure: Throwable) {
accountCreationError = failure
super.onFailure(failure)
}
})
}
// Test the error
accountCreationError.let {
assertTrue(it is Failure.ServerError
&& it.error.code == MatrixError.M_USER_IN_USE)
}
// No need to close the session, it has been deactivated
}
}

View file

@ -44,6 +44,7 @@ import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import timber.log.Timber
import java.util.ArrayList import java.util.ArrayList
import java.util.UUID import java.util.UUID
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
@ -58,6 +59,8 @@ class CommonTestHelper(context: Context) {
val matrix: Matrix val matrix: Matrix
init { init {
Timber.plant(Timber.DebugTree())
Matrix.initialize(context, MatrixConfiguration("TestFlavor")) Matrix.initialize(context, MatrixConfiguration("TestFlavor"))
matrix = Matrix.getInstance(context) matrix = Matrix.getInstance(context)
@ -183,9 +186,9 @@ class CommonTestHelper(context: Context) {
* @param testParams test params about the session * @param testParams test params about the session
* @return the session associated with the existing account * @return the session associated with the existing account
*/ */
private fun logIntoAccount(userId: String, fun logIntoAccount(userId: String,
password: String, password: String,
testParams: SessionTestParams): Session { testParams: SessionTestParams): Session {
val session = logAccountAndSync(userId, password, testParams) val session = logAccountAndSync(userId, password, testParams)
assertNotNull(session) assertNotNull(session)
return session return session
@ -260,14 +263,45 @@ class CommonTestHelper(context: Context) {
return session return session
} }
/**
* Log into the account and expect an error
*
* @param userName the account username
* @param password the password
*/
fun logAccountWithError(userName: String,
password: String): Throwable {
val hs = createHomeServerConfig()
doSync<LoginFlowResult> {
matrix.authenticationService
.getLoginFlow(hs, it)
}
var requestFailure: Throwable? = null
waitWithLatch { latch ->
matrix.authenticationService
.getLoginWizard()
.login(userName, password, "myDevice", object : TestMatrixCallback<Session>(latch, onlySuccessful = false) {
override fun onFailure(failure: Throwable) {
requestFailure = failure
super.onFailure(failure)
}
})
}
assertNotNull(requestFailure)
return requestFailure!!
}
/** /**
* Await for a latch and ensure the result is true * Await for a latch and ensure the result is true
* *
* @param latch * @param latch
* @throws InterruptedException * @throws InterruptedException
*/ */
fun await(latch: CountDownLatch, timout: Long? = TestConstants.timeOutMillis) { fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis) {
assertTrue(latch.await(timout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)) assertTrue(latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS))
} }
fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) { fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
@ -282,10 +316,10 @@ class CommonTestHelper(context: Context) {
} }
} }
fun waitWithLatch(timout: Long? = TestConstants.timeOutMillis, block: (CountDownLatch) -> Unit) { fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, block: (CountDownLatch) -> Unit) {
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
block(latch) block(latch)
await(latch, timout) await(latch, timeout)
} }
// Transform a method with a MatrixCallback to a synchronous method // Transform a method with a MatrixCallback to a synchronous method

View file

@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
@ -40,8 +41,6 @@ import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import java.util.HashMap
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
@ -140,64 +139,38 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
* @return Alice, Bob and Sam session * @return Alice, Bob and Sam session
*/ */
fun doE2ETestWithAliceAndBobAndSamInARoom(): CryptoTestData { fun doE2ETestWithAliceAndBobAndSamInARoom(): CryptoTestData {
val statuses = HashMap<String, String>()
val cryptoTestData = doE2ETestWithAliceAndBobInARoom() val cryptoTestData = doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId val aliceRoomId = cryptoTestData.roomId
val room = aliceSession.getRoom(aliceRoomId)!! val room = aliceSession.getRoom(aliceRoomId)!!
val samSession = mTestHelper.createAccount(TestConstants.USER_SAM, defaultSessionParams) val samSession = createSamAccountAndInviteToTheRoom(room)
val lock1 = CountDownLatch(2)
// val samEventListener = object : MXEventListener() {
// override fun onNewRoom(roomId: String) {
// if (TextUtils.equals(roomId, aliceRoomId)) {
// if (!statuses.containsKey("onNewRoom")) {
// statuses["onNewRoom"] = "onNewRoom"
// lock1.countDown()
// }
// }
// }
// }
//
// samSession.dataHandler.addListener(samEventListener)
room.invite(samSession.myUserId, null, object : TestMatrixCallback<Unit>(lock1) {
override fun onSuccess(data: Unit) {
statuses["invite"] = "invite"
super.onSuccess(data)
}
})
mTestHelper.await(lock1)
assertTrue(statuses.containsKey("invite") && statuses.containsKey("onNewRoom"))
// samSession.dataHandler.removeListener(samEventListener)
val lock2 = CountDownLatch(1)
samSession.joinRoom(aliceRoomId, null, object : TestMatrixCallback<Unit>(lock2) {
override fun onSuccess(data: Unit) {
statuses["joinRoom"] = "joinRoom"
super.onSuccess(data)
}
})
mTestHelper.await(lock2)
assertTrue(statuses.containsKey("joinRoom"))
// wait the initial sync // wait the initial sync
SystemClock.sleep(1000) SystemClock.sleep(1000)
// samSession.dataHandler.removeListener(samEventListener)
return CryptoTestData(aliceSession, aliceRoomId, cryptoTestData.secondSession, samSession) return CryptoTestData(aliceSession, aliceRoomId, cryptoTestData.secondSession, samSession)
} }
/**
* Create Sam account and invite him in the room. He will accept the invitation
* @Return Sam session
*/
fun createSamAccountAndInviteToTheRoom(room: Room): Session {
val samSession = mTestHelper.createAccount(TestConstants.USER_SAM, defaultSessionParams)
mTestHelper.doSync<Unit> {
room.invite(samSession.myUserId, null, it)
}
mTestHelper.doSync<Unit> {
samSession.joinRoom(room.roomId, null, it)
}
return samSession
}
/** /**
* @return Alice and Bob sessions * @return Alice and Bob sessions
*/ */

View file

@ -0,0 +1,247 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.crypto
import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.common.CommonTestHelper
import im.vector.matrix.android.common.CryptoTestHelper
import im.vector.matrix.android.common.TestConstants
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm
import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm
import org.amshove.kluent.shouldBe
import org.junit.Assert
import org.junit.Before
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.olm.OlmSession
import timber.log.Timber
import java.util.concurrent.CountDownLatch
/**
* Ref:
* - https://github.com/matrix-org/matrix-doc/pull/1719
* - https://matrix.org/docs/spec/client_server/latest#recovering-from-undecryptable-messages
* - https://github.com/matrix-org/matrix-js-sdk/pull/780
* - https://github.com/matrix-org/matrix-ios-sdk/pull/778
* - https://github.com/matrix-org/matrix-ios-sdk/pull/784
*/
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
class UnwedgingTest : InstrumentedTest {
private lateinit var messagesReceivedByBob: List<TimelineEvent>
private val mTestHelper = CommonTestHelper(context())
private val mCryptoTestHelper = CryptoTestHelper(mTestHelper)
@Before
fun init() {
messagesReceivedByBob = emptyList()
}
/**
* - Alice & Bob in a e2e room
* - Alice sends a 1st message with a 1st megolm session
* - Store the olm session between A&B devices
* - Alice sends a 2nd message with a 2nd megolm session
* - Simulate Alice using a backup of her OS and make her crypto state like after the first message
* - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session
*
* What Bob must see:
* -> No issue with the 2 first messages
* -> The third event must fail to decrypt at first because Bob the olm session is wedged
* -> This is automatically fixed after SDKs restarted the olm session
*/
@Test
fun testUnwedging() {
val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId
val bobSession = cryptoTestData.secondSession!!
val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
// bobSession.cryptoService().setWarnOnUnknownDevices(false)
// aliceSession.cryptoService().setWarnOnUnknownDevices(false)
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(20))
bobTimeline.start()
val bobFinalLatch = CountDownLatch(1)
val bobHasThreeDecryptedEventsListener = object : Timeline.Listener {
override fun onTimelineFailure(throwable: Throwable) {
// noop
}
override fun onNewTimelineEvents(eventIds: List<String>) {
// noop
}
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
val decryptedEventReceivedByBob = snapshot.filter { it.root.type == EventType.ENCRYPTED }
Timber.d("Bob can now decrypt ${decryptedEventReceivedByBob.size} messages")
if (decryptedEventReceivedByBob.size == 3) {
if (decryptedEventReceivedByBob[0].root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
bobFinalLatch.countDown()
}
}
}
}
bobTimeline.addListener(bobHasThreeDecryptedEventsListener)
var latch = CountDownLatch(1)
var bobEventsListener = createEventListener(latch, 1)
bobTimeline.addListener(bobEventsListener)
messagesReceivedByBob = emptyList()
// - Alice sends a 1st message with a 1st megolm session
roomFromAlicePOV.sendTextMessage("First message")
// Wait for the message to be received by Bob
mTestHelper.await(latch)
bobTimeline.removeListener(bobEventsListener)
messagesReceivedByBob.size shouldBe 1
val firstMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!!
// - Store the olm session between A&B devices
// Let us pickle our session with bob here so we can later unpickle it
// and wedge our session.
val sessionIdsForBob = aliceCryptoStore.getDeviceSessionIds(bobSession.cryptoService().getMyDevice().identityKey()!!)
sessionIdsForBob!!.size shouldBe 1
val olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), bobSession.cryptoService().getMyDevice().identityKey()!!)!!
val oldSession = serializeForRealm(olmSession.olmSession)
aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId)
Thread.sleep(6_000)
latch = CountDownLatch(1)
bobEventsListener = createEventListener(latch, 2)
bobTimeline.addListener(bobEventsListener)
messagesReceivedByBob = emptyList()
Timber.i("## CRYPTO | testUnwedging: Alice sends a 2nd message with a 2nd megolm session")
// - Alice sends a 2nd message with a 2nd megolm session
roomFromAlicePOV.sendTextMessage("Second message")
// Wait for the message to be received by Bob
mTestHelper.await(latch)
bobTimeline.removeListener(bobEventsListener)
messagesReceivedByBob.size shouldBe 2
// Session should have changed
val secondMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!!
Assert.assertNotEquals(firstMessageSession, secondMessageSession)
// Let us wedge the session now. Set crypto state like after the first message
Timber.i("## CRYPTO | testUnwedging: wedge the session now. Set crypto state like after the first message")
aliceCryptoStore.storeSession(OlmSessionWrapper(deserializeFromRealm<OlmSession>(oldSession)!!), bobSession.cryptoService().getMyDevice().identityKey()!!)
Thread.sleep(6_000)
// Force new session, and key share
aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId)
// Wait for the message to be received by Bob
mTestHelper.waitWithLatch {
bobEventsListener = createEventListener(it, 3)
bobTimeline.addListener(bobEventsListener)
messagesReceivedByBob = emptyList()
Timber.i("## CRYPTO | testUnwedging: Alice sends a 3rd message with a 3rd megolm session but a wedged olm session")
// - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session
roomFromAlicePOV.sendTextMessage("Third message")
// Bob should not be able to decrypt, because the session key could not be sent
}
bobTimeline.removeListener(bobEventsListener)
messagesReceivedByBob.size shouldBe 3
val thirdMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!!
Timber.i("## CRYPTO | testUnwedging: third message session ID $thirdMessageSession")
Assert.assertNotEquals(secondMessageSession, thirdMessageSession)
Assert.assertEquals(EventType.ENCRYPTED, messagesReceivedByBob[0].root.getClearType())
Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[1].root.getClearType())
Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[2].root.getClearType())
// Bob Should not be able to decrypt last message, because session could not be sent as the olm channel was wedged
mTestHelper.await(bobFinalLatch)
bobTimeline.removeListener(bobHasThreeDecryptedEventsListener)
// It's a trick to force key request on fail to decrypt
mTestHelper.doSync<Unit> {
bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD
), it)
}
// Wait until we received back the key
mTestHelper.waitWithLatch {
mTestHelper.retryPeriodicallyWithLatch(it) {
// we should get back the key and be able to decrypt
val result = tryThis {
bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
}
Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}")
result != null
}
}
bobTimeline.dispose()
cryptoTestData.cleanUp(mTestHelper)
}
private fun createEventListener(latch: CountDownLatch, expectedNumberOfMessages: Int): Timeline.Listener {
return object : Timeline.Listener {
override fun onTimelineFailure(throwable: Throwable) {
// noop
}
override fun onNewTimelineEvents(eventIds: List<String>) {
// noop
}
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
messagesReceivedByBob = snapshot.filter { it.root.type == EventType.ENCRYPTED }
if (messagesReceivedByBob.size == expectedNumberOfMessages) {
latch.countDown()
}
}
}
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.crypto.keysbackup
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.common.CommonTestHelper
import im.vector.matrix.android.common.CryptoTestData
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
/**
* Data class to store result of [KeysBackupTestHelper.createKeysBackupScenarioWithPassword]
*/
data class KeysBackupScenarioData(val cryptoTestData: CryptoTestData,
val aliceKeys: List<OlmInboundGroupSessionWrapper>,
val prepareKeysBackupDataResult: PrepareKeysBackupDataResult,
val aliceSession2: Session) {
fun cleanUp(testHelper: CommonTestHelper) {
cryptoTestData.cleanUp(testHelper)
testHelper.signOutAndClose(aliceSession2)
}
}

View file

@ -20,27 +20,19 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.listeners.StepProgressListener import im.vector.matrix.android.api.listeners.StepProgressListener
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
import im.vector.matrix.android.common.CommonTestHelper import im.vector.matrix.android.common.CommonTestHelper
import im.vector.matrix.android.common.CryptoTestData
import im.vector.matrix.android.common.CryptoTestHelper import im.vector.matrix.android.common.CryptoTestHelper
import im.vector.matrix.android.common.SessionTestParams
import im.vector.matrix.android.common.TestConstants import im.vector.matrix.android.common.TestConstants
import im.vector.matrix.android.common.TestMatrixCallback import im.vector.matrix.android.common.TestMatrixCallback
import im.vector.matrix.android.common.assertDictEquals
import im.vector.matrix.android.common.assertListEquals
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import im.vector.matrix.android.internal.crypto.MegolmSessionData
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
@ -61,9 +53,7 @@ class KeysBackupTest : InstrumentedTest {
private val mTestHelper = CommonTestHelper(context()) private val mTestHelper = CommonTestHelper(context())
private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) private val mCryptoTestHelper = CryptoTestHelper(mTestHelper)
private val mKeysBackupTestHelper = KeysBackupTestHelper(mTestHelper, mCryptoTestHelper)
private val defaultSessionParams = SessionTestParams(withInitialSync = false)
private val defaultSessionParamsWithInitialSync = SessionTestParams(withInitialSync = true)
/** /**
* - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys * - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys
@ -110,7 +100,7 @@ class KeysBackupTest : InstrumentedTest {
*/ */
@Test @Test
fun prepareKeysBackupVersionTest() { fun prepareKeysBackupVersionTest() {
val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams) val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
assertNotNull(bobSession.cryptoService().keysBackupService()) assertNotNull(bobSession.cryptoService().keysBackupService())
@ -139,7 +129,7 @@ class KeysBackupTest : InstrumentedTest {
*/ */
@Test @Test
fun createKeysBackupVersionTest() { fun createKeysBackupVersionTest() {
val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams) val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
val keysBackup = bobSession.cryptoService().keysBackupService() val keysBackup = bobSession.cryptoService().keysBackupService()
@ -182,7 +172,7 @@ class KeysBackupTest : InstrumentedTest {
val stateObserver = StateObserver(keysBackup, latch, 5) val stateObserver = StateObserver(keysBackup, latch, 5)
prepareAndCreateKeysBackupData(keysBackup) mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
mTestHelper.await(latch) mTestHelper.await(latch)
@ -216,7 +206,7 @@ class KeysBackupTest : InstrumentedTest {
val stateObserver = StateObserver(keysBackup) val stateObserver = StateObserver(keysBackup)
prepareAndCreateKeysBackupData(keysBackup) mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
// Check that backupAllGroupSessions returns valid data // Check that backupAllGroupSessions returns valid data
val nbOfKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false) val nbOfKeys = cryptoTestData.firstSession.cryptoService().inboundGroupSessionsCount(false)
@ -263,7 +253,7 @@ class KeysBackupTest : InstrumentedTest {
// - Pick a megolm key // - Pick a megolm key
val session = keysBackup.store.inboundGroupSessionsToBackup(1)[0] val session = keysBackup.store.inboundGroupSessionsToBackup(1)[0]
val keyBackupCreationInfo = prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo val keyBackupCreationInfo = mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo
// - Check encryptGroupSession() returns stg // - Check encryptGroupSession() returns stg
val keyBackupData = keysBackup.encryptGroupSession(session) val keyBackupData = keysBackup.encryptGroupSession(session)
@ -281,7 +271,7 @@ class KeysBackupTest : InstrumentedTest {
decryption!!) decryption!!)
assertNotNull(sessionData) assertNotNull(sessionData)
// - Compare the decrypted megolm key with the original one // - Compare the decrypted megolm key with the original one
assertKeysEquals(session.exportKeys(), sessionData) mKeysBackupTestHelper.assertKeysEquals(session.exportKeys(), sessionData)
stateObserver.stopAndCheckStates(null) stateObserver.stopAndCheckStates(null)
cryptoTestData.cleanUp(mTestHelper) cryptoTestData.cleanUp(mTestHelper)
@ -295,7 +285,7 @@ class KeysBackupTest : InstrumentedTest {
*/ */
@Test @Test
fun restoreKeysBackupTest() { fun restoreKeysBackupTest() {
val testData = createKeysBackupScenarioWithPassword(null) val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
// - Restore the e2e backup from the homeserver // - Restore the e2e backup from the homeserver
val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> { val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> {
@ -308,7 +298,7 @@ class KeysBackupTest : InstrumentedTest {
) )
} }
checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) mKeysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
testData.cleanUp(mTestHelper) testData.cleanUp(mTestHelper)
} }
@ -329,7 +319,7 @@ class KeysBackupTest : InstrumentedTest {
// fun restoreKeysBackupAndKeyShareRequestTest() { // fun restoreKeysBackupAndKeyShareRequestTest() {
// fail("Check with Valere for this test. I think we do not send key share request") // fail("Check with Valere for this test. I think we do not send key share request")
// //
// val testData = createKeysBackupScenarioWithPassword(null) // val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
// //
// // - Check the SDK sent key share requests // // - Check the SDK sent key share requests
// val cryptoStore2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store // val cryptoStore2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
@ -352,7 +342,7 @@ class KeysBackupTest : InstrumentedTest {
// ) // )
// } // }
// //
// checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) // mKeysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
// //
// // - There must be no more pending key share requests // // - There must be no more pending key share requests
// val unsentRequestAfterRestoration = cryptoStore2 // val unsentRequestAfterRestoration = cryptoStore2
@ -380,7 +370,7 @@ class KeysBackupTest : InstrumentedTest {
fun trustKeyBackupVersionTest() { fun trustKeyBackupVersionTest() {
// - Do an e2e backup to the homeserver with a recovery key // - Do an e2e backup to the homeserver with a recovery key
// - And log Alice on a new device // - And log Alice on a new device
val testData = createKeysBackupScenarioWithPassword(null) val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService())
@ -399,7 +389,7 @@ class KeysBackupTest : InstrumentedTest {
} }
// Wait for backup state to be ReadyToBackUp // Wait for backup state to be ReadyToBackUp
waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp) mKeysBackupTestHelper.waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp)
// - Backup must be enabled on the new device, on the same version // - Backup must be enabled on the new device, on the same version
assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version) assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version)
@ -439,7 +429,7 @@ class KeysBackupTest : InstrumentedTest {
fun trustKeyBackupVersionWithRecoveryKeyTest() { fun trustKeyBackupVersionWithRecoveryKeyTest() {
// - Do an e2e backup to the homeserver with a recovery key // - Do an e2e backup to the homeserver with a recovery key
// - And log Alice on a new device // - And log Alice on a new device
val testData = createKeysBackupScenarioWithPassword(null) val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService())
@ -458,7 +448,7 @@ class KeysBackupTest : InstrumentedTest {
} }
// Wait for backup state to be ReadyToBackUp // Wait for backup state to be ReadyToBackUp
waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp) mKeysBackupTestHelper.waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp)
// - Backup must be enabled on the new device, on the same version // - Backup must be enabled on the new device, on the same version
assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version) assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version)
@ -496,7 +486,7 @@ class KeysBackupTest : InstrumentedTest {
fun trustKeyBackupVersionWithWrongRecoveryKeyTest() { fun trustKeyBackupVersionWithWrongRecoveryKeyTest() {
// - Do an e2e backup to the homeserver with a recovery key // - Do an e2e backup to the homeserver with a recovery key
// - And log Alice on a new device // - And log Alice on a new device
val testData = createKeysBackupScenarioWithPassword(null) val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService())
@ -539,7 +529,7 @@ class KeysBackupTest : InstrumentedTest {
// - Do an e2e backup to the homeserver with a password // - Do an e2e backup to the homeserver with a password
// - And log Alice on a new device // - And log Alice on a new device
val testData = createKeysBackupScenarioWithPassword(password) val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService())
@ -558,7 +548,7 @@ class KeysBackupTest : InstrumentedTest {
} }
// Wait for backup state to be ReadyToBackUp // Wait for backup state to be ReadyToBackUp
waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp) mKeysBackupTestHelper.waitForKeysBackupToBeInState(testData.aliceSession2, KeysBackupState.ReadyToBackUp)
// - Backup must be enabled on the new device, on the same version // - Backup must be enabled on the new device, on the same version
assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version) assertEquals(testData.prepareKeysBackupDataResult.version, testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion?.version)
@ -599,7 +589,7 @@ class KeysBackupTest : InstrumentedTest {
// - Do an e2e backup to the homeserver with a password // - Do an e2e backup to the homeserver with a password
// - And log Alice on a new device // - And log Alice on a new device
val testData = createKeysBackupScenarioWithPassword(password) val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService()) val stateObserver = StateObserver(testData.aliceSession2.cryptoService().keysBackupService())
@ -634,7 +624,7 @@ class KeysBackupTest : InstrumentedTest {
*/ */
@Test @Test
fun restoreKeysBackupWithAWrongRecoveryKeyTest() { fun restoreKeysBackupWithAWrongRecoveryKeyTest() {
val testData = createKeysBackupScenarioWithPassword(null) val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
// - Try to restore the e2e backup with a wrong recovery key // - Try to restore the e2e backup with a wrong recovery key
val latch2 = CountDownLatch(1) val latch2 = CountDownLatch(1)
@ -669,7 +659,7 @@ class KeysBackupTest : InstrumentedTest {
fun testBackupWithPassword() { fun testBackupWithPassword() {
val password = "password" val password = "password"
val testData = createKeysBackupScenarioWithPassword(password) val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
// - Restore the e2e backup with the password // - Restore the e2e backup with the password
val steps = ArrayList<StepProgressListener.Step>() val steps = ArrayList<StepProgressListener.Step>()
@ -709,7 +699,7 @@ class KeysBackupTest : InstrumentedTest {
assertEquals(50, (steps[103] as StepProgressListener.Step.ImportingKey).progress) assertEquals(50, (steps[103] as StepProgressListener.Step.ImportingKey).progress)
assertEquals(100, (steps[104] as StepProgressListener.Step.ImportingKey).progress) assertEquals(100, (steps[104] as StepProgressListener.Step.ImportingKey).progress)
checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) mKeysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
testData.cleanUp(mTestHelper) testData.cleanUp(mTestHelper)
} }
@ -725,7 +715,7 @@ class KeysBackupTest : InstrumentedTest {
val password = "password" val password = "password"
val wrongPassword = "passw0rd" val wrongPassword = "passw0rd"
val testData = createKeysBackupScenarioWithPassword(password) val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
// - Try to restore the e2e backup with a wrong password // - Try to restore the e2e backup with a wrong password
val latch2 = CountDownLatch(1) val latch2 = CountDownLatch(1)
@ -760,7 +750,7 @@ class KeysBackupTest : InstrumentedTest {
fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() { fun testUseRecoveryKeyToRestoreAPasswordBasedKeysBackup() {
val password = "password" val password = "password"
val testData = createKeysBackupScenarioWithPassword(password) val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
// - Restore the e2e backup with the recovery key. // - Restore the e2e backup with the recovery key.
val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> { val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> {
@ -773,7 +763,7 @@ class KeysBackupTest : InstrumentedTest {
) )
} }
checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys) mKeysBackupTestHelper.checkRestoreSuccess(testData, importRoomKeysResult.totalNumberOfKeys, importRoomKeysResult.successfullyNumberOfImportedKeys)
testData.cleanUp(mTestHelper) testData.cleanUp(mTestHelper)
} }
@ -786,7 +776,7 @@ class KeysBackupTest : InstrumentedTest {
*/ */
@Test @Test
fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() { fun testUsePasswordToRestoreARecoveryKeyBasedKeysBackup() {
val testData = createKeysBackupScenarioWithPassword(null) val testData = mKeysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
// - Try to restore the e2e backup with a password // - Try to restore the e2e backup with a password
val latch2 = CountDownLatch(1) val latch2 = CountDownLatch(1)
@ -825,7 +815,7 @@ class KeysBackupTest : InstrumentedTest {
val stateObserver = StateObserver(keysBackup) val stateObserver = StateObserver(keysBackup)
// - Do an e2e backup to the homeserver // - Do an e2e backup to the homeserver
prepareAndCreateKeysBackupData(keysBackup) mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
// Get key backup version from the home server // Get key backup version from the home server
val keysVersionResult = mTestHelper.doSync<KeysVersionResult?> { val keysVersionResult = mTestHelper.doSync<KeysVersionResult?> {
@ -870,13 +860,13 @@ class KeysBackupTest : InstrumentedTest {
assertFalse(keysBackup.isEnabled) assertFalse(keysBackup.isEnabled)
val keyBackupCreationInfo = prepareAndCreateKeysBackupData(keysBackup) val keyBackupCreationInfo = mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
assertTrue(keysBackup.isEnabled) assertTrue(keysBackup.isEnabled)
// - Restart alice session // - Restart alice session
// - Log Alice on a new device // - Log Alice on a new device
val aliceSession2 = mTestHelper.logIntoAccount(cryptoTestData.firstSession.myUserId, defaultSessionParamsWithInitialSync) val aliceSession2 = mTestHelper.logIntoAccount(cryptoTestData.firstSession.myUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync)
cryptoTestData.cleanUp(mTestHelper) cryptoTestData.cleanUp(mTestHelper)
@ -950,7 +940,7 @@ class KeysBackupTest : InstrumentedTest {
}) })
// - Make alice back up her keys to her homeserver // - Make alice back up her keys to her homeserver
prepareAndCreateKeysBackupData(keysBackup) mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
assertTrue(keysBackup.isEnabled) assertTrue(keysBackup.isEnabled)
@ -1000,7 +990,7 @@ class KeysBackupTest : InstrumentedTest {
val stateObserver = StateObserver(keysBackup) val stateObserver = StateObserver(keysBackup)
// - Make alice back up her keys to her homeserver // - Make alice back up her keys to her homeserver
prepareAndCreateKeysBackupData(keysBackup) mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
// Wait for keys backup to finish by asking again to backup keys. // Wait for keys backup to finish by asking again to backup keys.
mTestHelper.doSync<Unit> { mTestHelper.doSync<Unit> {
@ -1012,7 +1002,7 @@ class KeysBackupTest : InstrumentedTest {
val aliceUserId = cryptoTestData.firstSession.myUserId val aliceUserId = cryptoTestData.firstSession.myUserId
// - Log Alice on a new device // - Log Alice on a new device
val aliceSession2 = mTestHelper.logIntoAccount(aliceUserId, defaultSessionParamsWithInitialSync) val aliceSession2 = mTestHelper.logIntoAccount(aliceUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync)
// - Post a message to have a new megolm session // - Post a message to have a new megolm session
aliceSession2.cryptoService().setWarnOnUnknownDevices(false) aliceSession2.cryptoService().setWarnOnUnknownDevices(false)
@ -1093,7 +1083,7 @@ class KeysBackupTest : InstrumentedTest {
assertFalse(keysBackup.isEnabled) assertFalse(keysBackup.isEnabled)
val keyBackupCreationInfo = prepareAndCreateKeysBackupData(keysBackup) val keyBackupCreationInfo = mKeysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
assertTrue(keysBackup.isEnabled) assertTrue(keysBackup.isEnabled)
@ -1106,169 +1096,4 @@ class KeysBackupTest : InstrumentedTest {
stateObserver.stopAndCheckStates(null) stateObserver.stopAndCheckStates(null)
cryptoTestData.cleanUp(mTestHelper) cryptoTestData.cleanUp(mTestHelper)
} }
/* ==========================================================================================
* Private
* ========================================================================================== */
/**
* As KeysBackup is doing asynchronous call to update its internal state, this method help to wait for the
* KeysBackup object to be in the specified state
*/
private fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) {
// If already in the wanted state, return
if (session.cryptoService().keysBackupService().state == state) {
return
}
// Else observe state changes
val latch = CountDownLatch(1)
session.cryptoService().keysBackupService().addListener(object : KeysBackupStateListener {
override fun onStateChange(newState: KeysBackupState) {
if (newState == state) {
session.cryptoService().keysBackupService().removeListener(this)
latch.countDown()
}
}
})
mTestHelper.await(latch)
}
private data class PrepareKeysBackupDataResult(val megolmBackupCreationInfo: MegolmBackupCreationInfo,
val version: String)
private fun prepareAndCreateKeysBackupData(keysBackup: KeysBackupService,
password: String? = null): PrepareKeysBackupDataResult {
val stateObserver = StateObserver(keysBackup)
val megolmBackupCreationInfo = mTestHelper.doSync<MegolmBackupCreationInfo> {
keysBackup.prepareKeysBackupVersion(password, null, it)
}
assertNotNull(megolmBackupCreationInfo)
assertFalse(keysBackup.isEnabled)
// Create the version
val keysVersion = mTestHelper.doSync<KeysVersion> {
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
}
assertNotNull(keysVersion.version)
// Backup must be enable now
assertTrue(keysBackup.isEnabled)
stateObserver.stopAndCheckStates(null)
return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version!!)
}
private fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) {
assertNotNull(keys1)
assertNotNull(keys2)
assertEquals(keys1?.algorithm, keys2?.algorithm)
assertEquals(keys1?.roomId, keys2?.roomId)
// No need to compare the shortcut
// assertEquals(keys1?.sender_claimed_ed25519_key, keys2?.sender_claimed_ed25519_key)
assertEquals(keys1?.senderKey, keys2?.senderKey)
assertEquals(keys1?.sessionId, keys2?.sessionId)
assertEquals(keys1?.sessionKey, keys2?.sessionKey)
assertListEquals(keys1?.forwardingCurve25519KeyChain, keys2?.forwardingCurve25519KeyChain)
assertDictEquals(keys1?.senderClaimedKeys, keys2?.senderClaimedKeys)
}
/**
* Data class to store result of [createKeysBackupScenarioWithPassword]
*/
private data class KeysBackupScenarioData(val cryptoTestData: CryptoTestData,
val aliceKeys: List<OlmInboundGroupSessionWrapper>,
val prepareKeysBackupDataResult: PrepareKeysBackupDataResult,
val aliceSession2: Session) {
fun cleanUp(testHelper: CommonTestHelper) {
cryptoTestData.cleanUp(testHelper)
testHelper.signOutAndClose(aliceSession2)
}
}
/**
* Common initial condition
* - Do an e2e backup to the homeserver
* - Log Alice on a new device, and wait for its keysBackup object to be ready (in state NotTrusted)
*
* @param password optional password
*/
private fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData {
val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService()
val stateObserver = StateObserver(keysBackup)
val aliceKeys = cryptoStore.inboundGroupSessionsToBackup(100)
// - Do an e2e backup to the homeserver
val prepareKeysBackupDataResult = prepareAndCreateKeysBackupData(keysBackup, password)
var lastProgress = 0
var lastTotal = 0
mTestHelper.doSync<Unit> {
keysBackup.backupAllGroupSessions(object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
lastProgress = progress
lastTotal = total
}
}, it)
}
assertEquals(2, lastProgress)
assertEquals(2, lastTotal)
val aliceUserId = cryptoTestData.firstSession.myUserId
// - Log Alice on a new device
val aliceSession2 = mTestHelper.logIntoAccount(aliceUserId, defaultSessionParamsWithInitialSync)
// Test check: aliceSession2 has no keys at login
assertEquals(0, aliceSession2.cryptoService().inboundGroupSessionsCount(false))
// Wait for backup state to be NotTrusted
waitForKeysBackupToBeInState(aliceSession2, KeysBackupState.NotTrusted)
stateObserver.stopAndCheckStates(null)
return KeysBackupScenarioData(cryptoTestData,
aliceKeys,
prepareKeysBackupDataResult,
aliceSession2)
}
/**
* Common restore success check after [createKeysBackupScenarioWithPassword]:
* - Imported keys number must be correct
* - The new device must have the same count of megolm keys
* - Alice must have the same keys on both devices
*/
private fun checkRestoreSuccess(testData: KeysBackupScenarioData,
total: Int,
imported: Int) {
// - Imported keys number must be correct
assertEquals(testData.aliceKeys.size, total)
assertEquals(total, imported)
// - The new device must have the same count of megolm keys
assertEquals(testData.aliceKeys.size, testData.aliceSession2.cryptoService().inboundGroupSessionsCount(false))
// - Alice must have the same keys on both devices
for (aliceKey1 in testData.aliceKeys) {
val aliceKey2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
.getInboundGroupSession(aliceKey1.olmInboundGroupSession!!.sessionIdentifier(), aliceKey1.senderKey!!)
assertNotNull(aliceKey2)
assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys())
}
}
} }

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.crypto.keysbackup
import im.vector.matrix.android.common.SessionTestParams
object KeysBackupTestConstants {
val defaultSessionParams = SessionTestParams(withInitialSync = false)
val defaultSessionParamsWithInitialSync = SessionTestParams(withInitialSync = true)
}

View file

@ -0,0 +1,182 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.crypto.keysbackup
import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
import im.vector.matrix.android.common.CommonTestHelper
import im.vector.matrix.android.common.CryptoTestHelper
import im.vector.matrix.android.common.assertDictEquals
import im.vector.matrix.android.common.assertListEquals
import im.vector.matrix.android.internal.crypto.MegolmSessionData
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
import org.junit.Assert
import java.util.concurrent.CountDownLatch
class KeysBackupTestHelper(
private val mTestHelper: CommonTestHelper,
private val mCryptoTestHelper: CryptoTestHelper) {
/**
* Common initial condition
* - Do an e2e backup to the homeserver
* - Log Alice on a new device, and wait for its keysBackup object to be ready (in state NotTrusted)
*
* @param password optional password
*/
fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData {
val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
val cryptoStore = (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store
val keysBackup = cryptoTestData.firstSession.cryptoService().keysBackupService()
val stateObserver = StateObserver(keysBackup)
val aliceKeys = cryptoStore.inboundGroupSessionsToBackup(100)
// - Do an e2e backup to the homeserver
val prepareKeysBackupDataResult = prepareAndCreateKeysBackupData(keysBackup, password)
var lastProgress = 0
var lastTotal = 0
mTestHelper.doSync<Unit> {
keysBackup.backupAllGroupSessions(object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
lastProgress = progress
lastTotal = total
}
}, it)
}
Assert.assertEquals(2, lastProgress)
Assert.assertEquals(2, lastTotal)
val aliceUserId = cryptoTestData.firstSession.myUserId
// - Log Alice on a new device
val aliceSession2 = mTestHelper.logIntoAccount(aliceUserId, KeysBackupTestConstants.defaultSessionParamsWithInitialSync)
// Test check: aliceSession2 has no keys at login
Assert.assertEquals(0, aliceSession2.cryptoService().inboundGroupSessionsCount(false))
// Wait for backup state to be NotTrusted
waitForKeysBackupToBeInState(aliceSession2, KeysBackupState.NotTrusted)
stateObserver.stopAndCheckStates(null)
return KeysBackupScenarioData(cryptoTestData,
aliceKeys,
prepareKeysBackupDataResult,
aliceSession2)
}
fun prepareAndCreateKeysBackupData(keysBackup: KeysBackupService,
password: String? = null): PrepareKeysBackupDataResult {
val stateObserver = StateObserver(keysBackup)
val megolmBackupCreationInfo = mTestHelper.doSync<MegolmBackupCreationInfo> {
keysBackup.prepareKeysBackupVersion(password, null, it)
}
Assert.assertNotNull(megolmBackupCreationInfo)
Assert.assertFalse(keysBackup.isEnabled)
// Create the version
val keysVersion = mTestHelper.doSync<KeysVersion> {
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
}
Assert.assertNotNull(keysVersion.version)
// Backup must be enable now
Assert.assertTrue(keysBackup.isEnabled)
stateObserver.stopAndCheckStates(null)
return PrepareKeysBackupDataResult(megolmBackupCreationInfo, keysVersion.version!!)
}
/**
* As KeysBackup is doing asynchronous call to update its internal state, this method help to wait for the
* KeysBackup object to be in the specified state
*/
fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) {
// If already in the wanted state, return
if (session.cryptoService().keysBackupService().state == state) {
return
}
// Else observe state changes
val latch = CountDownLatch(1)
session.cryptoService().keysBackupService().addListener(object : KeysBackupStateListener {
override fun onStateChange(newState: KeysBackupState) {
if (newState == state) {
session.cryptoService().keysBackupService().removeListener(this)
latch.countDown()
}
}
})
mTestHelper.await(latch)
}
fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) {
Assert.assertNotNull(keys1)
Assert.assertNotNull(keys2)
Assert.assertEquals(keys1?.algorithm, keys2?.algorithm)
Assert.assertEquals(keys1?.roomId, keys2?.roomId)
// No need to compare the shortcut
// assertEquals(keys1?.sender_claimed_ed25519_key, keys2?.sender_claimed_ed25519_key)
Assert.assertEquals(keys1?.senderKey, keys2?.senderKey)
Assert.assertEquals(keys1?.sessionId, keys2?.sessionId)
Assert.assertEquals(keys1?.sessionKey, keys2?.sessionKey)
assertListEquals(keys1?.forwardingCurve25519KeyChain, keys2?.forwardingCurve25519KeyChain)
assertDictEquals(keys1?.senderClaimedKeys, keys2?.senderClaimedKeys)
}
/**
* Common restore success check after [KeysBackupTestHelper.createKeysBackupScenarioWithPassword]:
* - Imported keys number must be correct
* - The new device must have the same count of megolm keys
* - Alice must have the same keys on both devices
*/
fun checkRestoreSuccess(testData: KeysBackupScenarioData,
total: Int,
imported: Int) {
// - Imported keys number must be correct
Assert.assertEquals(testData.aliceKeys.size, total)
Assert.assertEquals(total, imported)
// - The new device must have the same count of megolm keys
Assert.assertEquals(testData.aliceKeys.size, testData.aliceSession2.cryptoService().inboundGroupSessionsCount(false))
// - Alice must have the same keys on both devices
for (aliceKey1 in testData.aliceKeys) {
val aliceKey2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
.getInboundGroupSession(aliceKey1.olmInboundGroupSession!!.sessionIdentifier(), aliceKey1.senderKey!!)
Assert.assertNotNull(aliceKey2)
assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys())
}
}
}

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.crypto.keysbackup
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
data class PrepareKeysBackupDataResult(val megolmBackupCreationInfo: MegolmBackupCreationInfo,
val version: String)

View file

@ -23,11 +23,28 @@ import im.vector.matrix.android.api.util.Cancelable
* This interface defines methods to manage the account. It's implemented at the session level. * This interface defines methods to manage the account. It's implemented at the session level.
*/ */
interface AccountService { interface AccountService {
/** /**
* Ask the homeserver to change the password. * Ask the homeserver to change the password.
* @param password Current password. * @param password Current password.
* @param newPassword New password * @param newPassword New password
*/ */
fun changePassword(password: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable fun changePassword(password: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable
/**
* Deactivate the account.
*
* This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register
* the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account
* details from your identity server. <b>This action is irreversible</b>.\n\nDeactivating your account <b>does not by default
* cause us to forget messages you have sent</b>. If you would like us to forget your messages, please tick the box below.
*
* Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not
* be shared with any new or unregistered users, but registered users who already have access to these messages will still
* have access to their copy.
*
* @param password the account password
* @param eraseAllData set to true to forget all messages that have been sent. Warning: this will cause future users to see
* an incomplete view of conversations
*/
fun deactivateAccount(password: String, eraseAllData: Boolean, callback: MatrixCallback<Unit>): Cancelable
} }

View file

@ -111,6 +111,8 @@ interface CryptoService {
roomId: String, roomId: String,
callback: MatrixCallback<MXEncryptEventContentResult>) callback: MatrixCallback<MXEncryptEventContentResult>)
fun discardOutboundSession(roomId: String)
@Throws(MXCryptoError::class) @Throws(MXCryptoError::class)
fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult

View file

@ -81,6 +81,9 @@ object EventType {
// Relation Events // Relation Events
const val REACTION = "m.reaction" const val REACTION = "m.reaction"
// Unwedging
internal const val DUMMY = "m.dummy"
private val STATE_EVENTS = listOf( private val STATE_EVENTS = listOf(
STATE_ROOM_NAME, STATE_ROOM_NAME,
STATE_ROOM_TOPIC, STATE_ROOM_TOPIC,

View file

@ -104,6 +104,7 @@ interface Timeline {
interface Listener { interface Listener {
/** /**
* Call when the timeline has been updated through pagination or sync. * Call when the timeline has been updated through pagination or sync.
* The latest event is the first in the list
* @param snapshot the most up to date snapshot * @param snapshot the most up to date snapshot
*/ */
fun onTimelineUpdated(snapshot: List<TimelineEvent>) fun onTimelineUpdated(snapshot: List<TimelineEvent>)

View file

@ -21,13 +21,13 @@ package im.vector.matrix.android.internal.crypto
import android.content.Context import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.squareup.moshi.Types import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import dagger.Lazy import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.NoOpMatrixCallback import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.crypto.MXCryptoConfig import im.vector.matrix.android.api.crypto.MXCryptoConfig
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.api.listeners.ProgressListener
@ -45,7 +45,9 @@ import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import im.vector.matrix.android.internal.crypto.actions.MegolmSessionDataImporter import im.vector.matrix.android.internal.crypto.actions.MegolmSessionDataImporter
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
@ -59,6 +61,7 @@ import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.crypto.model.event.OlmEventContent
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
@ -72,13 +75,16 @@ import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceTask
import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask
import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.whereType import im.vector.matrix.android.internal.database.query.whereType
import im.vector.matrix.android.internal.di.DeviceId
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
@ -116,12 +122,15 @@ import kotlin.math.max
internal class DefaultCryptoService @Inject constructor( internal class DefaultCryptoService @Inject constructor(
// Olm Manager // Olm Manager
private val olmManager: OlmManager, private val olmManager: OlmManager,
// The credentials, @UserId
private val credentials: Credentials, private val userId: String,
@DeviceId
private val deviceId: String?,
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>, private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
// the crypto store // the crypto store
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
// Room encryptors store
private val roomEncryptorsStore: RoomEncryptorsStore,
// Olm device // Olm device
private val olmDevice: MXOlmDevice, private val olmDevice: MXOlmDevice,
// Set of parameters used to configure/customize the end-to-end crypto. // Set of parameters used to configure/customize the end-to-end crypto.
@ -162,7 +171,10 @@ internal class DefaultCryptoService @Inject constructor(
private val monarchy: Monarchy, private val monarchy: Monarchy,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val cryptoCoroutineScope: CoroutineScope private val cryptoCoroutineScope: CoroutineScope,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val sendToDeviceTask: SendToDeviceTask,
private val messageEncrypter: MessageEncrypter
) : CryptoService { ) : CryptoService {
init { init {
@ -171,11 +183,13 @@ internal class DefaultCryptoService @Inject constructor(
private val uiHandler = Handler(Looper.getMainLooper()) private val uiHandler = Handler(Looper.getMainLooper())
// MXEncrypting instance for each room.
private val roomEncryptors: MutableMap<String, IMXEncrypting> = HashMap()
private val isStarting = AtomicBoolean(false) private val isStarting = AtomicBoolean(false)
private val isStarted = AtomicBoolean(false) private val isStarted = AtomicBoolean(false)
// The date of the last time we forced establishment
// of a new session for each user:device.
private val lastNewSessionForcedDates = MXUsersDevicesMap<Long>()
fun onStateEvent(roomId: String, event: Event) { fun onStateEvent(roomId: String, event: Event) {
when { when {
event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
@ -199,7 +213,7 @@ internal class DefaultCryptoService @Inject constructor(
this.callback = object : MatrixCallback<Unit> { this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
// bg refresh of crypto device // bg refresh of crypto device
downloadKeys(listOf(credentials.userId), true, NoOpMatrixCallback()) downloadKeys(listOf(userId), true, NoOpMatrixCallback())
callback.onSuccess(data) callback.onSuccess(data)
} }
@ -398,7 +412,7 @@ internal class DefaultCryptoService @Inject constructor(
} }
/** /**
* Provides the device information for a device id and a user Id * Provides the device information for a user id and a device Id
* *
* @param userId the user id * @param userId the user id
* @param deviceId the device id * @param deviceId the device id
@ -493,14 +507,14 @@ internal class DefaultCryptoService @Inject constructor(
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId) val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) { if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) {
Timber.e("## setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId") Timber.e("## CRYPTO | setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId")
return false return false
} }
val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm) val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm)
if (!encryptingClass) { if (!encryptingClass) {
Timber.e("## setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm") Timber.e("## CRYPTO | setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm")
return false return false
} }
@ -511,9 +525,7 @@ internal class DefaultCryptoService @Inject constructor(
else -> olmEncryptionFactory.create(roomId) else -> olmEncryptionFactory.create(roomId)
} }
synchronized(roomEncryptors) { roomEncryptorsStore.put(roomId, alg)
roomEncryptors.put(roomId, alg)
}
// if encryption was not previously enabled in this room, we will have been // if encryption was not previously enabled in this room, we will have been
// ignoring new device events for these users so far. We may well have // ignoring new device events for these users so far. We may well have
@ -591,42 +603,44 @@ internal class DefaultCryptoService @Inject constructor(
callback: MatrixCallback<MXEncryptEventContentResult>) { callback: MatrixCallback<MXEncryptEventContentResult>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
if (!isStarted()) { if (!isStarted()) {
Timber.v("## encryptEventContent() : wait after e2e init") Timber.v("## CRYPTO | encryptEventContent() : wait after e2e init")
internalStart(false) internalStart(false)
} }
val userIds = getRoomUserIds(roomId) val userIds = getRoomUserIds(roomId)
var alg = synchronized(roomEncryptors) { var alg = roomEncryptorsStore.get(roomId)
roomEncryptors[roomId]
}
if (alg == null) { if (alg == null) {
val algorithm = getEncryptionAlgorithm(roomId) val algorithm = getEncryptionAlgorithm(roomId)
if (algorithm != null) { if (algorithm != null) {
if (setEncryptionInRoom(roomId, algorithm, false, userIds)) { if (setEncryptionInRoom(roomId, algorithm, false, userIds)) {
synchronized(roomEncryptors) { alg = roomEncryptorsStore.get(roomId)
alg = roomEncryptors[roomId]
}
} }
} }
} }
val safeAlgorithm = alg val safeAlgorithm = alg
if (safeAlgorithm != null) { if (safeAlgorithm != null) {
val t0 = System.currentTimeMillis() val t0 = System.currentTimeMillis()
Timber.v("## encryptEventContent() starts") Timber.v("## CRYPTO | encryptEventContent() starts")
runCatching { runCatching {
val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds) val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
Timber.v("## encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") Timber.v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
MXEncryptEventContentResult(content, EventType.ENCRYPTED) MXEncryptEventContentResult(content, EventType.ENCRYPTED)
}.foldToCallback(callback) }.foldToCallback(callback)
} else { } else {
val algorithm = getEncryptionAlgorithm(roomId) val algorithm = getEncryptionAlgorithm(roomId)
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON,
algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON) algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON)
Timber.e("## encryptEventContent() : $reason") Timber.e("## CRYPTO | encryptEventContent() : $reason")
callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason))) callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason)))
} }
} }
} }
override fun discardOutboundSession(roomId: String) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
roomEncryptorsStore.get(roomId)?.discardSessionKey()
}
}
/** /**
* Decrypt an event * Decrypt an event
* *
@ -664,20 +678,42 @@ internal class DefaultCryptoService @Inject constructor(
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the MXEventDecryptionResult data, or null in case of error * @return the MXEventDecryptionResult data, or null in case of error
*/ */
@Throws(MXCryptoError::class)
private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
val eventContent = event.content val eventContent = event.content
if (eventContent == null) { if (eventContent == null) {
Timber.e("## decryptEvent : empty event content") Timber.e("## CRYPTO | decryptEvent : empty event content")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
} else { } else {
val algorithm = eventContent["algorithm"]?.toString() val algorithm = eventContent["algorithm"]?.toString()
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm) val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
if (alg == null) { if (alg == null) {
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm) val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
Timber.e("## decryptEvent() : $reason") Timber.e("## CRYPTO | decryptEvent() : $reason")
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
} else { } else {
return alg.decryptEvent(event, timeline) try {
return alg.decryptEvent(event, timeline)
} catch (mxCryptoError: MXCryptoError) {
Timber.d("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
if (mxCryptoError is MXCryptoError.Base
&& mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
// need to find sending device
val olmContent = event.content.toModel<OlmEventContent>()
cryptoStore.getUserDevices(event.senderId ?: "")
?.values
?.firstOrNull { it.identityKey() == olmContent?.senderKey }
?.let {
markOlmSessionForUnwedging(event.senderId ?: "", it)
}
?: run {
Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device")
}
}
}
throw mxCryptoError
}
} }
} }
} }
@ -730,30 +766,30 @@ internal class DefaultCryptoService @Inject constructor(
*/ */
private fun onRoomKeyEvent(event: Event) { private fun onRoomKeyEvent(event: Event) {
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
Timber.v("## GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>") Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>")
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
Timber.e("## GOSSIP onRoomKeyEvent() : missing fields") Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : missing fields")
return return
} }
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm) val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm)
if (alg == null) { if (alg == null) {
Timber.e("## GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
return return
} }
alg.onRoomKeyEvent(event, keysBackupService) alg.onRoomKeyEvent(event, keysBackupService)
} }
private fun onSecretSendReceived(event: Event) { private fun onSecretSendReceived(event: Event) {
Timber.i("## GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}") Timber.i("## CRYPTO | GOSSIP onSecretSend() : onSecretSendReceived ${event.content?.get("sender_key")}")
if (!event.isEncrypted()) { if (!event.isEncrypted()) {
// secret send messages must be encrypted // secret send messages must be encrypted
Timber.e("## GOSSIP onSecretSend() :Received unencrypted secret send event") Timber.e("## CRYPTO | GOSSIP onSecretSend() :Received unencrypted secret send event")
return return
} }
// Was that sent by us? // Was that sent by us?
if (event.senderId != credentials.userId) { if (event.senderId != userId) {
Timber.e("## GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}") Timber.e("## CRYPTO | GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}")
return return
} }
@ -763,13 +799,13 @@ internal class DefaultCryptoService @Inject constructor(
.getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId } .getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId }
if (existingRequest == null) { if (existingRequest == null) {
Timber.i("## GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") Timber.i("## CRYPTO | GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
return return
} }
if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) { if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) {
// TODO Ask to application layer? // TODO Ask to application layer?
Timber.v("## onSecretSend() : secret not handled by SDK") Timber.v("## CRYPTO | onSecretSend() : secret not handled by SDK")
} }
} }
@ -805,7 +841,7 @@ internal class DefaultCryptoService @Inject constructor(
try { try {
loadRoomMembersTask.execute(params) loadRoomMembersTask.execute(params)
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
Timber.e(throwable, "## onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ") Timber.e(throwable, "## CRYPTO | onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ")
} finally { } finally {
val userIds = getRoomUserIds(roomId) val userIds = getRoomUserIds(roomId)
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds) setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
@ -835,16 +871,8 @@ internal class DefaultCryptoService @Inject constructor(
* @param event the membership event causing the change * @param event the membership event causing the change
*/ */
private fun onRoomMembershipEvent(roomId: String, event: Event) { private fun onRoomMembershipEvent(roomId: String, event: Event) {
val alg: IMXEncrypting? roomEncryptorsStore.get(roomId) ?: /* No encrypting in this room */ return
synchronized(roomEncryptors) {
alg = roomEncryptors[roomId]
}
if (null == alg) {
// No encrypting in this room
return
}
event.stateKey?.let { userId -> event.stateKey?.let { userId ->
val roomMember: RoomMemberSummary? = event.content.toModel() val roomMember: RoomMemberSummary? = event.content.toModel()
val membership = roomMember?.membership val membership = roomMember?.membership
@ -938,13 +966,13 @@ internal class DefaultCryptoService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.main) { cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching { runCatching {
withContext(coroutineDispatchers.crypto) { withContext(coroutineDispatchers.crypto) {
Timber.v("## importRoomKeys starts") Timber.v("## CRYPTO | importRoomKeys starts")
val t0 = System.currentTimeMillis() val t0 = System.currentTimeMillis()
val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password) val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password)
val t1 = System.currentTimeMillis() val t1 = System.currentTimeMillis()
Timber.v("## importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms") Timber.v("## CRYPTO | importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms")
val importedSessions = MoshiProvider.providesMoshi() val importedSessions = MoshiProvider.providesMoshi()
.adapter<List<MegolmSessionData>>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java)) .adapter<List<MegolmSessionData>>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java))
@ -952,7 +980,7 @@ internal class DefaultCryptoService @Inject constructor(
val t2 = System.currentTimeMillis() val t2 = System.currentTimeMillis()
Timber.v("## importRoomKeys : JSON parsing ${t2 - t1} ms") Timber.v("## CRYPTO | importRoomKeys : JSON parsing ${t2 - t1} ms")
if (importedSessions == null) { if (importedSessions == null) {
throw Exception("Error") throw Exception("Error")
@ -1087,7 +1115,7 @@ internal class DefaultCryptoService @Inject constructor(
*/ */
override fun reRequestRoomKeyForEvent(event: Event) { override fun reRequestRoomKeyForEvent(event: Event) {
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also { val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
Timber.e("## reRequestRoomKeyForEvent Failed to re-request key, null content") Timber.e("## CRYPTO | reRequestRoomKeyForEvent Failed to re-request key, null content")
} }
val requestBody = RoomKeyRequestBody( val requestBody = RoomKeyRequestBody(
@ -1102,18 +1130,18 @@ internal class DefaultCryptoService @Inject constructor(
override fun requestRoomKeyForEvent(event: Event) { override fun requestRoomKeyForEvent(event: Event) {
val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also { val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
Timber.e("## requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}") Timber.e("## CRYPTO | requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}")
} }
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
if (!isStarted()) { if (!isStarted()) {
Timber.v("## requestRoomKeyForEvent() : wait after e2e init") Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init")
internalStart(false) internalStart(false)
} }
roomDecryptorProvider roomDecryptorProvider
.getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm) .getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
?.requestKeysForEvent(event) ?: run { ?.requestKeysForEvent(event) ?: run {
Timber.v("## requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}") Timber.v("## CRYPTO | requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
} }
} }
} }
@ -1136,6 +1164,39 @@ internal class DefaultCryptoService @Inject constructor(
incomingGossipingRequestManager.removeRoomKeysRequestListener(listener) incomingGossipingRequestManager.removeRoomKeysRequestListener(listener)
} }
private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
val deviceKey = deviceInfo.identityKey()
val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
val now = System.currentTimeMillis()
if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
return
}
Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
// Now send a blank message on that session so the other side knows about it.
// (The keyshare request is sent in the clear so that won't do)
// We send this first such that, as long as the toDevice messages arrive in the
// same order we sent them, the other end will get this first, set up the new session,
// then get the keyshare request and send the key over this new session (because it
// is the session it has most recently received a message on).
val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY)
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
sendToDeviceTask.execute(sendToDeviceParams)
}
}
/** /**
* Provides the list of unknown devices * Provides the list of unknown devices
* *
@ -1178,7 +1239,7 @@ internal class DefaultCryptoService @Inject constructor(
* ========================================================================================== */ * ========================================================================================== */
override fun toString(): String { override fun toString(): String {
return "DefaultCryptoService of " + credentials.userId + " (" + credentials.deviceId + ")" return "DefaultCryptoService of $userId ($deviceId)"
} }
override fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest> { override fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest> {
@ -1192,4 +1253,15 @@ internal class DefaultCryptoService @Inject constructor(
override fun getGossipingEventsTrail(): List<Event> { override fun getGossipingEventsTrail(): List<Event> {
return cryptoStore.getGossipingEventsTrail() return cryptoStore.getGossipingEventsTrail()
} }
/* ==========================================================================================
* For test only
* ========================================================================================== */
@VisibleForTesting
val cryptoStoreForTesting = cryptoStore
companion object {
const val CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS = 3_600_000 // one hour
}
} }

View file

@ -108,7 +108,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
res = !notReadyToRetryHS.contains(userId.substringAfterLast(':')) res = !notReadyToRetryHS.contains(userId.substringAfterLast(':'))
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## canRetryKeysDownload() failed") Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed")
} }
} }
@ -137,7 +137,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
for (userId in userIds) { for (userId in userIds) {
if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) { if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) {
Timber.v("## startTrackingDeviceList() : Now tracking device list for $userId") Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
isUpdated = true isUpdated = true
} }
@ -161,7 +161,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
for (userId in changed) { for (userId in changed) {
if (deviceTrackingStatuses.containsKey(userId)) { if (deviceTrackingStatuses.containsKey(userId)) {
Timber.v("## invalidateUserDeviceList() : Marking device list outdated for $userId") Timber.v("## CRYPTO | invalidateUserDeviceList() : Marking device list outdated for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
isUpdated = true isUpdated = true
} }
@ -169,7 +169,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
for (userId in left) { for (userId in left) {
if (deviceTrackingStatuses.containsKey(userId)) { if (deviceTrackingStatuses.containsKey(userId)) {
Timber.v("## invalidateUserDeviceList() : No longer tracking device list for $userId") Timber.v("## CRYPTO | invalidateUserDeviceList() : No longer tracking device list for $userId")
deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED
isUpdated = true isUpdated = true
} }
@ -259,7 +259,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
* @param forceDownload Always download the keys even if cached. * @param forceDownload Always download the keys even if cached.
*/ */
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<CryptoDeviceInfo> { suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<CryptoDeviceInfo> {
Timber.v("## downloadKeys() : forceDownload $forceDownload : $userIds") Timber.v("## CRYPTO | downloadKeys() : forceDownload $forceDownload : $userIds")
// Map from userId -> deviceId -> MXDeviceInfo // Map from userId -> deviceId -> MXDeviceInfo
val stored = MXUsersDevicesMap<CryptoDeviceInfo>() val stored = MXUsersDevicesMap<CryptoDeviceInfo>()
@ -288,13 +288,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
} }
} }
return if (downloadUsers.isEmpty()) { return if (downloadUsers.isEmpty()) {
Timber.v("## downloadKeys() : no new user device") Timber.v("## CRYPTO | downloadKeys() : no new user device")
stored stored
} else { } else {
Timber.v("## downloadKeys() : starts") Timber.v("## CRYPTO | downloadKeys() : starts")
val t0 = System.currentTimeMillis() val t0 = System.currentTimeMillis()
val result = doKeyDownloadForUsers(downloadUsers) val result = doKeyDownloadForUsers(downloadUsers)
Timber.v("## downloadKeys() : doKeyDownloadForUsers succeeds after ${System.currentTimeMillis() - t0} ms") Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${System.currentTimeMillis() - t0} ms")
result.also { result.also {
it.addEntriesFromMap(stored) it.addEntriesFromMap(stored)
} }
@ -307,7 +307,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
* @param downloadUsers the user ids list * @param downloadUsers the user ids list
*/ */
private suspend fun doKeyDownloadForUsers(downloadUsers: List<String>): MXUsersDevicesMap<CryptoDeviceInfo> { private suspend fun doKeyDownloadForUsers(downloadUsers: List<String>): MXUsersDevicesMap<CryptoDeviceInfo> {
Timber.v("## doKeyDownloadForUsers() : doKeyDownloadForUsers $downloadUsers") Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers $downloadUsers")
// get the user ids which did not already trigger a keys download // get the user ids which did not already trigger a keys download
val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) } val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) }
if (filteredUsers.isEmpty()) { if (filteredUsers.isEmpty()) {
@ -318,16 +318,16 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
val response = try { val response = try {
downloadKeysForUsersTask.execute(params) downloadKeysForUsersTask.execute(params)
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
Timber.e(throwable, "##doKeyDownloadForUsers(): error") Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
onKeysDownloadFailed(filteredUsers) onKeysDownloadFailed(filteredUsers)
throw throwable throw throwable
} }
Timber.v("## doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
for (userId in filteredUsers) { for (userId in filteredUsers) {
// al devices = // al devices =
val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) } val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }
Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $models") Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for $userId : $models")
if (!models.isNullOrEmpty()) { if (!models.isNullOrEmpty()) {
val workingCopy = models.toMutableMap() val workingCopy = models.toMutableMap()
for ((deviceId, deviceInfo) in models) { for ((deviceId, deviceInfo) in models) {
@ -361,13 +361,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
// Handle cross signing keys update // Handle cross signing keys update
val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also { val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also {
Timber.v("## CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}") Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}")
} }
val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also { val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also {
Timber.v("## CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}") Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
} }
val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also { val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also {
Timber.v("## CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}") Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
} }
cryptoStore.storeUserCrossSigningKeys( cryptoStore.storeUserCrossSigningKeys(
userId, userId,
@ -395,28 +395,28 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
*/ */
private fun validateDeviceKeys(deviceKeys: CryptoDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: CryptoDeviceInfo?): Boolean { private fun validateDeviceKeys(deviceKeys: CryptoDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: CryptoDeviceInfo?): Boolean {
if (null == deviceKeys) { if (null == deviceKeys) {
Timber.e("## validateDeviceKeys() : deviceKeys is null from $userId:$deviceId") Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys is null from $userId:$deviceId")
return false return false
} }
if (null == deviceKeys.keys) { if (null == deviceKeys.keys) {
Timber.e("## validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId") Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId")
return false return false
} }
if (null == deviceKeys.signatures) { if (null == deviceKeys.signatures) {
Timber.e("## validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId") Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId")
return false return false
} }
// Check that the user_id and device_id in the received deviceKeys are correct // Check that the user_id and device_id in the received deviceKeys are correct
if (deviceKeys.userId != userId) { if (deviceKeys.userId != userId) {
Timber.e("## validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId") Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId")
return false return false
} }
if (deviceKeys.deviceId != deviceId) { if (deviceKeys.deviceId != deviceId) {
Timber.e("## validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId") Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId")
return false return false
} }
@ -424,21 +424,21 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
val signKey = deviceKeys.keys[signKeyId] val signKey = deviceKeys.keys[signKeyId]
if (null == signKey) { if (null == signKey) {
Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key") Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key")
return false return false
} }
val signatureMap = deviceKeys.signatures[userId] val signatureMap = deviceKeys.signatures[userId]
if (null == signatureMap) { if (null == signatureMap) {
Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId") Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId")
return false return false
} }
val signature = signatureMap[signKeyId] val signature = signatureMap[signKeyId]
if (null == signature) { if (null == signature) {
Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed") Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed")
return false return false
} }
@ -453,7 +453,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
} }
if (!isVerified) { if (!isVerified) {
Timber.e("## validateDeviceKeys() : Unable to verify signature on device " + userId + ":" Timber.e("## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":"
+ deviceKeys.deviceId + " with error " + errorMessage) + deviceKeys.deviceId + " with error " + errorMessage)
return false return false
} }
@ -464,12 +464,12 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
// best off sticking with the original keys. // best off sticking with the original keys.
// //
// Should we warn the user about it somehow? // Should we warn the user about it somehow?
Timber.e("## validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" Timber.e("## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":"
+ deviceKeys.deviceId + " has changed : " + deviceKeys.deviceId + " has changed : "
+ previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey) + previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey)
Timber.e("## validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys") Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys")
Timber.e("## validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}") Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}")
return false return false
} }
@ -501,10 +501,10 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
doKeyDownloadForUsers(users) doKeyDownloadForUsers(users)
}.fold( }.fold(
{ {
Timber.v("## refreshOutdatedDeviceLists() : done") Timber.v("## CRYPTO | refreshOutdatedDeviceLists() : done")
}, },
{ {
Timber.e(it, "## refreshOutdatedDeviceLists() : ERROR updating device keys for users $users") Timber.e(it, "## CRYPTO | refreshOutdatedDeviceLists() : ERROR updating device keys for users $users")
} }
) )
} }

View file

@ -32,7 +32,10 @@ import im.vector.matrix.android.internal.crypto.model.rest.GossipingToDeviceObje
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -43,7 +46,10 @@ internal class IncomingGossipingRequestManager @Inject constructor(
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
private val cryptoConfig: MXCryptoConfig, private val cryptoConfig: MXCryptoConfig,
private val gossipingWorkManager: GossipingWorkManager, private val gossipingWorkManager: GossipingWorkManager,
private val roomDecryptorProvider: RoomDecryptorProvider) { private val roomEncryptorsStore: RoomEncryptorsStore,
private val roomDecryptorProvider: RoomDecryptorProvider,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope) {
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
// we received in the current sync. // we received in the current sync.
@ -90,7 +96,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
* @param event the announcement event. * @param event the announcement event.
*/ */
fun onGossipingRequestEvent(event: Event) { fun onGossipingRequestEvent(event: Event) {
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}") Timber.v("## CRYPTO | GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}")
val roomKeyShare = event.getClearContent().toModel<GossipingDefaultContent>() val roomKeyShare = event.getClearContent().toModel<GossipingDefaultContent>()
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
when (roomKeyShare?.action) { when (roomKeyShare?.action) {
@ -155,7 +161,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
} }
receivedRequestCancellations?.forEach { request -> receivedRequestCancellations?.forEach { request ->
Timber.v("## GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request") Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
// we should probably only notify the app of cancellations we told it // we should probably only notify the app of cancellations we told it
// about, but we don't currently have a record of that, so we just pass // about, but we don't currently have a record of that, so we just pass
// everything through. // everything through.
@ -178,17 +184,42 @@ internal class IncomingGossipingRequestManager @Inject constructor(
} }
private fun processIncomingRoomKeyRequest(request: IncomingRoomKeyRequest) { private fun processIncomingRoomKeyRequest(request: IncomingRoomKeyRequest) {
val userId = request.userId val userId = request.userId ?: return
val deviceId = request.deviceId val deviceId = request.deviceId ?: return
val body = request.requestBody val body = request.requestBody ?: return
val roomId = body!!.roomId val roomId = body.roomId ?: return
val alg = body.algorithm val alg = body.algorithm ?: return
Timber.v("## GOSSIP processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}") Timber.v("## CRYPTO | GOSSIP processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
if (userId == null || credentials.userId != userId) { if (credentials.userId != userId) {
// TODO: determine if we sent this device the keys already: in Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request from other user")
Timber.w("## GOSSIP processReceivedGossipingRequests() : Ignoring room key request from other user for now") val senderKey = body.senderKey ?: return Unit
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) .also { Timber.w("missing senderKey") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
val sessionId = body.sessionId ?: return Unit
.also { Timber.w("missing sessionId") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
if (alg != MXCRYPTO_ALGORITHM_MEGOLM) {
return Unit
.also { Timber.w("Only megolm is accepted here") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
}
val roomEncryptor = roomEncryptorsStore.get(roomId) ?: return Unit
.also { Timber.w("no room Encryptor") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
val isSuccess = roomEncryptor.reshareKey(sessionId, userId, deviceId, senderKey)
if (isSuccess) {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
} else {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.UNABLE_TO_PROCESS)
}
}
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.RE_REQUESTED)
return return
} }
// TODO: should we queue up requests we don't yet have keys for, in case they turn up later? // TODO: should we queue up requests we don't yet have keys for, in case they turn up later?
@ -196,18 +227,18 @@ internal class IncomingGossipingRequestManager @Inject constructor(
// the keys for the requested events, and can drop the requests. // the keys for the requested events, and can drop the requests.
val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg) val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg)
if (null == decryptor) { if (null == decryptor) {
Timber.w("## GOSSIP processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId") Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request for unknown $alg in room $roomId")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return return
} }
if (!decryptor.hasKeysForKeyRequest(request)) { if (!decryptor.hasKeysForKeyRequest(request)) {
Timber.w("## GOSSIP processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}") Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request for unknown session ${body.sessionId!!}")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return return
} }
if (credentials.deviceId == deviceId && credentials.userId == userId) { if (credentials.deviceId == deviceId && credentials.userId == userId) {
Timber.v("## GOSSIP processReceivedGossipingRequests() : oneself device - ignored") Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : oneself device - ignored")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return return
} }
@ -219,16 +250,16 @@ internal class IncomingGossipingRequestManager @Inject constructor(
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
} }
// if the device is verified already, share the keys // if the device is verified already, share the keys
val device = cryptoStore.getUserDevice(userId, deviceId!!) val device = cryptoStore.getUserDevice(userId, deviceId)
if (device != null) { if (device != null) {
if (device.isVerified) { if (device.isVerified) {
Timber.v("## GOSSIP processReceivedGossipingRequests() : device is already verified: sharing keys") Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : device is already verified: sharing keys")
request.share?.run() request.share?.run()
return return
} }
if (device.isBlocked) { if (device.isBlocked) {
Timber.v("## GOSSIP processReceivedGossipingRequests() : device is blocked -> ignored") Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : device is blocked -> ignored")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return return
} }
@ -236,7 +267,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
// As per config we automatically discard untrusted devices request // As per config we automatically discard untrusted devices request
if (cryptoConfig.discardRoomKeyRequestsFromUntrustedDevices) { if (cryptoConfig.discardRoomKeyRequestsFromUntrustedDevices) {
Timber.v("## processReceivedGossipingRequests() : discardRoomKeyRequestsFromUntrustedDevices") Timber.v("## CRYPTO | processReceivedGossipingRequests() : discardRoomKeyRequestsFromUntrustedDevices")
// At this point the device is unknown, we don't want to bother user with that // At this point the device is unknown, we don't want to bother user with that
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return return
@ -249,30 +280,30 @@ internal class IncomingGossipingRequestManager @Inject constructor(
private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) { private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) {
val secretName = request.secretName ?: return Unit.also { val secretName = request.secretName ?: return Unit.also {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
Timber.v("## GOSSIP processIncomingSecretShareRequest() : Missing secret name") Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Missing secret name")
} }
val userId = request.userId val userId = request.userId
if (userId == null || credentials.userId != userId) { if (userId == null || credentials.userId != userId) {
Timber.e("## GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from other users") Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from other users")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return return
} }
val deviceId = request.deviceId val deviceId = request.deviceId
?: return Unit.also { ?: return Unit.also {
Timber.e("## GOSSIP processIncomingSecretShareRequest() : Malformed request, no ") Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Malformed request, no ")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
} }
val device = cryptoStore.getUserDevice(userId, deviceId) val device = cryptoStore.getUserDevice(userId, deviceId)
?: return Unit.also { ?: return Unit.also {
Timber.e("## GOSSIP processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}") Timber.e("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Received secret share request from unknown device ${request.deviceId}")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
} }
if (!device.isVerified || device.isBlocked) { if (!device.isVerified || device.isBlocked) {
Timber.v("## GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device") Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Ignoring secret share request from untrusted/blocked session $device")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
return return
} }
@ -289,7 +320,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
} }
else -> null else -> null
}?.let { secretValue -> }?.let { secretValue ->
Timber.i("## GOSSIP processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted") Timber.i("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Sharing secret $secretName with $device locally trusted")
if (isDeviceLocallyVerified == true && hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId)) { if (isDeviceLocallyVerified == true && hasBeenVerifiedLessThanFiveMinutesFromNow(deviceId)) {
val params = SendGossipWorker.Params( val params = SendGossipWorker.Params(
sessionId = sessionId, sessionId = sessionId,
@ -301,13 +332,13 @@ internal class IncomingGossipingRequestManager @Inject constructor(
val workRequest = gossipingWorkManager.createWork<SendGossipWorker>(WorkerParamsFactory.toData(params), true) val workRequest = gossipingWorkManager.createWork<SendGossipWorker>(WorkerParamsFactory.toData(params), true)
gossipingWorkManager.postWork(workRequest) gossipingWorkManager.postWork(workRequest)
} else { } else {
Timber.v("## GOSSIP processIncomingSecretShareRequest() : Can't share secret $secretName with $device, verification too old") Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : Can't share secret $secretName with $device, verification too old")
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
} }
return return
} }
Timber.v("## GOSSIP processIncomingSecretShareRequest() : $secretName unknown at SDK level, asking to app layer") Timber.v("## CRYPTO | GOSSIP processIncomingSecretShareRequest() : $secretName unknown at SDK level, asking to app layer")
request.ignore = Runnable { request.ignore = Runnable {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
@ -341,7 +372,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
try { try {
listener.onRoomKeyRequest(request) listener.onRoomKeyRequest(request)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## onRoomKeyRequest() failed") Timber.e(e, "## CRYPTO | onRoomKeyRequest() failed")
} }
} }
} }
@ -358,7 +389,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
return return
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## GOSSIP onRoomKeyRequest() failed") Timber.e(e, "## CRYPTO | GOSSIP onRoomKeyRequest() failed")
} }
} }
} }
@ -377,7 +408,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
try { try {
listener.onRoomKeyRequestCancellation(request) listener.onRoomKeyRequestCancellation(request)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## GOSSIP onRoomKeyRequestCancellation() failed") Timber.e(e, "## CRYPTO | GOSSIP onRoomKeyRequestCancellation() failed")
} }
} }
} }

View file

@ -342,6 +342,8 @@ internal class MXOlmDevice @Inject constructor(
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## encryptMessage() : failed") Timber.e(e, "## encryptMessage() : failed")
} }
} else {
Timber.e("## encryptMessage() : Failed to encrypt unknown session $sessionId")
} }
return res return res
@ -625,6 +627,7 @@ internal class MXOlmDevice @Inject constructor(
* @param senderKey the base64-encoded curve25519 key of the sender. * @param senderKey the base64-encoded curve25519 key of the sender.
* @return the decrypting result. Nil if the sessionId is unknown. * @return the decrypting result. Nil if the sessionId is unknown.
*/ */
@Throws(MXCryptoError::class)
fun decryptGroupMessage(body: String, fun decryptGroupMessage(body: String,
roomId: String, roomId: String,
timeline: String?, timeline: String?,
@ -662,8 +665,7 @@ internal class MXOlmDevice @Inject constructor(
adapter.fromJson(payloadString) adapter.fromJson(payloadString)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e("## decryptGroupMessage() : fails to parse the payload") Timber.e("## decryptGroupMessage() : fails to parse the payload")
throw throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
} }
return OlmDecryptionResult( return OlmDecryptionResult(

View file

@ -55,7 +55,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients)?.let { cryptoStore.getOrAddOutgoingRoomKeyRequest(requestBody, recipients)?.let {
// Don't resend if it's already done, you need to cancel first (reRequest) // Don't resend if it's already done, you need to cancel first (reRequest)
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) { if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
Timber.v("## GOSSIP sendOutgoingRoomKeyRequest() : we already request for that session: $it") Timber.v("## CRYPTO - GOSSIP sendOutgoingRoomKeyRequest() : we already request for that session: $it")
return@launch return@launch
} }
@ -72,7 +72,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let { cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let {
// TODO check if there is already one that is being sent? // TODO check if there is already one that is being sent?
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) { if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
Timber.v("## GOSSIP sendSecretShareRequest() : we already request for that session: $it") Timber.v("## CRYPTO - GOSSIP sendSecretShareRequest() : we already request for that session: $it")
return@launch return@launch
} }
@ -113,7 +113,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody) val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody)
?: // no request was made for this key ?: // no request was made for this key
return Unit.also { return Unit.also {
Timber.v("## GOSSIP cancelRoomKeyRequest() Unknown request") Timber.v("## CRYPTO - GOSSIP cancelRoomKeyRequest() Unknown request $requestBody")
} }
sendOutgoingRoomKeyRequestCancellation(req, andResend) sendOutgoingRoomKeyRequestCancellation(req, andResend)
@ -125,7 +125,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
* @param request the request * @param request the request
*/ */
private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) { private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) {
Timber.v("## GOSSIP sendOutgoingRoomKeyRequest() : Requesting keys $request") Timber.v("## CRYPTO - GOSSIP sendOutgoingRoomKeyRequest() : Requesting keys $request")
val params = SendGossipRequestWorker.Params( val params = SendGossipRequestWorker.Params(
sessionId = sessionId, sessionId = sessionId,
@ -143,7 +143,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
* @param request the request * @param request the request
*/ */
private fun sendOutgoingRoomKeyRequestCancellation(request: OutgoingRoomKeyRequest, resend: Boolean = false) { private fun sendOutgoingRoomKeyRequestCancellation(request: OutgoingRoomKeyRequest, resend: Boolean = false) {
Timber.v("$request") Timber.v("## CRYPTO - sendOutgoingRoomKeyRequestCancellation $request")
val params = CancelGossipRequestWorker.Params.fromRequest(sessionId, request) val params = CancelGossipRequestWorker.Params.fromRequest(sessionId, request)
cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.CANCELLING) cryptoStore.updateOutgoingGossipingRequestState(request.requestId, OutgoingGossipingRequestState.CANCELLING)

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.crypto
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
import im.vector.matrix.android.internal.session.SessionScope
import javax.inject.Inject
@SessionScope
internal class RoomEncryptorsStore @Inject constructor() {
// MXEncrypting instance for each room.
private val roomEncryptors = mutableMapOf<String, IMXEncrypting>()
fun put(roomId: String, alg: IMXEncrypting) {
synchronized(roomEncryptors) {
roomEncryptors.put(roomId, alg)
}
}
fun get(roomId: String): IMXEncrypting? {
return synchronized(roomEncryptors) {
roomEncryptors[roomId]
}
}
}

View file

@ -25,10 +25,11 @@ import im.vector.matrix.android.internal.crypto.tasks.ClaimOneTimeKeysForUsersDe
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val olmDevice: MXOlmDevice, internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) { private val olmDevice: MXOlmDevice,
private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) {
suspend fun handle(devicesByUser: Map<String, List<CryptoDeviceInfo>>): MXUsersDevicesMap<MXOlmSessionResult> { suspend fun handle(devicesByUser: Map<String, List<CryptoDeviceInfo>>, force: Boolean = false): MXUsersDevicesMap<MXOlmSessionResult> {
val devicesWithoutSession = ArrayList<CryptoDeviceInfo>() val devicesWithoutSession = ArrayList<CryptoDeviceInfo>()
val results = MXUsersDevicesMap<MXOlmSessionResult>() val results = MXUsersDevicesMap<MXOlmSessionResult>()
@ -40,7 +41,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
val sessionId = olmDevice.getSessionId(key!!) val sessionId = olmDevice.getSessionId(key!!)
if (sessionId.isNullOrEmpty()) { if (sessionId.isNullOrEmpty() || force) {
devicesWithoutSession.add(deviceInfo) devicesWithoutSession.add(deviceInfo)
} }
@ -68,11 +69,11 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
// //
// That should eventually resolve itself, but it's poor form. // That should eventually resolve itself, but it's poor form.
Timber.v("## claimOneTimeKeysForUsersDevices() : $usersDevicesToClaim") Timber.v("## CRYPTO | claimOneTimeKeysForUsersDevices() : $usersDevicesToClaim")
val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim) val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
val oneTimeKeys = oneTimeKeysForUsersDeviceTask.execute(claimParams) val oneTimeKeys = oneTimeKeysForUsersDeviceTask.execute(claimParams)
Timber.v("## claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys") Timber.v("## CRYPTO | claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys")
for ((userId, deviceInfos) in devicesByUser) { for ((userId, deviceInfos) in devicesByUser) {
for (deviceInfo in deviceInfos) { for (deviceInfo in deviceInfos) {
var oneTimeKey: MXKey? = null var oneTimeKey: MXKey? = null
@ -80,7 +81,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
if (null != deviceIds) { if (null != deviceIds) {
for (deviceId in deviceIds) { for (deviceId in deviceIds) {
val olmSessionResult = results.getObject(userId, deviceId) val olmSessionResult = results.getObject(userId, deviceId)
if (olmSessionResult!!.sessionId != null) { if (olmSessionResult!!.sessionId != null && !force) {
// We already have a result for this device // We already have a result for this device
continue continue
} }
@ -89,7 +90,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
oneTimeKey = key oneTimeKey = key
} }
if (oneTimeKey == null) { if (oneTimeKey == null) {
Timber.v("## ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm Timber.v("## CRYPTO | ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm
+ " for device " + userId + " : " + deviceId) + " for device " + userId + " : " + deviceId)
continue continue
} }
@ -125,14 +126,14 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
sessionId = olmDevice.createOutboundSession(deviceInfo.identityKey()!!, oneTimeKey.value) sessionId = olmDevice.createOutboundSession(deviceInfo.identityKey()!!, oneTimeKey.value)
if (!sessionId.isNullOrEmpty()) { if (!sessionId.isNullOrEmpty()) {
Timber.v("## verifyKeyAndStartSession() : Started new sessionid " + sessionId Timber.v("## CRYPTO | verifyKeyAndStartSession() : Started new sessionid " + sessionId
+ " for device " + deviceInfo + "(theirOneTimeKey: " + oneTimeKey.value + ")") + " for device " + deviceInfo + "(theirOneTimeKey: " + oneTimeKey.value + ")")
} else { } else {
// Possibly a bad key // Possibly a bad key
Timber.e("## verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId") Timber.e("## CRYPTO | verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId")
} }
} else { } else {
Timber.e("## verifyKeyAndStartSession() : Unable to verify signature on one-time key for device " + userId Timber.e("## CRYPTO | verifyKeyAndStartSession() : Unable to verify signature on one-time key for device " + userId
+ ":" + deviceId + " Error " + errorMessage) + ":" + deviceId + " Error " + errorMessage)
} }
} }

View file

@ -16,19 +16,24 @@
package im.vector.matrix.android.internal.crypto.actions package im.vector.matrix.android.internal.crypto.actions
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_OLM import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_OLM
import im.vector.matrix.android.internal.crypto.MXOlmDevice import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedMessage import im.vector.matrix.android.internal.crypto.model.rest.EncryptedMessage
import im.vector.matrix.android.internal.di.DeviceId
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.convertToUTF8 import im.vector.matrix.android.internal.util.convertToUTF8
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class MessageEncrypter @Inject constructor(private val credentials: Credentials, internal class MessageEncrypter @Inject constructor(
private val olmDevice: MXOlmDevice) { @UserId
private val userId: String,
@DeviceId
private val deviceId: String?,
private val olmDevice: MXOlmDevice) {
/** /**
* Encrypt an event payload for a list of devices. * Encrypt an event payload for a list of devices.
* This method must be called from the getCryptoHandler() thread. * This method must be called from the getCryptoHandler() thread.
@ -37,13 +42,13 @@ internal class MessageEncrypter @Inject constructor(private val credentials: Cre
* @param deviceInfos list of device infos to encrypt for. * @param deviceInfos list of device infos to encrypt for.
* @return the content for an m.room.encrypted event. * @return the content for an m.room.encrypted event.
*/ */
fun encryptMessage(payloadFields: Map<String, Any>, deviceInfos: List<CryptoDeviceInfo>): EncryptedMessage { fun encryptMessage(payloadFields: Content, deviceInfos: List<CryptoDeviceInfo>): EncryptedMessage {
val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! } val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! }
val payloadJson = payloadFields.toMutableMap() val payloadJson = payloadFields.toMutableMap()
payloadJson["sender"] = credentials.userId payloadJson["sender"] = userId
payloadJson["sender_device"] = credentials.deviceId!! payloadJson["sender_device"] = deviceId!!
// Include the Ed25519 key so that the recipient knows what // Include the Ed25519 key so that the recipient knows what
// device this message came from. // device this message came from.
@ -53,11 +58,9 @@ internal class MessageEncrypter @Inject constructor(private val credentials: Cre
// homeserver signed by the ed25519 key this proves that // homeserver signed by the ed25519 key this proves that
// the curve25519 key and the ed25519 key are owned by // the curve25519 key and the ed25519 key are owned by
// the same device. // the same device.
val keysMap = HashMap<String, String>() payloadJson["keys"] = mapOf("ed25519" to olmDevice.deviceEd25519Key!!)
keysMap["ed25519"] = olmDevice.deviceEd25519Key!!
payloadJson["keys"] = keysMap
val ciphertext = HashMap<String, Any>() val ciphertext = mutableMapOf<String, Any>()
for ((deviceKey, deviceInfo) in deviceInfoParticipantKey) { for ((deviceKey, deviceInfo) in deviceInfoParticipantKey) {
val sessionId = olmDevice.getSessionId(deviceKey) val sessionId = olmDevice.getSessionId(deviceKey)

View file

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.crypto.algorithms package im.vector.matrix.android.internal.crypto.algorithms
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
@ -35,6 +36,7 @@ internal interface IMXDecrypting {
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the decryption information, or an error * @return the decryption information, or an error
*/ */
@Throws(MXCryptoError::class)
fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
/** /**

View file

@ -33,4 +33,34 @@ internal interface IMXEncrypting {
* @return the encrypted content * @return the encrypted content
*/ */
suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content
/**
* In Megolm, each recipient maintains a record of the ratchet value which allows
* them to decrypt any messages sent in the session after the corresponding point
* in the conversation. If this value is compromised, an attacker can similarly
* decrypt past messages which were encrypted by a key derived from the
* compromised or subsequent ratchet values. This gives 'partial' forward
* secrecy.
*
* To mitigate this issue, the application should offer the user the option to
* discard historical conversations, by winding forward any stored ratchet values,
* or discarding sessions altogether.
*/
fun discardSessionKey()
/**
* Re-shares a session key with devices if the key has already been
* sent to them.
*
* @param sessionId The id of the outbound session to share.
* @param userId The id of the user who owns the target device.
* @param deviceId The id of the target device.
* @param senderKey The key of the originating device for the session.
*
* @return true in case of success
*/
suspend fun reshareKey(sessionId: String,
userId: String,
deviceId: String,
senderKey: String): Boolean
} }

View file

@ -63,6 +63,7 @@ internal class MXMegolmDecryption(private val userId: String,
*/ */
private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap() private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()
@Throws(MXCryptoError::class)
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
// If cross signing is enabled, we don't send request until the keys are trusted // If cross signing is enabled, we don't send request until the keys are trusted
// There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once // There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once
@ -70,7 +71,9 @@ internal class MXMegolmDecryption(private val userId: String,
return decryptEvent(event, timeline, requestOnFail) return decryptEvent(event, timeline, requestOnFail)
} }
@Throws(MXCryptoError::class)
private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult { private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
Timber.v("## CRYPTO | decryptEvent ${event.eventId} , requestKeysOnFail:$requestKeysOnFail")
if (event.roomId.isNullOrBlank()) { if (event.roomId.isNullOrBlank()) {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
} }
@ -188,7 +191,7 @@ internal class MXMegolmDecryption(private val userId: String,
val events = timeline.getOrPut(timelineId) { ArrayList() } val events = timeline.getOrPut(timelineId) { ArrayList() }
if (event !in events) { if (event !in events) {
Timber.v("## addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}") Timber.v("## CRYPTO | addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}")
events.add(event) events.add(event)
} }
} }
@ -199,6 +202,7 @@ internal class MXMegolmDecryption(private val userId: String,
* @param event the key event. * @param event the key event.
*/ */
override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) { override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {
Timber.v("## CRYPTO | onRoomKeyEvent()")
var exportFormat = false var exportFormat = false
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
@ -207,11 +211,11 @@ internal class MXMegolmDecryption(private val userId: String,
val forwardingCurve25519KeyChain: MutableList<String> = ArrayList() val forwardingCurve25519KeyChain: MutableList<String> = ArrayList()
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.sessionId.isNullOrEmpty() || roomKeyContent.sessionKey.isNullOrEmpty()) { if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.sessionId.isNullOrEmpty() || roomKeyContent.sessionKey.isNullOrEmpty()) {
Timber.e("## onRoomKeyEvent() : Key event is missing fields") Timber.e("## CRYPTO | onRoomKeyEvent() : Key event is missing fields")
return return
} }
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
Timber.v("## onRoomKeyEvent(), forward adding key : roomId ${roomKeyContent.roomId}" + Timber.v("## CRYPTO | onRoomKeyEvent(), forward adding key : roomId ${roomKeyContent.roomId}" +
" sessionId ${roomKeyContent.sessionId} sessionKey ${roomKeyContent.sessionKey}") " sessionId ${roomKeyContent.sessionId} sessionKey ${roomKeyContent.sessionKey}")
val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>() val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>()
?: return ?: return
@ -221,7 +225,7 @@ internal class MXMegolmDecryption(private val userId: String,
} }
if (senderKey == null) { if (senderKey == null) {
Timber.e("## onRoomKeyEvent() : event is missing sender_key field") Timber.e("## CRYPTO | onRoomKeyEvent() : event is missing sender_key field")
return return
} }
@ -230,18 +234,18 @@ internal class MXMegolmDecryption(private val userId: String,
exportFormat = true exportFormat = true
senderKey = forwardedRoomKeyContent.senderKey senderKey = forwardedRoomKeyContent.senderKey
if (null == senderKey) { if (null == senderKey) {
Timber.e("## onRoomKeyEvent() : forwarded_room_key event is missing sender_key field") Timber.e("## CRYPTO | onRoomKeyEvent() : forwarded_room_key event is missing sender_key field")
return return
} }
if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) { if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) {
Timber.e("## forwarded_room_key_event is missing sender_claimed_ed25519_key field") Timber.e("## CRYPTO | forwarded_room_key_event is missing sender_claimed_ed25519_key field")
return return
} }
keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key
} else { } else {
Timber.v("## onRoomKeyEvent(), Adding key : roomId " + roomKeyContent.roomId + " sessionId " + roomKeyContent.sessionId Timber.v("## CRYPTO | onRoomKeyEvent(), Adding key : roomId " + roomKeyContent.roomId + " sessionId " + roomKeyContent.sessionId
+ " sessionKey " + roomKeyContent.sessionKey) // from " + event); + " sessionKey " + roomKeyContent.sessionKey) // from " + event);
if (null == senderKey) { if (null == senderKey) {
@ -253,6 +257,7 @@ internal class MXMegolmDecryption(private val userId: String,
keysClaimed = event.getKeysClaimed().toMutableMap() keysClaimed = event.getKeysClaimed().toMutableMap()
} }
Timber.e("## CRYPTO | onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId, val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId,
roomKeyContent.sessionKey, roomKeyContent.sessionKey,
roomKeyContent.roomId, roomKeyContent.roomId,
@ -284,7 +289,7 @@ internal class MXMegolmDecryption(private val userId: String,
* @param sessionId the session id * @param sessionId the session id
*/ */
override fun onNewSession(senderKey: String, sessionId: String) { override fun onNewSession(senderKey: String, sessionId: String) {
Timber.v("ON NEW SESSION $sessionId - $senderKey") Timber.v(" CRYPTO | ON NEW SESSION $sessionId - $senderKey")
newSessionListener?.onNewSession(null, senderKey, sessionId) newSessionListener?.onNewSession(null, senderKey, sessionId)
} }
@ -318,7 +323,7 @@ internal class MXMegolmDecryption(private val userId: String,
// were no one-time keys. // were no one-time keys.
return@mapCatching return@mapCatching
} }
Timber.v("## shareKeysWithDevice() : sharing keys for session" + Timber.v("## CRYPTO | shareKeysWithDevice() : sharing keys for session" +
" ${body.senderKey}|${body.sessionId} with device $userId:$deviceId") " ${body.senderKey}|${body.sessionId} with device $userId:$deviceId")
val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY) val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY)
@ -337,7 +342,7 @@ internal class MXMegolmDecryption(private val userId: String,
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
val sendToDeviceMap = MXUsersDevicesMap<Any>() val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(userId, deviceId, encodedPayload) sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
Timber.v("## shareKeysWithDevice() : sending to $userId:$deviceId") Timber.v("## CRYPTO | shareKeysWithDevice() : sending to $userId:$deviceId")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
sendToDeviceTask.execute(sendToDeviceParams) sendToDeviceTask.execute(sendToDeviceParams)
} }

View file

@ -40,7 +40,7 @@ import timber.log.Timber
internal class MXMegolmEncryption( internal class MXMegolmEncryption(
// The id of the room we will be sending to. // The id of the room we will be sending to.
private var roomId: String, private val roomId: String,
private val olmDevice: MXOlmDevice, private val olmDevice: MXOlmDevice,
private val defaultKeysBackupService: DefaultKeysBackupService, private val defaultKeysBackupService: DefaultKeysBackupService,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
@ -66,17 +66,25 @@ internal class MXMegolmEncryption(
override suspend fun encryptEventContent(eventContent: Content, override suspend fun encryptEventContent(eventContent: Content,
eventType: String, eventType: String,
userIds: List<String>): Content { userIds: List<String>): Content {
val ts = System.currentTimeMillis()
Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom")
val devices = getDevicesInRoom(userIds) val devices = getDevicesInRoom(userIds)
Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.map}")
val outboundSession = ensureOutboundSession(devices) val outboundSession = ensureOutboundSession(devices)
return encryptContent(outboundSession, eventType, eventContent) return encryptContent(outboundSession, eventType, eventContent)
} }
override fun discardSessionKey() {
outboundSession = null
}
/** /**
* Prepare a new session. * Prepare a new session.
* *
* @return the session description * @return the session description
*/ */
private fun prepareNewSessionInRoom(): MXOutboundSessionInfo { private fun prepareNewSessionInRoom(): MXOutboundSessionInfo {
Timber.v("## CRYPTO | prepareNewSessionInRoom() ")
val sessionId = olmDevice.createOutboundGroupSession() val sessionId = olmDevice.createOutboundGroupSession()
val keysClaimedMap = HashMap<String, String>() val keysClaimedMap = HashMap<String, String>()
@ -96,6 +104,7 @@ internal class MXMegolmEncryption(
* @param devicesInRoom the devices list * @param devicesInRoom the devices list
*/ */
private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): MXOutboundSessionInfo { private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): MXOutboundSessionInfo {
Timber.v("## CRYPTO | ensureOutboundSession start")
var session = outboundSession var session = outboundSession
if (session == null if (session == null
// Need to make a brand new session? // Need to make a brand new session?
@ -132,7 +141,7 @@ internal class MXMegolmEncryption(
devicesByUsers: Map<String, List<CryptoDeviceInfo>>) { devicesByUsers: Map<String, List<CryptoDeviceInfo>>) {
// nothing to send, the task is done // nothing to send, the task is done
if (devicesByUsers.isEmpty()) { if (devicesByUsers.isEmpty()) {
Timber.v("## shareKey() : nothing more to do") Timber.v("## CRYPTO | shareKey() : nothing more to do")
return return
} }
// reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user) // reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user)
@ -145,7 +154,7 @@ internal class MXMegolmEncryption(
break break
} }
} }
Timber.v("## shareKey() ; userId ${subMap.keys}") Timber.v("## CRYPTO | shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}")
shareUserDevicesKey(session, subMap) shareUserDevicesKey(session, subMap)
val remainingDevices = devicesByUsers - subMap.keys val remainingDevices = devicesByUsers - subMap.keys
shareKey(session, remainingDevices) shareKey(session, remainingDevices)
@ -174,10 +183,10 @@ internal class MXMegolmEncryption(
payload["content"] = submap payload["content"] = submap
var t0 = System.currentTimeMillis() var t0 = System.currentTimeMillis()
Timber.v("## shareUserDevicesKey() : starts") Timber.v("## CRYPTO | shareUserDevicesKey() : starts")
val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser) val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
Timber.v("## shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after " Timber.v("## CRYPTO | shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after "
+ (System.currentTimeMillis() - t0) + " ms") + (System.currentTimeMillis() - t0) + " ms")
val contentMap = MXUsersDevicesMap<Any>() val contentMap = MXUsersDevicesMap<Any>()
var haveTargets = false var haveTargets = false
@ -200,17 +209,17 @@ internal class MXMegolmEncryption(
// so just skip it. // so just skip it.
continue continue
} }
Timber.v("## shareUserDevicesKey() : Sharing keys with device $userId:$deviceID") Timber.v("## CRYPTO | shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo))) contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo)))
haveTargets = true haveTargets = true
} }
} }
if (haveTargets) { if (haveTargets) {
t0 = System.currentTimeMillis() t0 = System.currentTimeMillis()
Timber.v("## shareUserDevicesKey() : has target") Timber.v("## CRYPTO | shareUserDevicesKey() : has target")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
sendToDeviceTask.execute(sendToDeviceParams) sendToDeviceTask.execute(sendToDeviceParams)
Timber.v("## shareUserDevicesKey() : sendToDevice succeeds after " Timber.v("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after "
+ (System.currentTimeMillis() - t0) + " ms") + (System.currentTimeMillis() - t0) + " ms")
// Add the devices we have shared with to session.sharedWithDevices. // Add the devices we have shared with to session.sharedWithDevices.
@ -224,7 +233,7 @@ internal class MXMegolmEncryption(
} }
} }
} else { } else {
Timber.v("## shareUserDevicesKey() : no need to sharekey") Timber.v("## CRYPTO | shareUserDevicesKey() : no need to sharekey")
} }
} }
@ -305,4 +314,49 @@ internal class MXMegolmEncryption(
throw MXCryptoError.UnknownDevice(unknownDevices) throw MXCryptoError.UnknownDevice(unknownDevices)
} }
} }
override suspend fun reshareKey(sessionId: String,
userId: String,
deviceId: String,
senderKey: String): Boolean {
Timber.d("[MXMegolmEncryption] reshareKey: $sessionId to $userId:$deviceId")
val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false
.also { Timber.w("Device not found") }
// Get the chain index of the key we previously sent this device
val chainIndex = outboundSession?.sharedWithDevices?.getObject(userId, deviceId)?.toLong() ?: return false
.also { Timber.w("[MXMegolmEncryption] reshareKey : ERROR : Never share megolm with this device") }
val devicesByUser = mapOf(userId to listOf(deviceInfo))
val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
val olmSessionResult = usersDeviceMap.getObject(userId, deviceId)
olmSessionResult?.sessionId
?: // no session with this device, probably because there were no one-time keys.
// ensureOlmSessionsForDevicesAction has already done the logging, so just skip it.
return false
Timber.d("[MXMegolmEncryption] reshareKey: sharing keys for session $senderKey|$sessionId:$chainIndex with device $userId:$deviceId")
val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY)
runCatching { olmDevice.getInboundGroupSession(sessionId, senderKey, roomId) }
.fold(
{
// TODO
payloadJson["content"] = it.exportKeys(chainIndex) ?: ""
},
{
// TODO
}
)
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
val sendToDeviceMap = MXUsersDevicesMap<Any>()
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
Timber.v("## CRYPTO | CRYPTO | shareKeysWithDevice() : sending to $userId:$deviceId")
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
sendToDeviceTask.execute(sendToDeviceParams)
return true
}
} }

View file

@ -38,6 +38,7 @@ internal class MXOlmDecryption(
private val userId: String) private val userId: String)
: IMXDecrypting { : IMXDecrypting {
@Throws(MXCryptoError::class)
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
val olmEventContent = event.content.toModel<OlmEventContent>() ?: run { val olmEventContent = event.content.toModel<OlmEventContent>() ?: run {
Timber.e("## decryptEvent() : bad event format") Timber.e("## decryptEvent() : bad event format")

View file

@ -29,7 +29,7 @@ import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
internal class MXOlmEncryption( internal class MXOlmEncryption(
private var roomId: String, private val roomId: String,
private val olmDevice: MXOlmDevice, private val olmDevice: MXOlmDevice,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
private val messageEncrypter: MessageEncrypter, private val messageEncrypter: MessageEncrypter,
@ -78,4 +78,13 @@ internal class MXOlmEncryption(
deviceListManager.downloadKeys(users, false) deviceListManager.downloadKeys(users, false)
ensureOlmSessionsForUsersAction.handle(users) ensureOlmSessionsForUsersAction.handle(users)
} }
override fun discardSessionKey() {
// No need for olm
}
override suspend fun reshareKey(sessionId: String, userId: String, deviceId: String, senderKey: String): Boolean {
// No need for olm
return false
}
} }

View file

@ -103,10 +103,11 @@ class OlmInboundGroupSessionWrapper : Serializable {
/** /**
* Export the inbound group session keys * Export the inbound group session keys
* @param index the index to export. If null, the first known index will be used
* *
* @return the inbound group session as MegolmSessionData if the operation succeeds * @return the inbound group session as MegolmSessionData if the operation succeeds
*/ */
fun exportKeys(): MegolmSessionData? { fun exportKeys(index: Long? = null): MegolmSessionData? {
return try { return try {
if (null == forwardingCurve25519KeyChain) { if (null == forwardingCurve25519KeyChain) {
forwardingCurve25519KeyChain = ArrayList() forwardingCurve25519KeyChain = ArrayList()
@ -116,6 +117,8 @@ class OlmInboundGroupSessionWrapper : Serializable {
return null return null
} }
val wantedIndex = index ?: olmInboundGroupSession!!.firstKnownIndex
MegolmSessionData( MegolmSessionData(
senderClaimedEd25519Key = keysClaimed?.get("ed25519"), senderClaimedEd25519Key = keysClaimed?.get("ed25519"),
forwardingCurve25519KeyChain = ArrayList(forwardingCurve25519KeyChain!!), forwardingCurve25519KeyChain = ArrayList(forwardingCurve25519KeyChain!!),
@ -123,7 +126,7 @@ class OlmInboundGroupSessionWrapper : Serializable {
senderClaimedKeys = keysClaimed, senderClaimedKeys = keysClaimed,
roomId = roomId, roomId = roomId,
sessionId = olmInboundGroupSession!!.sessionIdentifier(), sessionId = olmInboundGroupSession!!.sessionIdentifier(),
sessionKey = olmInboundGroupSession!!.export(olmInboundGroupSession!!.firstKnownIndex), sessionKey = olmInboundGroupSession!!.export(wantedIndex),
algorithm = MXCRYPTO_ALGORITHM_MEGOLM algorithm = MXCRYPTO_ALGORITHM_MEGOLM
) )
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.crypto.model.rest
/**
* Class representing the dummy content
* Ref: https://matrix.org/docs/spec/client_server/latest#id82
*/
typealias DummyContent = Unit

View file

@ -20,28 +20,53 @@ import com.squareup.moshi.JsonClass
/** /**
* Class representing the forward room key request body content * Class representing the forward room key request body content
* Ref: https://matrix.org/docs/spec/client_server/latest#m-forwarded-room-key
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ForwardedRoomKeyContent( data class ForwardedRoomKeyContent(
/**
* Required. The encryption algorithm the key in this event is to be used with.
*/
@Json(name = "algorithm") @Json(name = "algorithm")
val algorithm: String? = null, val algorithm: String? = null,
/**
* Required. The room where the key is used.
*/
@Json(name = "room_id") @Json(name = "room_id")
val roomId: String? = null, val roomId: String? = null,
/**
* Required. The Curve25519 key of the device which initiated the session originally.
*/
@Json(name = "sender_key") @Json(name = "sender_key")
val senderKey: String? = null, val senderKey: String? = null,
/**
* Required. The ID of the session that the key is for.
*/
@Json(name = "session_id") @Json(name = "session_id")
val sessionId: String? = null, val sessionId: String? = null,
/**
* Required. The key to be exchanged.
*/
@Json(name = "session_key") @Json(name = "session_key")
val sessionKey: String? = null, val sessionKey: String? = null,
/**
* Required. Chain of Curve25519 keys. It starts out empty, but each time the key is forwarded to another device,
* the previous sender in the chain is added to the end of the list. For example, if the key is forwarded
* from A to B to C, this field is empty between A and B, and contains A's Curve25519 key between B and C.
*/
@Json(name = "forwarding_curve25519_key_chain") @Json(name = "forwarding_curve25519_key_chain")
val forwardingCurve25519KeyChain: List<String>? = null, val forwardingCurve25519KeyChain: List<String>? = null,
/**
* Required. The Ed25519 key of the device which initiated the session originally. It is 'claimed' because the
* receiving device has no way to tell that the original room_key actually came from a device which owns the
* private part of this key unless they have done device verification.
*/
@Json(name = "sender_claimed_ed25519_key") @Json(name = "sender_claimed_ed25519_key")
val senderClaimedEd25519Key: String? = null val senderClaimedEd25519Key: String? = null
) )

View file

@ -419,7 +419,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
?: return IntegrityResult.Error(SharedSecretStorageError.UnknownKey(keyId ?: "")) ?: return IntegrityResult.Error(SharedSecretStorageError.UnknownKey(keyId ?: ""))
if (keyInfo.content.algorithm != SSSS_ALGORITHM_AES_HMAC_SHA2 if (keyInfo.content.algorithm != SSSS_ALGORITHM_AES_HMAC_SHA2
|| keyInfo.content.algorithm != SSSS_ALGORITHM_CURVE25519_AES_SHA2) { && keyInfo.content.algorithm != SSSS_ALGORITHM_CURVE25519_AES_SHA2) {
// Unsupported algorithm // Unsupported algorithm
return IntegrityResult.Error( return IntegrityResult.Error(
SharedSecretStorageError.UnsupportedAlgorithm(keyInfo.content.algorithm ?: "") SharedSecretStorageError.UnsupportedAlgorithm(keyInfo.content.algorithm ?: "")

View file

@ -196,7 +196,8 @@ internal interface IMXCryptoStore {
*/ */
fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?) fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?)
fun storeUserCrossSigningKeys(userId: String, masterKey: CryptoCrossSigningKey?, fun storeUserCrossSigningKeys(userId: String,
masterKey: CryptoCrossSigningKey?,
selfSigningKey: CryptoCrossSigningKey?, selfSigningKey: CryptoCrossSigningKey?,
userSigningKey: CryptoCrossSigningKey?) userSigningKey: CryptoCrossSigningKey?)
@ -262,7 +263,7 @@ internal interface IMXCryptoStore {
* @param deviceKey the public key of the other device. * @param deviceKey the public key of the other device.
* @return The Base64 end-to-end session, or null if not found * @return The Base64 end-to-end session, or null if not found
*/ */
fun getDeviceSession(sessionId: String?, deviceKey: String?): OlmSessionWrapper? fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper?
/** /**
* Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist * Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist

View file

@ -555,11 +555,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
} }
override fun getDeviceSession(sessionId: String?, deviceKey: String?): OlmSessionWrapper? { override fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
if (sessionId == null || deviceKey == null) {
return null
}
val key = OlmSessionEntity.createPrimaryKey(sessionId, deviceKey) val key = OlmSessionEntity.createPrimaryKey(sessionId, deviceKey)
// If not in cache (or not found), try to read it from realm // If not in cache (or not found), try to read it from realm

View file

@ -51,7 +51,7 @@ internal class UserAgentHolder @Inject constructor(private val context: Context,
appName = pm.getApplicationLabel(appInfo).toString() appName = pm.getApplicationLabel(appInfo).toString()
val pkgInfo = pm.getPackageInfo(context.applicationContext.packageName, 0) val pkgInfo = pm.getPackageInfo(context.applicationContext.packageName, 0)
appVersion = pkgInfo.versionName appVersion = pkgInfo.versionName ?: ""
// Use appPackageName instead of appName if appName contains any non-ASCII character // Use appPackageName instead of appName if appName contains any non-ASCII character
if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) { if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) {

View file

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.session.account package im.vector.matrix.android.internal.session.account
import im.vector.matrix.android.api.session.account.model.ChangePasswordParams
import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call import retrofit2.Call
import retrofit2.http.Body import retrofit2.http.Body
@ -30,4 +29,12 @@ internal interface AccountAPI {
*/ */
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password")
fun changePassword(@Body params: ChangePasswordParams): Call<Unit> fun changePassword(@Body params: ChangePasswordParams): Call<Unit>
/**
* Deactivate the user account
*
* @param params the deactivate account params
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/deactivate")
fun deactivate(@Body params: DeactivateAccountParams): Call<Unit>
} }

View file

@ -39,6 +39,9 @@ internal abstract class AccountModule {
@Binds @Binds
abstract fun bindChangePasswordTask(task: DefaultChangePasswordTask): ChangePasswordTask abstract fun bindChangePasswordTask(task: DefaultChangePasswordTask): ChangePasswordTask
@Binds
abstract fun bindDeactivateAccountTask(task: DefaultDeactivateAccountTask): DeactivateAccountTask
@Binds @Binds
abstract fun bindAccountService(service: DefaultAccountService): AccountService abstract fun bindAccountService(service: DefaultAccountService): AccountService
} }

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.matrix.android.api.session.account.model package im.vector.matrix.android.internal.session.account
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -17,7 +17,6 @@
package im.vector.matrix.android.internal.session.account package im.vector.matrix.android.internal.session.account
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.account.model.ChangePasswordParams
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.session.account
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
@JsonClass(generateAdapter = true)
internal data class DeactivateAccountParams(
@Json(name = "auth")
val auth: UserPasswordAuth? = null,
// Set to true to erase all data of the account
@Json(name = "erase")
val erase: Boolean
) {
companion object {
fun create(userId: String, password: String, erase: Boolean): DeactivateAccountParams {
return DeactivateAccountParams(
auth = UserPasswordAuth(user = userId, password = password),
erase = erase
)
}
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.session.account
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.cleanup.CleanupSession
import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface DeactivateAccountTask : Task<DeactivateAccountTask.Params, Unit> {
data class Params(
val password: String,
val eraseAllData: Boolean
)
}
internal class DefaultDeactivateAccountTask @Inject constructor(
private val accountAPI: AccountAPI,
private val eventBus: EventBus,
@UserId private val userId: String,
private val cleanupSession: CleanupSession
) : DeactivateAccountTask {
override suspend fun execute(params: DeactivateAccountTask.Params) {
val deactivateAccountParams = DeactivateAccountParams.create(userId, params.password, params.eraseAllData)
executeRequest<Unit>(eventBus) {
apiCall = accountAPI.deactivate(deactivateAccountParams)
}
cleanupSession.handle()
}
}

View file

@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.task.configureWith
import javax.inject.Inject import javax.inject.Inject
internal class DefaultAccountService @Inject constructor(private val changePasswordTask: ChangePasswordTask, internal class DefaultAccountService @Inject constructor(private val changePasswordTask: ChangePasswordTask,
private val deactivateAccountTask: DeactivateAccountTask,
private val taskExecutor: TaskExecutor) : AccountService { private val taskExecutor: TaskExecutor) : AccountService {
override fun changePassword(password: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable { override fun changePassword(password: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable {
@ -33,4 +34,12 @@ internal class DefaultAccountService @Inject constructor(private val changePassw
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun deactivateAccount(password: String, eraseAllData: Boolean, callback: MatrixCallback<Unit>): Cancelable {
return deactivateAccountTask
.configureWith(DeactivateAccountTask.Params(password, eraseAllData)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
} }

View file

@ -0,0 +1,87 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.session.cleanup
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.CryptoModule
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.di.SessionCacheDirectory
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.SessionFilesDirectory
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.di.WorkManagerProvider
import im.vector.matrix.android.internal.session.SessionModule
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import io.realm.Realm
import io.realm.RealmConfiguration
import timber.log.Timber
import java.io.File
import javax.inject.Inject
internal class CleanupSession @Inject constructor(
private val workManagerProvider: WorkManagerProvider,
@SessionId private val sessionId: String,
private val sessionManager: SessionManager,
private val sessionParamsStore: SessionParamsStore,
@SessionDatabase private val clearSessionDataTask: ClearCacheTask,
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
@SessionFilesDirectory private val sessionFiles: File,
@SessionCacheDirectory private val sessionCache: File,
private val realmKeysUtils: RealmKeysUtils,
@SessionDatabase private val realmSessionConfiguration: RealmConfiguration,
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
@UserMd5 private val userMd5: String
) {
suspend fun handle() {
Timber.d("Cleanup: release session...")
sessionManager.releaseSession(sessionId)
Timber.d("Cleanup: cancel pending works...")
workManagerProvider.cancelAllWorks()
Timber.d("Cleanup: delete session params...")
sessionParamsStore.delete(sessionId)
Timber.d("Cleanup: clear session data...")
clearSessionDataTask.execute(Unit)
Timber.d("Cleanup: clear crypto data...")
clearCryptoDataTask.execute(Unit)
Timber.d("Cleanup: clear file system")
sessionFiles.deleteRecursively()
sessionCache.deleteRecursively()
Timber.d("Cleanup: clear the database keys")
realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5))
realmKeysUtils.clear(CryptoModule.getKeyAlias(userMd5))
// Sanity check
if (BuildConfig.DEBUG) {
Realm.getGlobalInstanceCount(realmSessionConfiguration)
.takeIf { it > 0 }
?.let { Timber.e("All realm instance for session has not been closed ($it)") }
Realm.getGlobalInstanceCount(realmCryptoConfiguration)
.takeIf { it > 0 }
?.let { Timber.e("All realm instance for crypto has not been closed ($it)") }
}
}
}

View file

@ -16,58 +16,31 @@
package im.vector.matrix.android.internal.session.signout package im.vector.matrix.android.internal.session.signout
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.CryptoModule
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.di.SessionCacheDirectory
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.SessionFilesDirectory
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.di.WorkManagerProvider
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.SessionModule import im.vector.matrix.android.internal.session.cleanup.CleanupSession
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import io.realm.Realm
import io.realm.RealmConfiguration
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import timber.log.Timber import timber.log.Timber
import java.io.File
import java.net.HttpURLConnection import java.net.HttpURLConnection
import javax.inject.Inject import javax.inject.Inject
internal interface SignOutTask : Task<SignOutTask.Params, Unit> { internal interface SignOutTask : Task<SignOutTask.Params, Unit> {
data class Params( data class Params(
val sigOutFromHomeserver: Boolean val signOutFromHomeserver: Boolean
) )
} }
internal class DefaultSignOutTask @Inject constructor( internal class DefaultSignOutTask @Inject constructor(
private val workManagerProvider: WorkManagerProvider,
@SessionId private val sessionId: String,
private val signOutAPI: SignOutAPI, private val signOutAPI: SignOutAPI,
private val sessionManager: SessionManager, private val eventBus: EventBus,
private val sessionParamsStore: SessionParamsStore, private val cleanupSession: CleanupSession
@SessionDatabase private val clearSessionDataTask: ClearCacheTask,
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
@SessionFilesDirectory private val sessionFiles: File,
@SessionCacheDirectory private val sessionCache: File,
private val realmKeysUtils: RealmKeysUtils,
@SessionDatabase private val realmSessionConfiguration: RealmConfiguration,
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
@UserMd5 private val userMd5: String,
private val eventBus: EventBus
) : SignOutTask { ) : SignOutTask {
override suspend fun execute(params: SignOutTask.Params) { override suspend fun execute(params: SignOutTask.Params) {
// It should be done even after a soft logout, to be sure the deviceId is deleted on the // It should be done even after a soft logout, to be sure the deviceId is deleted on the
if (params.sigOutFromHomeserver) { if (params.signOutFromHomeserver) {
Timber.d("SignOut: send request...") Timber.d("SignOut: send request...")
try { try {
executeRequest<Unit>(eventBus) { executeRequest<Unit>(eventBus) {
@ -87,37 +60,7 @@ internal class DefaultSignOutTask @Inject constructor(
} }
} }
Timber.d("SignOut: release session...") Timber.d("SignOut: cleanup session...")
sessionManager.releaseSession(sessionId) cleanupSession.handle()
Timber.d("SignOut: cancel pending works...")
workManagerProvider.cancelAllWorks()
Timber.d("SignOut: delete session params...")
sessionParamsStore.delete(sessionId)
Timber.d("SignOut: clear session data...")
clearSessionDataTask.execute(Unit)
Timber.d("SignOut: clear crypto data...")
clearCryptoDataTask.execute(Unit)
Timber.d("SignOut: clear file system")
sessionFiles.deleteRecursively()
sessionCache.deleteRecursively()
Timber.d("SignOut: clear the database keys")
realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5))
realmKeysUtils.clear(CryptoModule.getKeyAlias(userMd5))
// Sanity check
if (BuildConfig.DEBUG) {
Realm.getGlobalInstanceCount(realmSessionConfiguration)
.takeIf { it > 0 }
?.let { Timber.e("All realm instance for session has not been closed ($it)") }
Realm.getGlobalInstanceCount(realmCryptoConfiguration)
.takeIf { it > 0 }
?.let { Timber.e("All realm instance for crypto has not been closed ($it)") }
}
} }
} }

View file

@ -39,10 +39,10 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
toDevice.events?.forEachIndexed { index, event -> toDevice.events?.forEachIndexed { index, event ->
initialSyncProgressService?.reportProgress(((index / total.toFloat()) * 100).toInt()) initialSyncProgressService?.reportProgress(((index / total.toFloat()) * 100).toInt())
// Decrypt event if necessary // Decrypt event if necessary
decryptEvent(event, null) decryptToDeviceEvent(event, null)
if (event.getClearType() == EventType.MESSAGE if (event.getClearType() == EventType.MESSAGE
&& event.getClearContent()?.toModel<MessageContent>()?.msgType == "m.bad.encrypted") { && event.getClearContent()?.toModel<MessageContent>()?.msgType == "m.bad.encrypted") {
Timber.e("## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") Timber.e("## CRYPTO | handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}")
} else { } else {
verificationService.onToDeviceEvent(event) verificationService.onToDeviceEvent(event)
cryptoService.onToDeviceEvent(event) cryptoService.onToDeviceEvent(event)
@ -61,28 +61,24 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
* @param timelineId the timeline identifier * @param timelineId the timeline identifier
* @return true if the event has been decrypted * @return true if the event has been decrypted
*/ */
private fun decryptEvent(event: Event, timelineId: String?): Boolean { private fun decryptToDeviceEvent(event: Event, timelineId: String?): Boolean {
Timber.v("## CRYPTO | decryptToDeviceEvent")
if (event.getClearType() == EventType.ENCRYPTED) { if (event.getClearType() == EventType.ENCRYPTED) {
var result: MXEventDecryptionResult? = null var result: MXEventDecryptionResult? = null
try { try {
result = cryptoService.decryptEvent(event, timelineId ?: "") result = cryptoService.decryptEvent(event, timelineId ?: "")
} catch (exception: MXCryptoError) { } catch (exception: MXCryptoError) {
event.mCryptoError = (exception as? MXCryptoError.Base)?.errorType // setCryptoError(exception.cryptoError) event.mCryptoError = (exception as? MXCryptoError.Base)?.errorType // setCryptoError(exception.cryptoError)
Timber.e("## CRYPTO | Failed to decrypt to device event: ${event.mCryptoError ?: exception}")
} }
if (null != result) { if (null != result) {
// event.mxDecryptionResult = MXDecryptionResult(
// payload = result.clearEvent,
// keysClaimed = map
// )
// TODO persist that?
event.mxDecryptionResult = OlmDecryptionResult( event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent, payload = result.clearEvent,
senderKey = result.senderCurve25519Key, senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
) )
// event.setClearData(result)
return true return true
} }
} }

View file

@ -81,6 +81,7 @@ import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFra
import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment
import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment
import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment
import im.vector.riotx.features.settings.account.deactivation.DeactivateAccountFragment
import im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment import im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
import im.vector.riotx.features.settings.devtools.AccountDataFragment import im.vector.riotx.features.settings.devtools.AccountDataFragment
@ -445,8 +446,14 @@ interface FragmentModule {
@IntoMap @IntoMap
@FragmentKey(BootstrapAccountPasswordFragment::class) @FragmentKey(BootstrapAccountPasswordFragment::class)
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(BootstrapMigrateBackupFragment::class) @FragmentKey(BootstrapMigrateBackupFragment::class)
fun bindBootstrapMigrateBackupFragment(fragment: BootstrapMigrateBackupFragment): Fragment fun bindBootstrapMigrateBackupFragment(fragment: BootstrapMigrateBackupFragment): Fragment
@Binds
@IntoMap
@FragmentKey(DeactivateAccountFragment::class)
fun bindDeactivateAccountFragment(fragment: DeactivateAccountFragment): Fragment
} }

View file

@ -49,6 +49,7 @@ data class MainActivityArgs(
val clearCache: Boolean = false, val clearCache: Boolean = false,
val clearCredentials: Boolean = false, val clearCredentials: Boolean = false,
val isUserLoggedOut: Boolean = false, val isUserLoggedOut: Boolean = false,
val isAccountDeactivated: Boolean = false,
val isSoftLogout: Boolean = false val isSoftLogout: Boolean = false
) : Parcelable ) : Parcelable
@ -110,6 +111,7 @@ class MainActivity : VectorBaseActivity() {
clearCache = argsFromIntent?.clearCache ?: false, clearCache = argsFromIntent?.clearCache ?: false,
clearCredentials = argsFromIntent?.clearCredentials ?: false, clearCredentials = argsFromIntent?.clearCredentials ?: false,
isUserLoggedOut = argsFromIntent?.isUserLoggedOut ?: false, isUserLoggedOut = argsFromIntent?.isUserLoggedOut ?: false,
isAccountDeactivated = argsFromIntent?.isAccountDeactivated ?: false,
isSoftLogout = argsFromIntent?.isSoftLogout ?: false isSoftLogout = argsFromIntent?.isSoftLogout ?: false
) )
} }
@ -121,7 +123,14 @@ class MainActivity : VectorBaseActivity() {
return return
} }
when { when {
args.clearCredentials -> session.signOut( args.isAccountDeactivated -> {
// Just do the local cleanup
Timber.w("Account deactivated, start app")
sessionHolder.clearActiveSession()
doLocalCleanup()
startNextActivityAndFinish()
}
args.clearCredentials -> session.signOut(
!args.isUserLoggedOut, !args.isUserLoggedOut,
object : MatrixCallback<Unit> { object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
@ -135,7 +144,7 @@ class MainActivity : VectorBaseActivity() {
displayError(failure) displayError(failure)
} }
}) })
args.clearCache -> session.clearCache( args.clearCache -> session.clearCache(
object : MatrixCallback<Unit> { object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
doLocalCleanup() doLocalCleanup()
@ -182,16 +191,16 @@ class MainActivity : VectorBaseActivity() {
private fun startNextActivityAndFinish() { private fun startNextActivityAndFinish() {
val intent = when { val intent = when {
args.clearCredentials args.clearCredentials
&& !args.isUserLoggedOut -> && (!args.isUserLoggedOut || args.isAccountDeactivated) ->
// User has explicitly asked to log out // User has explicitly asked to log out or deactivated his account
LoginActivity.newIntent(this, null) LoginActivity.newIntent(this, null)
args.isSoftLogout -> args.isSoftLogout ->
// The homeserver has invalidated the token, with a soft logout // The homeserver has invalidated the token, with a soft logout
SoftLogoutActivity.newIntent(this) SoftLogoutActivity.newIntent(this)
args.isUserLoggedOut -> args.isUserLoggedOut ->
// the homeserver has invalidated the token (password changed, device deleted, other security reasons) // the homeserver has invalidated the token (password changed, device deleted, other security reasons)
SignedOutActivity.newIntent(this) SignedOutActivity.newIntent(this)
sessionHolder.hasActiveSession() -> sessionHolder.hasActiveSession() ->
// We have a session. // We have a session.
// Check it can be opened // Check it can be opened
if (sessionHolder.getActiveSession().isOpenable) { if (sessionHolder.getActiveSession().isOpenable) {
@ -200,7 +209,7 @@ class MainActivity : VectorBaseActivity() {
// The token is still invalid // The token is still invalid
SoftLogoutActivity.newIntent(this) SoftLogoutActivity.newIntent(this)
} }
else -> else ->
// First start, or no active session // First start, or no active session
LoginActivity.newIntent(this, null) LoginActivity.newIntent(this, null)
} }

View file

@ -44,6 +44,7 @@ enum class Command(val command: String, val parameters: String, @StringRes val d
POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll), POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll),
SHRUG("/shrug", "<message>", R.string.command_description_shrug), SHRUG("/shrug", "<message>", R.string.command_description_shrug),
PLAIN("/plain", "<message>", R.string.command_description_plain), PLAIN("/plain", "<message>", R.string.command_description_plain),
DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session),
// TODO temporary command // TODO temporary command
VERIFY_USER("/verify", "<user-id>", R.string.command_description_verify); VERIFY_USER("/verify", "<user-id>", R.string.command_description_verify);

View file

@ -281,6 +281,9 @@ object CommandParser {
ParsedCommand.ErrorSyntax(Command.POLL) ParsedCommand.ErrorSyntax(Command.POLL)
} }
} }
Command.DISCARD_SESSION.command -> {
ParsedCommand.DiscardSession
}
else -> { else -> {
// Unknown command // Unknown command
ParsedCommand.ErrorUnknownSlashCommand(slashCommand) ParsedCommand.ErrorUnknownSlashCommand(slashCommand)

View file

@ -52,4 +52,5 @@ sealed class ParsedCommand {
class SendShrug(val message: CharSequence) : ParsedCommand() class SendShrug(val message: CharSequence) : ParsedCommand()
class VerifyUser(val userId: String) : ParsedCommand() class VerifyUser(val userId: String) : ParsedCommand()
class SendPoll(val question: String, val options: List<String>) : ParsedCommand() class SendPoll(val question: String, val options: List<String>) : ParsedCommand()
object DiscardSession: ParsedCommand()
} }

View file

@ -28,6 +28,7 @@ import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.viewModel
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
@ -37,6 +38,7 @@ import im.vector.riotx.core.extensions.addFragmentToBackstack
import im.vector.riotx.core.platform.SimpleFragmentActivity import im.vector.riotx.core.platform.SimpleFragmentActivity
import im.vector.riotx.core.platform.WaitingViewData import im.vector.riotx.core.platform.WaitingViewData
import kotlinx.android.synthetic.main.activity.* import kotlinx.android.synthetic.main.activity.*
import java.net.HttpURLConnection
import javax.inject.Inject import javax.inject.Inject
class CreateDirectRoomActivity : SimpleFragmentActivity() { class CreateDirectRoomActivity : SimpleFragmentActivity() {
@ -91,8 +93,14 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
if (error is CreateRoomFailure.CreatedWithTimeout) { if (error is CreateRoomFailure.CreatedWithTimeout) {
finish() finish()
} else { } else {
val message = if (error is Failure.ServerError && error.httpCode == HttpURLConnection.HTTP_INTERNAL_ERROR /*500*/) {
// This error happen if the invited userId does not exist.
getString(R.string.create_room_dm_failure)
} else {
errorFormatter.toHumanReadable(error)
}
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setMessage(errorFormatter.toHumanReadable(error)) .setMessage(message)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} }

View file

@ -23,6 +23,7 @@ import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
@ -56,15 +57,29 @@ class DirectoryUsersController @Inject constructor(private val session: Session,
override fun buildModels() { override fun buildModels() {
val currentState = state ?: return val currentState = state ?: return
val hasSearch = currentState.directorySearchTerm.isNotBlank() val hasSearch = currentState.directorySearchTerm.isNotBlank()
val asyncUsers = currentState.directoryUsers when (val asyncUsers = currentState.directoryUsers) {
when (asyncUsers) {
is Uninitialized -> renderEmptyState(false) is Uninitialized -> renderEmptyState(false)
is Loading -> renderLoading() is Loading -> renderLoading()
is Success -> renderSuccess(asyncUsers(), currentState.selectedUsers.map { it.userId }, hasSearch) is Success -> renderSuccess(
computeUsersList(asyncUsers(), currentState.directorySearchTerm),
currentState.selectedUsers.map { it.userId },
hasSearch
)
is Fail -> renderFailure(asyncUsers.error) is Fail -> renderFailure(asyncUsers.error)
} }
} }
/**
* Eventually add the searched terms, if it is a userId, and if not already present in the result
*/
private fun computeUsersList(directoryUsers: List<User>, searchTerms: String): List<User> {
return directoryUsers +
searchTerms
.takeIf { terms -> MatrixPatterns.isUserId(terms) && !directoryUsers.any { it.userId == terms } }
?.let { listOf(User(it)) }
.orEmpty()
}
private fun renderLoading() { private fun renderLoading() {
loadingItem { loadingItem {
id("loading") id("loading")

View file

@ -44,6 +44,7 @@ import im.vector.matrix.android.api.session.crypto.verification.VerificationTran
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.securestorage.IntegrityResult
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64 import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
@ -73,7 +74,8 @@ data class VerificationBottomSheetViewState(
val isMe: Boolean = false, val isMe: Boolean = false,
val currentDeviceCanCrossSign: Boolean = false, val currentDeviceCanCrossSign: Boolean = false,
val userWantsToCancel: Boolean = false, val userWantsToCancel: Boolean = false,
val userThinkItsNotHim: Boolean = false val userThinkItsNotHim: Boolean = false,
val quadSContainsSecrets: Boolean = true
) : MvRxState ) : MvRxState
class VerificationBottomSheetViewModel @AssistedInject constructor( class VerificationBottomSheetViewModel @AssistedInject constructor(
@ -116,6 +118,10 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
session.cryptoService().verificationService().getExistingTransaction(args.otherUserId, it) as? QrCodeVerificationTransaction session.cryptoService().verificationService().getExistingTransaction(args.otherUserId, it) as? QrCodeVerificationTransaction
} }
val ssssOk = session.sharedSecretStorageService.checkShouldBeAbleToAccessSecrets(
listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME),
null // default key
) is IntegrityResult.Success
setState { setState {
copy( copy(
otherUserMxItem = userItem?.toMatrixItem(), otherUserMxItem = userItem?.toMatrixItem(),
@ -126,7 +132,8 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
selfVerificationMode = selfVerificationMode, selfVerificationMode = selfVerificationMode,
roomId = args.roomId, roomId = args.roomId,
isMe = args.otherUserId == session.myUserId, isMe = args.otherUserId == session.myUserId,
currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign() currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign(),
quadSContainsSecrets = ssssOk
) )
} }

View file

@ -65,14 +65,16 @@ class VerificationRequestController @Inject constructor(
title(stringProvider.getString(R.string.verification_request_waiting, matrixItem.getBestName())) title(stringProvider.getString(R.string.verification_request_waiting, matrixItem.getBestName()))
} }
bottomSheetVerificationActionItem { if (state.quadSContainsSecrets) {
id("passphrase") bottomSheetVerificationActionItem {
title(stringProvider.getString(R.string.verification_cannot_access_other_session)) id("passphrase")
titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) title(stringProvider.getString(R.string.verification_cannot_access_other_session))
subTitle(stringProvider.getString(R.string.verification_use_passphrase)) titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
iconRes(R.drawable.ic_arrow_right) subTitle(stringProvider.getString(R.string.verification_use_passphrase))
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) iconRes(R.drawable.ic_arrow_right)
listener { listener?.onClickRecoverFromPassphrase() } iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
listener { listener?.onClickRecoverFromPassphrase() }
}
} }
} else { } else {
val styledText = val styledText =

View file

@ -35,6 +35,7 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.ToolbarConfigurable
@ -92,6 +93,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
.subscribe { sharedAction -> .subscribe { sharedAction ->
when (sharedAction) { when (sharedAction) {
is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START) is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)
is HomeActivitySharedAction.CloseDrawer -> drawerLayout.closeDrawer(GravityCompat.START)
is HomeActivitySharedAction.OpenGroup -> { is HomeActivitySharedAction.OpenGroup -> {
drawerLayout.closeDrawer(GravityCompat.START) drawerLayout.closeDrawer(GravityCompat.START)
replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java) replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java)
@ -99,7 +101,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
is HomeActivitySharedAction.PromptForSecurityBootstrap -> { is HomeActivitySharedAction.PromptForSecurityBootstrap -> {
BootstrapBottomSheet.show(supportFragmentManager, true) BootstrapBottomSheet.show(supportFragmentManager, true)
} }
} }.exhaustive
} }
.disposeOnDestroy() .disposeOnDestroy()

View file

@ -23,6 +23,7 @@ import im.vector.riotx.core.platform.VectorSharedAction
*/ */
sealed class HomeActivitySharedAction : VectorSharedAction { sealed class HomeActivitySharedAction : VectorSharedAction {
object OpenDrawer : HomeActivitySharedAction() object OpenDrawer : HomeActivitySharedAction()
object CloseDrawer : HomeActivitySharedAction()
object OpenGroup : HomeActivitySharedAction() object OpenGroup : HomeActivitySharedAction()
object PromptForSecurityBootstrap : HomeActivitySharedAction() object PromptForSecurityBootstrap : HomeActivitySharedAction()
} }

View file

@ -33,10 +33,15 @@ class HomeDrawerFragment @Inject constructor(
private val avatarRenderer: AvatarRenderer private val avatarRenderer: AvatarRenderer
) : VectorBaseFragment() { ) : VectorBaseFragment() {
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
override fun getLayoutResId() = R.layout.fragment_home_drawer override fun getLayoutResId() = R.layout.fragment_home_drawer
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java)
if (savedInstanceState == null) { if (savedInstanceState == null) {
replaceChildFragment(R.id.homeDrawerGroupListContainer, GroupListFragment::class.java) replaceChildFragment(R.id.homeDrawerGroupListContainer, GroupListFragment::class.java)
} }
@ -49,11 +54,13 @@ class HomeDrawerFragment @Inject constructor(
} }
} }
homeDrawerHeaderSettingsView.setOnClickListener { homeDrawerHeaderSettingsView.setOnClickListener {
sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer)
navigator.openSettings(requireActivity()) navigator.openSettings(requireActivity())
} }
// Debug menu // Debug menu
homeDrawerHeaderDebugView.setOnClickListener { homeDrawerHeaderDebugView.setOnClickListener {
sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer)
navigator.openDebug(requireActivity()) navigator.openDebug(requireActivity())
} }
} }

View file

@ -447,6 +447,19 @@ class RoomDetailViewModel @AssistedInject constructor(
// TODO // TODO
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented) _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
} }
is ParsedCommand.DiscardSession -> {
if (room.isEncrypted()) {
session.cryptoService().discardOutboundSession(room.roomId)
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
} else {
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
_viewEvents.post(
RoomDetailViewEvents
.ShowMessage(stringProvider.getString(R.string.command_description_discard_session_not_handled))
)
}
}
}.exhaustive }.exhaustive
} }
is SendMode.EDIT -> { is SendMode.EDIT -> {

View file

@ -19,6 +19,7 @@ package im.vector.riotx.features.login
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo
import androidx.autofill.HintConstants import androidx.autofill.HintConstants
import androidx.core.view.isVisible import androidx.core.view.isVisible
import butterknife.OnClick import butterknife.OnClick
@ -40,7 +41,8 @@ import kotlinx.android.synthetic.main.fragment_login.*
import javax.inject.Inject import javax.inject.Inject
/** /**
* In this screen, in signin mode: * In this screen:
* In signin mode:
* - the user is asked for login (or email) and password to sign in to a homeserver. * - the user is asked for login (or email) and password to sign in to a homeserver.
* - He also can reset his password * - He also can reset his password
* In signup mode: * In signup mode:
@ -49,6 +51,7 @@ import javax.inject.Inject
class LoginFragment @Inject constructor() : AbstractLoginFragment() { class LoginFragment @Inject constructor() : AbstractLoginFragment() {
private var passwordShown = false private var passwordShown = false
private var isSignupMode = false
override fun getLayoutResId() = R.layout.fragment_login override fun getLayoutResId() = R.layout.fragment_login
@ -57,6 +60,14 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
setupSubmitButton() setupSubmitButton()
setupPasswordReveal() setupPasswordReveal()
passwordField.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
submit()
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
} }
private fun setupAutoFill(state: LoginViewState) { private fun setupAutoFill(state: LoginViewState) {
@ -82,7 +93,20 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
val login = loginField.text.toString() val login = loginField.text.toString()
val password = passwordField.text.toString() val password = passwordField.text.toString()
loginViewModel.handle(LoginAction.LoginOrRegister(login, password, getString(R.string.login_mobile_device_riotx))) // This can be called by the IME action, so deal with empty cases
var error = 0
if (login.isEmpty()) {
loginFieldTil.error = getString(if (isSignupMode) R.string.error_empty_field_choose_user_name else R.string.error_empty_field_enter_user_name)
error++
}
if (password.isEmpty()) {
passwordFieldTil.error = getString(if (isSignupMode) R.string.error_empty_field_choose_password else R.string.error_empty_field_your_password)
error++
}
if (error == 0) {
loginViewModel.handle(LoginAction.LoginOrRegister(login, password, getString(R.string.login_mobile_device_riotx)))
}
} }
private fun cleanupUi() { private fun cleanupUi() {
@ -190,6 +214,8 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
} }
override fun updateWithState(state: LoginViewState) { override fun updateWithState(state: LoginViewState) {
isSignupMode = state.signMode == SignMode.SignUp
setupUi(state) setupUi(state)
setupAutoFill(state) setupAutoFill(state)
setupButtons(state) setupButtons(state)

View file

@ -191,7 +191,12 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
backgroundHandler.removeCallbacksAndMessages(null) backgroundHandler.removeCallbacksAndMessages(null)
backgroundHandler.postDelayed( backgroundHandler.postDelayed(
{ {
refreshNotificationDrawerBg() try {
refreshNotificationDrawerBg()
} catch (throwable: Throwable) {
// It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer
Timber.w(throwable, "refreshNotificationDrawerBg failure")
}
}, 200) }, 200)
} }

View file

@ -159,7 +159,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
private const val DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY = "DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY" private const val DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY = "DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY"
private const val DID_MIGRATE_TO_NOTIFICATION_REWORK = "DID_MIGRATE_TO_NOTIFICATION_REWORK" private const val DID_MIGRATE_TO_NOTIFICATION_REWORK = "DID_MIGRATE_TO_NOTIFICATION_REWORK"
private const val DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY = "DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY" private const val DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY = "DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY"
const val SETTINGS_DEACTIVATE_ACCOUNT_KEY = "SETTINGS_DEACTIVATE_ACCOUNT_KEY"
private const val SETTINGS_DISPLAY_ALL_EVENTS_KEY = "SETTINGS_DISPLAY_ALL_EVENTS_KEY" private const val SETTINGS_DISPLAY_ALL_EVENTS_KEY = "SETTINGS_DISPLAY_ALL_EVENTS_KEY"
private const val MEDIA_SAVING_3_DAYS = 0 private const val MEDIA_SAVING_3_DAYS = 0

View file

@ -20,6 +20,7 @@ import android.content.Intent
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
@ -43,6 +44,8 @@ class VectorSettingsActivity : VectorBaseActivity(),
private var keyToHighlight: String? = null private var keyToHighlight: String? = null
var ignoreInvalidTokenError = false
@Inject lateinit var session: Session @Inject lateinit var session: Session
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
@ -57,7 +60,7 @@ class VectorSettingsActivity : VectorBaseActivity(),
when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) { when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) {
EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS -> EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS ->
replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG) replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG)
EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY -> EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY ->
replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG) replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG)
else -> else ->
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG) replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
@ -110,6 +113,14 @@ class VectorSettingsActivity : VectorBaseActivity(),
return keyToHighlight return keyToHighlight
} }
override fun handleInvalidToken(globalError: GlobalError.InvalidToken) {
if (ignoreInvalidTokenError) {
Timber.w("Ignoring invalid token global error")
} else {
super.handleInvalidToken(globalError)
}
}
companion object { companion object {
fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java) fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java)
.apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) } .apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) }

View file

@ -234,19 +234,6 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
false false
} }
// Deactivate account section
// deactivate account
findPreference<VectorPreference>(VectorPreferences.SETTINGS_DEACTIVATE_ACCOUNT_KEY)!!
.onPreferenceClickListener = Preference.OnPreferenceClickListener {
activity?.let {
notImplemented()
// TODO startActivity(DeactivateAccountActivity.getIntent(it))
}
false
}
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {

View file

@ -0,0 +1,119 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.riotx.features.settings.account.deactivation
import android.content.Context
import android.os.Bundle
import android.view.View
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.riotx.R
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.showPassword
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.MainActivityArgs
import im.vector.riotx.features.settings.VectorSettingsActivity
import kotlinx.android.synthetic.main.fragment_deactivate_account.*
import javax.inject.Inject
class DeactivateAccountFragment @Inject constructor(
val viewModelFactory: DeactivateAccountViewModel.Factory
) : VectorBaseFragment() {
private val viewModel: DeactivateAccountViewModel by fragmentViewModel()
override fun getLayoutResId() = R.layout.fragment_deactivate_account
override fun onResume() {
super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.deactivate_account_title)
}
private var settingsActivity: VectorSettingsActivity? = null
override fun onAttach(context: Context) {
super.onAttach(context)
settingsActivity = context as? VectorSettingsActivity
}
override fun onDetach() {
super.onDetach()
settingsActivity = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupUi()
setupViewListeners()
observeViewEvents()
}
private fun setupUi() {
deactivateAccountPassword.textChanges()
.subscribe {
deactivateAccountPasswordTil.error = null
deactivateAccountSubmit.isEnabled = it.isNotEmpty()
}
.disposeOnDestroyView()
}
private fun setupViewListeners() {
deactivateAccountPasswordReveal.setOnClickListener {
viewModel.handle(DeactivateAccountAction.TogglePassword)
}
deactivateAccountSubmit.setOnClickListener {
viewModel.handle(DeactivateAccountAction.DeactivateAccount(
deactivateAccountPassword.text.toString(),
deactivateAccountEraseCheckbox.isChecked))
}
}
private fun observeViewEvents() {
viewModel.observeViewEvents {
when (it) {
is DeactivateAccountViewEvents.Loading -> {
settingsActivity?.ignoreInvalidTokenError = true
showLoadingDialog(it.message)
}
DeactivateAccountViewEvents.EmptyPassword -> {
settingsActivity?.ignoreInvalidTokenError = false
deactivateAccountPasswordTil.error = getString(R.string.error_empty_field_your_password)
}
DeactivateAccountViewEvents.InvalidPassword -> {
settingsActivity?.ignoreInvalidTokenError = false
deactivateAccountPasswordTil.error = getString(R.string.settings_fail_to_update_password_invalid_current_password)
}
is DeactivateAccountViewEvents.OtherFailure -> {
settingsActivity?.ignoreInvalidTokenError = false
displayErrorDialog(it.throwable)
}
DeactivateAccountViewEvents.Done ->
MainActivity.restartApp(activity!!, MainActivityArgs(clearCredentials = true, isAccountDeactivated = true))
}.exhaustive
}
}
override fun invalidate() = withState(viewModel) { state ->
deactivateAccountPassword.showPassword(state.passwordShown)
deactivateAccountPasswordReveal.setImageResource(if (state.passwordShown) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.riotx.features.settings.account.deactivation
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for deactivate account settings screen
*/
sealed class DeactivateAccountViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : DeactivateAccountViewEvents()
object EmptyPassword : DeactivateAccountViewEvents()
object InvalidPassword : DeactivateAccountViewEvents()
data class OtherFailure(val throwable: Throwable) : DeactivateAccountViewEvents()
object Done : DeactivateAccountViewEvents()
}

View file

@ -0,0 +1,93 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.riotx.features.settings.account.deactivation
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.isInvalidPassword
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction
data class DeactivateAccountViewState(
val passwordShown: Boolean = false
) : MvRxState
sealed class DeactivateAccountAction : VectorViewModelAction {
object TogglePassword : DeactivateAccountAction()
data class DeactivateAccount(val password: String, val eraseAllData: Boolean) : DeactivateAccountAction()
}
class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private val initialState: DeactivateAccountViewState,
private val session: Session)
: VectorViewModel<DeactivateAccountViewState, DeactivateAccountAction, DeactivateAccountViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel
}
override fun handle(action: DeactivateAccountAction) {
when (action) {
DeactivateAccountAction.TogglePassword -> handleTogglePassword()
is DeactivateAccountAction.DeactivateAccount -> handleDeactivateAccount(action)
}.exhaustive
}
private fun handleTogglePassword() = withState {
setState {
copy(passwordShown = !passwordShown)
}
}
private fun handleDeactivateAccount(action: DeactivateAccountAction.DeactivateAccount) {
if (action.password.isEmpty()) {
_viewEvents.post(DeactivateAccountViewEvents.EmptyPassword)
return
}
_viewEvents.post(DeactivateAccountViewEvents.Loading())
session.deactivateAccount(action.password, action.eraseAllData, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_viewEvents.post(DeactivateAccountViewEvents.Done)
}
override fun onFailure(failure: Throwable) {
if (failure.isInvalidPassword()) {
_viewEvents.post(DeactivateAccountViewEvents.InvalidPassword)
} else {
_viewEvents.post(DeactivateAccountViewEvents.OtherFailure(failure))
}
}
})
}
companion object : MvRxViewModelFactory<DeactivateAccountViewModel, DeactivateAccountViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: DeactivateAccountViewState): DeactivateAccountViewModel? {
val fragment: DeactivateAccountFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.viewModelFactory.create(state)
}
}
}

View file

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/deactivateAccountContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/deactivate_account_content"
android:textColor="?riotx_text_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/deactivateAccountEraseCheckbox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/deactivate_account_delete_checkbox"
android:textColor="?riotx_text_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountContent" />
<TextView
android:id="@+id/deactivateAccountPromptPassword"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/deactivate_account_prompt_password"
android:textColor="?riotx_text_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountEraseCheckbox" />
<FrameLayout
android:id="@+id/deactivateAccountPasswordContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/auth_password_placeholder"
android:inputType="textPassword"
android:maxLines="1"
android:nextFocusDown="@+id/login_password"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountPromptPassword">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/deactivateAccountPasswordTil"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_signup_password_hint"
app:errorEnabled="true"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/deactivateAccountPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textPassword"
android:maxLines="1"
android:paddingEnd="48dp"
android:paddingRight="48dp"
tools:ignore="RtlSymmetry" />
</com.google.android.material.textfield.TextInputLayout>
<ImageView
android:id="@+id/deactivateAccountPasswordReveal"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_gravity="end"
android:layout_marginTop="8dp"
android:background="?attr/selectableItemBackground"
android:scaleType="center"
android:src="@drawable/ic_eye_black"
android:tint="?attr/colorAccent"
tools:contentDescription="@string/a11y_show_password" />
</FrameLayout>
<Button
android:id="@+id/deactivateAccountSubmit"
style="@style/VectorButtonStyleDestructive"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="@string/deactivate_account_submit"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountPasswordContainer" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -82,6 +82,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ems="10" android:ems="10"
android:imeOptions="actionDone"
android:inputType="textPassword" android:inputType="textPassword"
android:maxLines="1" android:maxLines="1"
android:paddingEnd="48dp" android:paddingEnd="48dp"
@ -104,19 +105,18 @@
</FrameLayout> </FrameLayout>
<RelativeLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="22dp" android:layout_marginTop="22dp">
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/forgetPasswordButton" android:id="@+id/forgetPasswordButton"
style="@style/Style.Vector.Login.Button.Text" style="@style/Style.Vector.Login.Button.Text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start" android:text="@string/auth_forgot_password"
android:text="@string/auth_forgot_password" /> app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/loginSubmit" android:id="@+id/loginSubmit"
@ -124,12 +124,12 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_gravity="end"
android:text="@string/auth_login" android:text="@string/auth_login"
app:layout_constraintEnd_toEndOf="parent"
tools:enabled="false" tools:enabled="false"
tools:ignore="RelativeOverlap" /> tools:ignore="RelativeOverlap" />
</RelativeLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout> </LinearLayout>

View file

@ -6,12 +6,14 @@
<!-- Sections has been created to limit merge conflicts. --> <!-- Sections has been created to limit merge conflicts. -->
<!-- BEGIN Strings added by Valere --> <!-- BEGIN Strings added by Valere -->
<string name="command_description_discard_session">Forces the current outbound group session in an encrypted room to be discarded</string>
<string name="command_description_discard_session_not_handled">Only supported in encrypted rooms</string>
<!-- END Strings added by Valere --> <!-- END Strings added by Valere -->
<!-- BEGIN Strings added by Benoit --> <!-- BEGIN Strings added by Benoit -->
<string name="error_empty_field_choose_user_name">Please choose a username.</string>
<string name="error_empty_field_choose_password">Please choose a password.</string>
<!-- END Strings added by Benoit --> <!-- END Strings added by Benoit -->
@ -30,4 +32,5 @@
<!-- END Strings added by Others --> <!-- END Strings added by Others -->
<string name="create_room_dm_failure">"We couldn't create your DM. Please check the users you want to invite and try again."</string>
</resources> </resources>

View file

@ -97,14 +97,13 @@
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceCategory <im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/settings_deactivate_account_section">
android:key="SETTINGS_DEACTIVATE_ACCOUNT_CATEGORY_KEY"
android:title="@string/settings_deactivate_account_section"
app:isPreferenceVisible="@bool/false_not_implemented">
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_DEACTIVATE_ACCOUNT_KEY" android:key="SETTINGS_DEACTIVATE_ACCOUNT_KEY"
android:title="@string/settings_deactivate_my_account" /> android:persistent="false"
android:title="@string/settings_deactivate_my_account"
app:fragment="im.vector.riotx.features.settings.account.deactivation.DeactivateAccountFragment" />
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>