mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 10:25:35 +03:00
Merge branch 'develop' into feature/misleading_url_target
This commit is contained in:
commit
a80181da9e
74 changed files with 1976 additions and 572 deletions
|
@ -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>
|
|
@ -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 🗣:
|
||||||
-
|
-
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 ?: "")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())) {
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)") }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 -> {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
106
vector/src/main/res/layout/fragment_deactivate_account.xml
Normal file
106
vector/src/main/res/layout/fragment_deactivate_account.xml
Normal 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>
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue