mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 11:59:12 +03:00
Merge remote-tracking branch 'origin/develop' into task/eric/replace_flatten_with_direct_parent
This commit is contained in:
commit
67dd250f83
180 changed files with 3913 additions and 1103 deletions
11
CHANGES.md
11
CHANGES.md
|
@ -1,3 +1,14 @@
|
|||
Changes in Element v1.4.27 (2022-07-06)
|
||||
=======================================
|
||||
|
||||
Bugfixes 🐛
|
||||
----------
|
||||
- Fixes crash when sharing plain text, such as a url ([#6451](https://github.com/vector-im/element-android/issues/6451))
|
||||
- Fix crashes on Timeline [Thread] due to range validation ([#6461](https://github.com/vector-im/element-android/issues/6461))
|
||||
- Fix crashes when opening Thread ([#6463](https://github.com/vector-im/element-android/issues/6463))
|
||||
- Fix ConcurrentModificationException on BackgroundDetectionObserver ([#6469](https://github.com/vector-im/element-android/issues/6469))
|
||||
|
||||
|
||||
Changes in Element v1.4.26 (2022-06-30)
|
||||
=======================================
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ buildscript {
|
|||
classpath libs.gradle.gradlePlugin
|
||||
classpath libs.gradle.kotlinPlugin
|
||||
classpath libs.gradle.hiltPlugin
|
||||
classpath 'com.google.gms:google-services:4.3.10'
|
||||
classpath 'com.google.gms:google-services:4.3.13'
|
||||
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.4.0.2513'
|
||||
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5'
|
||||
classpath "com.likethesalad.android:stem-plugin:2.1.1"
|
||||
|
|
1
changelog.d/5398.bugfix
Normal file
1
changelog.d/5398.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Adds LoginType to SessionParams to fix soft logout form not showing for SSO and Password type
|
1
changelog.d/5853.feature
Normal file
1
changelog.d/5853.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Improve user experience when he is first invited to a room. Users will be able to decrypt and view previous messages
|
1
changelog.d/6423.misc
Normal file
1
changelog.d/6423.misc
Normal file
|
@ -0,0 +1 @@
|
|||
[Poll] - Add a description under undisclosed poll when not ended
|
1
changelog.d/6430.bugfix
Normal file
1
changelog.d/6430.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
[Poll] Fixes visible and wrong votes in closed poll after removing 2 previous polls
|
1
changelog.d/6434.misc
Normal file
1
changelog.d/6434.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Add code check to prevent modification of frozen class
|
1
changelog.d/6436.misc
Normal file
1
changelog.d/6436.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Let your Activity or Fragment implement `VectorMenuProvider` if they provide a menu.
|
1
changelog.d/6442.bugfix
Normal file
1
changelog.d/6442.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix HTML entities being displayed in messages
|
1
changelog.d/6450.bugfix
Normal file
1
changelog.d/6450.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Gallery picker can pick external images
|
1
changelog.d/6451.bugfix
Normal file
1
changelog.d/6451.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fixes crash when sharing plain text, such as a url
|
1
changelog.d/6458.misc
Normal file
1
changelog.d/6458.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Rename Android Service to use `AndroidService` suffix
|
1
changelog.d/6461.bugfix
Normal file
1
changelog.d/6461.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix crashes on Timeline [Thread] due to range validation
|
1
changelog.d/6463.bugfix
Normal file
1
changelog.d/6463.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix crashes when opening Thread
|
1
changelog.d/6469.bugfix
Normal file
1
changelog.d/6469.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix ConcurrentModificationException on BackgroundDetectionObserver
|
|
@ -7,6 +7,7 @@ def excludes = [
|
|||
'**/*Activity*',
|
||||
'**/*Fragment*',
|
||||
'**/*Application*',
|
||||
'**/*AndroidService*',
|
||||
|
||||
// We would like to exclude android widgets as well but our naming is inconsistent
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ def markwon = "4.6.2"
|
|||
def moshi = "1.13.0"
|
||||
def lifecycle = "2.4.1"
|
||||
def flowBinding = "1.2.0"
|
||||
def flipper = "0.152.0"
|
||||
def flipper = "0.153.0"
|
||||
def epoxy = "4.6.2"
|
||||
def mavericks = "2.7.0"
|
||||
def glide = "4.13.2"
|
||||
|
@ -29,7 +29,7 @@ def bigImageViewer = "1.8.1"
|
|||
def jjwt = "0.11.5"
|
||||
def vanniktechEmoji = "0.15.0"
|
||||
|
||||
def fragment = "1.4.1"
|
||||
def fragment = "1.5.0"
|
||||
|
||||
// Testing
|
||||
def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
|
||||
|
@ -50,7 +50,7 @@ ext.libs = [
|
|||
],
|
||||
androidx : [
|
||||
'annotation' : "androidx.annotation:annotation:1.4.0",
|
||||
'activity' : "androidx.activity:activity:1.4.0",
|
||||
'activity' : "androidx.activity:activity:1.5.0",
|
||||
'annotations' : "androidx.annotation:annotation:1.3.0",
|
||||
'appCompat' : "androidx.appcompat:appcompat:1.4.2",
|
||||
'biometric' : "androidx.biometric:biometric:1.1.0",
|
||||
|
|
|
@ -191,7 +191,7 @@ Examples of prefixes:
|
|||
- `[Bugfix]`
|
||||
- etc.
|
||||
|
||||
Also, it's still possible to add labels to the PRs, such as `A-` or `T-` labels, even if this is not a string requirement. We prefer to spend time to add labels on issues.
|
||||
Also, it's still possible to add labels to the PRs, such as `A-` or `T-` labels, even if this is not a strong requirement. We prefer to spend time to add labels on issues.
|
||||
|
||||
##### PR description
|
||||
|
||||
|
|
2
fastlane/metadata/android/en-US/changelogs/40104270.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40104270.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Main changes in this version: Various bug fixes and stability improvements.
|
||||
Full changelog: https://github.com/vector-im/element-android/releases
|
|
@ -49,7 +49,7 @@ class MediaPicker : Picker<MultiPickerBaseMediaType>() {
|
|||
return Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
|
||||
type = "video/*, image/*"
|
||||
type = "*/*"
|
||||
val mimeTypes = arrayOf("image/*", "video/*")
|
||||
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ android {
|
|||
|
||||
dependencies {
|
||||
implementation libs.androidx.appCompat
|
||||
implementation libs.androidx.fragmentKtx
|
||||
implementation libs.google.material
|
||||
// Pref theme
|
||||
implementation libs.androidx.preferenceKtx
|
||||
|
|
|
@ -18,8 +18,12 @@ package im.vector.lib.ui.styles.debug
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import im.vector.lib.ui.styles.R
|
||||
|
@ -31,6 +35,7 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setupMenu()
|
||||
val views = ActivityDebugMaterialThemeBinding.inflate(layoutInflater)
|
||||
setContentView(views.root)
|
||||
|
||||
|
@ -72,6 +77,27 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun setupMenu() {
|
||||
addMenuProvider(
|
||||
object : MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.menu_debug, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
Toast.makeText(
|
||||
this@DebugMaterialThemeActivity,
|
||||
"Menu ${menuItem.title} clicked!",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return true
|
||||
}
|
||||
},
|
||||
this,
|
||||
Lifecycle.State.RESUMED
|
||||
)
|
||||
}
|
||||
|
||||
private fun showTestDialog(theme: Int) {
|
||||
MaterialAlertDialogBuilder(this, theme)
|
||||
.setTitle("Dialog title")
|
||||
|
@ -82,9 +108,4 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() {
|
|||
.setNeutralButton("Neutral", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_debug, menu)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,12 +18,15 @@ package org.matrix.android.sdk.common
|
|||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
@ -38,7 +41,10 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationResult
|
|||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.getRoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
|
@ -47,6 +53,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
|||
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.CancellationException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
@ -54,7 +61,7 @@ import java.util.concurrent.TimeUnit
|
|||
* This class exposes methods to be used in common cases
|
||||
* Registration, login, Sync, Sending messages...
|
||||
*/
|
||||
class CommonTestHelper private constructor(context: Context) {
|
||||
class CommonTestHelper internal constructor(context: Context) {
|
||||
|
||||
companion object {
|
||||
internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) {
|
||||
|
@ -241,6 +248,37 @@ class CommonTestHelper private constructor(context: Context) {
|
|||
return sentEvents
|
||||
}
|
||||
|
||||
fun waitForAndAcceptInviteInRoom(otherSession: Session, roomID: String) {
|
||||
waitWithLatch { latch ->
|
||||
retryPeriodicallyWithLatch(latch) {
|
||||
val roomSummary = otherSession.getRoomSummary(roomID)
|
||||
(roomSummary != null && roomSummary.membership == Membership.INVITE).also {
|
||||
if (it) {
|
||||
Log.v("# TEST", "${otherSession.myUserId} can see the invite")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// not sure why it's taking so long :/
|
||||
runBlockingTest(90_000) {
|
||||
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $roomID")
|
||||
try {
|
||||
otherSession.roomService().joinRoom(roomID)
|
||||
} catch (ex: JoinRoomFailure.JoinedWithTimeout) {
|
||||
// it's ok we will wait after
|
||||
}
|
||||
}
|
||||
|
||||
Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
|
||||
waitWithLatch {
|
||||
retryPeriodicallyWithLatch(it) {
|
||||
val roomSummary = otherSession.getRoomSummary(roomID)
|
||||
roomSummary != null && roomSummary.membership == Membership.JOIN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reply in a thread
|
||||
* @param room the room where to send the messages
|
||||
|
@ -285,6 +323,8 @@ class CommonTestHelper private constructor(context: Context) {
|
|||
)
|
||||
assertNotNull(session)
|
||||
return session.also {
|
||||
// most of the test was created pre-MSC3061 so ensure compatibility
|
||||
it.cryptoService().enableShareKeyOnInvite(false)
|
||||
trackedSessions.add(session)
|
||||
}
|
||||
}
|
||||
|
@ -428,16 +468,26 @@ class CommonTestHelper private constructor(context: Context) {
|
|||
* @param latch
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis) {
|
||||
fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis, job: Job? = null) {
|
||||
assertTrue(
|
||||
"Timed out after " + timeout + "ms waiting for something to happen. See stacktrace for cause.",
|
||||
latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)
|
||||
latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS).also {
|
||||
if (!it) {
|
||||
// cancel job on timeout
|
||||
job?.cancel("Await timeout")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
|
||||
while (true) {
|
||||
delay(1000)
|
||||
try {
|
||||
delay(1000)
|
||||
} catch (ex: CancellationException) {
|
||||
// the job was canceled, just stop
|
||||
return
|
||||
}
|
||||
if (condition()) {
|
||||
latch.countDown()
|
||||
return
|
||||
|
@ -447,10 +497,10 @@ class CommonTestHelper private constructor(context: Context) {
|
|||
|
||||
fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, dispatcher: CoroutineDispatcher = Dispatchers.Main, block: suspend (CountDownLatch) -> Unit) {
|
||||
val latch = CountDownLatch(1)
|
||||
coroutineScope.launch(dispatcher) {
|
||||
val job = coroutineScope.launch(dispatcher) {
|
||||
block(latch)
|
||||
}
|
||||
await(latch, timeout)
|
||||
await(latch, timeout, job)
|
||||
}
|
||||
|
||||
fun <T> runBlockingTest(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T {
|
||||
|
|
|
@ -53,6 +53,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
|
|||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
|
@ -76,11 +77,14 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
|
|||
/**
|
||||
* @return alice session
|
||||
*/
|
||||
fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true): CryptoTestData {
|
||||
fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData {
|
||||
val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
|
||||
|
||||
val roomId = testHelper.runBlockingTest {
|
||||
aliceSession.roomService().createRoom(CreateRoomParams().apply { name = "MyRoom" })
|
||||
aliceSession.roomService().createRoom(CreateRoomParams().apply {
|
||||
historyVisibility = roomHistoryVisibility
|
||||
name = "MyRoom"
|
||||
})
|
||||
}
|
||||
if (encryptedRoom) {
|
||||
testHelper.waitWithLatch { latch ->
|
||||
|
@ -104,8 +108,8 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
|
|||
/**
|
||||
* @return alice and bob sessions
|
||||
*/
|
||||
fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true): CryptoTestData {
|
||||
val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom)
|
||||
fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData {
|
||||
val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom, roomHistoryVisibility)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val aliceRoomId = cryptoTestData.roomId
|
||||
|
||||
|
|
|
@ -0,0 +1,298 @@
|
|||
/*
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import android.util.Log
|
||||
import androidx.test.filters.LargeTest
|
||||
import org.amshove.kluent.internal.assertEquals
|
||||
import org.junit.Assert
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
|
||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
|
||||
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import org.matrix.android.sdk.common.CryptoTestData
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
import org.matrix.android.sdk.common.TestConstants
|
||||
import org.matrix.android.sdk.common.TestMatrixCallback
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
@LargeTest
|
||||
class E2EShareKeysConfigTest : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun msc3061ShouldBeDisabledByDefault() = runCryptoTest(context()) { _, commonTestHelper ->
|
||||
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false))
|
||||
Assert.assertFalse("MSC3061 is lab and should be disabled by default", aliceSession.cryptoService().isShareKeysOnInviteEnabled())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ensureKeysAreNotSharedIfOptionDisabled() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
|
||||
aliceSession.cryptoService().enableShareKeyOnInvite(false)
|
||||
val roomId = commonTestHelper.runBlockingTest {
|
||||
aliceSession.roomService().createRoom(CreateRoomParams().apply {
|
||||
historyVisibility = RoomHistoryVisibility.SHARED
|
||||
name = "MyRoom"
|
||||
enableEncryption()
|
||||
})
|
||||
}
|
||||
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true
|
||||
}
|
||||
}
|
||||
val roomAlice = aliceSession.roomService().getRoom(roomId)!!
|
||||
|
||||
// send some messages
|
||||
val withSession1 = commonTestHelper.sendTextMessage(roomAlice, "Hello", 1)
|
||||
aliceSession.cryptoService().discardOutboundSession(roomId)
|
||||
val withSession2 = commonTestHelper.sendTextMessage(roomAlice, "World", 1)
|
||||
|
||||
// Create bob account
|
||||
val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(withInitialSync = true))
|
||||
|
||||
// Let alice invite bob
|
||||
commonTestHelper.runBlockingTest {
|
||||
roomAlice.membershipService().invite(bobSession.myUserId)
|
||||
}
|
||||
|
||||
commonTestHelper.waitForAndAcceptInviteInRoom(bobSession, roomId)
|
||||
|
||||
// Bob has join but should not be able to decrypt history
|
||||
cryptoTestHelper.ensureCannotDecrypt(
|
||||
withSession1.map { it.eventId } + withSession2.map { it.eventId },
|
||||
bobSession,
|
||||
roomId
|
||||
)
|
||||
|
||||
// We don't need bob anymore
|
||||
commonTestHelper.signOutAndClose(bobSession)
|
||||
|
||||
// Now let's enable history key sharing on alice side
|
||||
aliceSession.cryptoService().enableShareKeyOnInvite(true)
|
||||
|
||||
// let's add a new message first
|
||||
val afterFlagOn = commonTestHelper.sendTextMessage(roomAlice, "After", 1)
|
||||
|
||||
// Worth nothing to check that the session was rotated
|
||||
Assert.assertNotEquals(
|
||||
"Session should have been rotated",
|
||||
withSession2.first().root.content?.get("session_id")!!,
|
||||
afterFlagOn.first().root.content?.get("session_id")!!
|
||||
)
|
||||
|
||||
// Invite a new user
|
||||
val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
|
||||
|
||||
// Let alice invite sam
|
||||
commonTestHelper.runBlockingTest {
|
||||
roomAlice.membershipService().invite(samSession.myUserId)
|
||||
}
|
||||
|
||||
commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId)
|
||||
|
||||
// Sam shouldn't be able to decrypt messages with the first session, but should decrypt the one with 3rd session
|
||||
cryptoTestHelper.ensureCannotDecrypt(
|
||||
withSession1.map { it.eventId } + withSession2.map { it.eventId },
|
||||
samSession,
|
||||
roomId
|
||||
)
|
||||
|
||||
cryptoTestHelper.ensureCanDecrypt(
|
||||
afterFlagOn.map { it.eventId },
|
||||
samSession,
|
||||
roomId,
|
||||
afterFlagOn.map { it.root.getClearContent()?.get("body") as String })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ifSharingDisabledOnAliceSideBobShouldNotShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED)
|
||||
val aliceSession = testData.firstSession.also {
|
||||
it.cryptoService().enableShareKeyOnInvite(false)
|
||||
}
|
||||
val bobSession = testData.secondSession!!.also {
|
||||
it.cryptoService().enableShareKeyOnInvite(true)
|
||||
}
|
||||
|
||||
val (fromAliceNotSharable, fromBobSharable, samSession) = commonAliceAndBobSendMessages(commonTestHelper, aliceSession, testData, bobSession)
|
||||
|
||||
// Bob should have shared history keys to sam.
|
||||
// But has alice hasn't enabled sharing, bob shouldn't send her sessions
|
||||
cryptoTestHelper.ensureCannotDecrypt(
|
||||
fromAliceNotSharable.map { it.eventId },
|
||||
samSession,
|
||||
testData.roomId
|
||||
)
|
||||
|
||||
cryptoTestHelper.ensureCanDecrypt(
|
||||
fromBobSharable.map { it.eventId },
|
||||
samSession,
|
||||
testData.roomId,
|
||||
fromBobSharable.map { it.root.getClearContent()?.get("body") as String })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ifSharingEnabledOnAliceSideBobShouldShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED)
|
||||
val aliceSession = testData.firstSession.also {
|
||||
it.cryptoService().enableShareKeyOnInvite(true)
|
||||
}
|
||||
val bobSession = testData.secondSession!!.also {
|
||||
it.cryptoService().enableShareKeyOnInvite(true)
|
||||
}
|
||||
|
||||
val (fromAliceNotSharable, fromBobSharable, samSession) = commonAliceAndBobSendMessages(commonTestHelper, aliceSession, testData, bobSession)
|
||||
|
||||
cryptoTestHelper.ensureCanDecrypt(
|
||||
fromAliceNotSharable.map { it.eventId },
|
||||
samSession,
|
||||
testData.roomId,
|
||||
fromAliceNotSharable.map { it.root.getClearContent()?.get("body") as String })
|
||||
|
||||
cryptoTestHelper.ensureCanDecrypt(
|
||||
fromBobSharable.map { it.eventId },
|
||||
samSession,
|
||||
testData.roomId,
|
||||
fromBobSharable.map { it.root.getClearContent()?.get("body") as String })
|
||||
}
|
||||
|
||||
private fun commonAliceAndBobSendMessages(commonTestHelper: CommonTestHelper, aliceSession: Session, testData: CryptoTestData, bobSession: Session): Triple<List<TimelineEvent>, List<TimelineEvent>, Session> {
|
||||
val fromAliceNotSharable = commonTestHelper.sendTextMessage(aliceSession.getRoom(testData.roomId)!!, "Hello from alice", 1)
|
||||
val fromBobSharable = commonTestHelper.sendTextMessage(bobSession.getRoom(testData.roomId)!!, "Hello from bob", 1)
|
||||
|
||||
// Now let bob invite Sam
|
||||
// Invite a new user
|
||||
val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
|
||||
|
||||
// Let bob invite sam
|
||||
commonTestHelper.runBlockingTest {
|
||||
bobSession.getRoom(testData.roomId)!!.membershipService().invite(samSession.myUserId)
|
||||
}
|
||||
|
||||
commonTestHelper.waitForAndAcceptInviteInRoom(samSession, testData.roomId)
|
||||
return Triple(fromAliceNotSharable, fromBobSharable, samSession)
|
||||
}
|
||||
|
||||
// test flag on backup is correct
|
||||
|
||||
@Test
|
||||
fun testBackupFlagIsCorrect() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
|
||||
val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
|
||||
aliceSession.cryptoService().enableShareKeyOnInvite(false)
|
||||
val roomId = commonTestHelper.runBlockingTest {
|
||||
aliceSession.roomService().createRoom(CreateRoomParams().apply {
|
||||
historyVisibility = RoomHistoryVisibility.SHARED
|
||||
name = "MyRoom"
|
||||
enableEncryption()
|
||||
})
|
||||
}
|
||||
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true
|
||||
}
|
||||
}
|
||||
val roomAlice = aliceSession.roomService().getRoom(roomId)!!
|
||||
|
||||
// send some messages
|
||||
val notSharableMessage = commonTestHelper.sendTextMessage(roomAlice, "Hello", 1)
|
||||
aliceSession.cryptoService().enableShareKeyOnInvite(true)
|
||||
val sharableMessage = commonTestHelper.sendTextMessage(roomAlice, "World", 1)
|
||||
|
||||
Log.v("#E2E TEST", "Create and start key backup for bob ...")
|
||||
val keysBackupService = aliceSession.cryptoService().keysBackupService()
|
||||
val keyBackupPassword = "FooBarBaz"
|
||||
val megolmBackupCreationInfo = commonTestHelper.doSync<MegolmBackupCreationInfo> {
|
||||
keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
|
||||
}
|
||||
val version = commonTestHelper.doSync<KeysVersion> {
|
||||
keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
|
||||
}
|
||||
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
keysBackupService.backupAllGroupSessions(
|
||||
null,
|
||||
TestMatrixCallback(latch, true)
|
||||
)
|
||||
}
|
||||
|
||||
// signout
|
||||
commonTestHelper.signOutAndClose(aliceSession)
|
||||
|
||||
val newAliceSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
|
||||
newAliceSession.cryptoService().enableShareKeyOnInvite(true)
|
||||
|
||||
newAliceSession.cryptoService().keysBackupService().let { kbs ->
|
||||
val keyVersionResult = commonTestHelper.doSync<KeysVersionResult?> {
|
||||
kbs.getVersion(version.version, it)
|
||||
}
|
||||
|
||||
val importedResult = commonTestHelper.doSync<ImportRoomKeysResult> {
|
||||
kbs.restoreKeyBackupWithPassword(
|
||||
keyVersionResult!!,
|
||||
keyBackupPassword,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(2, importedResult.totalNumberOfKeys)
|
||||
}
|
||||
|
||||
// Now let's invite sam
|
||||
// Invite a new user
|
||||
val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
|
||||
|
||||
// Let alice invite sam
|
||||
commonTestHelper.runBlockingTest {
|
||||
newAliceSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId)
|
||||
}
|
||||
|
||||
commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId)
|
||||
|
||||
// Sam shouldn't be able to decrypt messages with the first session, but should decrypt the one with 3rd session
|
||||
cryptoTestHelper.ensureCannotDecrypt(
|
||||
notSharableMessage.map { it.eventId },
|
||||
samSession,
|
||||
roomId
|
||||
)
|
||||
|
||||
cryptoTestHelper.ensureCanDecrypt(
|
||||
sharableMessage.map { it.eventId },
|
||||
samSession,
|
||||
roomId,
|
||||
sharableMessage.map { it.root.getClearContent()?.get("body") as String })
|
||||
}
|
||||
}
|
|
@ -23,7 +23,6 @@ import org.amshove.kluent.fail
|
|||
import org.amshove.kluent.internal.assertEquals
|
||||
import org.junit.Assert
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
@ -49,9 +48,7 @@ import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventCon
|
|||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.getRoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
|
||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
|
@ -67,10 +64,10 @@ import org.matrix.android.sdk.common.TestMatrixCallback
|
|||
import org.matrix.android.sdk.mustFail
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
// @Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.")
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
@LargeTest
|
||||
@Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.")
|
||||
class E2eeSanityTests : InstrumentedTest {
|
||||
|
||||
@get:Rule val rule = RetryTestRule(3)
|
||||
|
@ -115,7 +112,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
|
||||
// All user should accept invite
|
||||
otherAccounts.forEach { otherSession ->
|
||||
waitForAndAcceptInviteInRoom(testHelper, otherSession, e2eRoomID)
|
||||
testHelper.waitForAndAcceptInviteInRoom(otherSession, e2eRoomID)
|
||||
Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
|
||||
}
|
||||
|
||||
|
@ -156,7 +153,7 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
}
|
||||
|
||||
newAccount.forEach {
|
||||
waitForAndAcceptInviteInRoom(testHelper, it, e2eRoomID)
|
||||
testHelper.waitForAndAcceptInviteInRoom(it, e2eRoomID)
|
||||
}
|
||||
|
||||
ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID)
|
||||
|
@ -740,37 +737,6 @@ class E2eeSanityTests : InstrumentedTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun waitForAndAcceptInviteInRoom(testHelper: CommonTestHelper, otherSession: Session, e2eRoomID: String) {
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val roomSummary = otherSession.getRoomSummary(e2eRoomID)
|
||||
(roomSummary != null && roomSummary.membership == Membership.INVITE).also {
|
||||
if (it) {
|
||||
Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// not sure why it's taking so long :/
|
||||
testHelper.runBlockingTest(90_000) {
|
||||
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
|
||||
try {
|
||||
otherSession.roomService().joinRoom(e2eRoomID)
|
||||
} catch (ex: JoinRoomFailure.JoinedWithTimeout) {
|
||||
// it's ok we will wait after
|
||||
}
|
||||
}
|
||||
|
||||
Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
|
||||
testHelper.waitWithLatch {
|
||||
testHelper.retryPeriodicallyWithLatch(it) {
|
||||
val roomSummary = otherSession.getRoomSummary(e2eRoomID)
|
||||
roomSummary != null && roomSummary.membership == Membership.JOIN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List<String>, session: Session, e2eRoomID: String) {
|
||||
testHelper.waitWithLatch { latch ->
|
||||
sentEventIds.forEach { sentEventId ->
|
||||
|
|
|
@ -0,0 +1,424 @@
|
|||
/*
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto
|
||||
|
||||
import android.util.Log
|
||||
import androidx.test.filters.LargeTest
|
||||
import org.amshove.kluent.internal.assertEquals
|
||||
import org.amshove.kluent.internal.assertNotEquals
|
||||
import org.junit.Assert
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
|
||||
import org.matrix.android.sdk.api.session.room.model.shouldShareHistory
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import org.matrix.android.sdk.common.SessionTestParams
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
@LargeTest
|
||||
class E2eeShareKeysHistoryTest : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun testShareMessagesHistoryWithRoomWorldReadable() {
|
||||
testShareHistoryWithRoomVisibility(RoomHistoryVisibility.WORLD_READABLE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testShareMessagesHistoryWithRoomShared() {
|
||||
testShareHistoryWithRoomVisibility(RoomHistoryVisibility.SHARED)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testShareMessagesHistoryWithRoomJoined() {
|
||||
testShareHistoryWithRoomVisibility(RoomHistoryVisibility.JOINED)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testShareMessagesHistoryWithRoomInvited() {
|
||||
testShareHistoryWithRoomVisibility(RoomHistoryVisibility.INVITED)
|
||||
}
|
||||
|
||||
/**
|
||||
* In this test we create a room and test that new members
|
||||
* can decrypt history when the room visibility is
|
||||
* RoomHistoryVisibility.SHARED or RoomHistoryVisibility.WORLD_READABLE.
|
||||
* We should not be able to view messages/decrypt otherwise
|
||||
*/
|
||||
private fun testShareHistoryWithRoomVisibility(roomHistoryVisibility: RoomHistoryVisibility? = null) =
|
||||
runCryptoTest(context()) { cryptoTestHelper, testHelper ->
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, roomHistoryVisibility)
|
||||
|
||||
val e2eRoomID = cryptoTestData.roomId
|
||||
|
||||
// Alice
|
||||
val aliceSession = cryptoTestData.firstSession.also {
|
||||
it.cryptoService().enableShareKeyOnInvite(true)
|
||||
}
|
||||
val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!!
|
||||
|
||||
// Bob
|
||||
val bobSession = cryptoTestData.secondSession!!.also {
|
||||
it.cryptoService().enableShareKeyOnInvite(true)
|
||||
}
|
||||
val bobRoomPOV = bobSession.roomService().getRoom(e2eRoomID)!!
|
||||
|
||||
assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2)
|
||||
Log.v("#E2E TEST", "Alice and Bob are in roomId: $e2eRoomID")
|
||||
|
||||
val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, "Hello Bob, I am Alice!", testHelper)
|
||||
Assert.assertTrue("Message should be sent", aliceMessageId != null)
|
||||
Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID")
|
||||
|
||||
// Bob should be able to decrypt the message
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
|
||||
(timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE).also {
|
||||
if (it) {
|
||||
Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new user
|
||||
val arisSession = testHelper.createAccount("aris", SessionTestParams(true)).also {
|
||||
it.cryptoService().enableShareKeyOnInvite(true)
|
||||
}
|
||||
Log.v("#E2E TEST", "Aris user created")
|
||||
|
||||
// Alice invites new user to the room
|
||||
testHelper.runBlockingTest {
|
||||
Log.v("#E2E TEST", "Alice invites ${arisSession.myUserId}")
|
||||
aliceRoomPOV.membershipService().invite(arisSession.myUserId)
|
||||
}
|
||||
|
||||
waitForAndAcceptInviteInRoom(arisSession, e2eRoomID, testHelper)
|
||||
|
||||
ensureMembersHaveJoined(aliceSession, arrayListOf(arisSession), e2eRoomID, testHelper)
|
||||
Log.v("#E2E TEST", "Aris has joined roomId: $e2eRoomID")
|
||||
|
||||
when (roomHistoryVisibility) {
|
||||
RoomHistoryVisibility.WORLD_READABLE,
|
||||
RoomHistoryVisibility.SHARED,
|
||||
null
|
||||
-> {
|
||||
// Aris should be able to decrypt the message
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
|
||||
(timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE
|
||||
).also {
|
||||
if (it) {
|
||||
Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RoomHistoryVisibility.INVITED,
|
||||
RoomHistoryVisibility.JOINED -> {
|
||||
// Aris should not even be able to get the message
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)
|
||||
?.timelineService()
|
||||
?.getTimelineEvent(aliceMessageId!!)
|
||||
timelineEvent == null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testHelper.signOutAndClose(arisSession)
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNeedsRotationFromWorldReadableToShared() {
|
||||
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("shared"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNeedsRotationFromWorldReadableToInvited() {
|
||||
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("invited"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNeedsRotationFromWorldReadableToJoined() {
|
||||
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("joined"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNeedsRotationFromSharedToWorldReadable() {
|
||||
testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("world_readable"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNeedsRotationFromSharedToInvited() {
|
||||
testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("invited"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNeedsRotationFromSharedToJoined() {
|
||||
testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("joined"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNeedsRotationFromInvitedToShared() {
|
||||
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("shared"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNeedsRotationFromInvitedToWorldReadable() {
|
||||
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("world_readable"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNeedsRotationFromInvitedToJoined() {
|
||||
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("joined"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNeedsRotationFromJoinedToShared() {
|
||||
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("shared"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNeedsRotationFromJoinedToInvited() {
|
||||
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("invited"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNeedsRotationFromJoinedToWorldReadable() {
|
||||
testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("world_readable"))
|
||||
}
|
||||
|
||||
/**
|
||||
* In this test we will test that a rotation is needed when
|
||||
* When the room's history visibility setting changes to world_readable or shared
|
||||
* from invited or joined, or changes to invited or joined from world_readable or shared,
|
||||
* senders that support this flag must rotate their megolm sessions.
|
||||
*/
|
||||
private fun testRotationDueToVisibilityChange(
|
||||
initRoomHistoryVisibility: RoomHistoryVisibility,
|
||||
nextRoomHistoryVisibility: RoomHistoryVisibilityContent
|
||||
) {
|
||||
val testHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(testHelper)
|
||||
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, initRoomHistoryVisibility)
|
||||
val e2eRoomID = cryptoTestData.roomId
|
||||
|
||||
// Alice
|
||||
val aliceSession = cryptoTestData.firstSession.also {
|
||||
it.cryptoService().enableShareKeyOnInvite(true)
|
||||
}
|
||||
val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!!
|
||||
// val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
|
||||
|
||||
// Bob
|
||||
val bobSession = cryptoTestData.secondSession!!
|
||||
|
||||
val bobRoomPOV = bobSession.roomService().getRoom(e2eRoomID)!!
|
||||
|
||||
assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2)
|
||||
Log.v("#E2E TEST ROTATION", "Alice and Bob are in roomId: $e2eRoomID")
|
||||
|
||||
val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, "Hello Bob, I am Alice!", testHelper)
|
||||
Assert.assertTrue("Message should be sent", aliceMessageId != null)
|
||||
Log.v("#E2E TEST ROTATION", "Alice sent message to roomId: $e2eRoomID")
|
||||
|
||||
// Bob should be able to decrypt the message
|
||||
var firstAliceMessageMegolmSessionId: String? = null
|
||||
val bobRoomPov = bobSession.roomService().getRoom(e2eRoomID)
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = bobRoomPov
|
||||
?.timelineService()
|
||||
?.getTimelineEvent(aliceMessageId!!)
|
||||
(timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE).also {
|
||||
if (it) {
|
||||
firstAliceMessageMegolmSessionId = timelineEvent?.root?.content?.get("session_id") as? String
|
||||
Log.v(
|
||||
"#E2E TEST",
|
||||
"Bob can decrypt the message (sid:$firstAliceMessageMegolmSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Assert.assertNotNull("megolm session id can't be null", firstAliceMessageMegolmSessionId)
|
||||
|
||||
var secondAliceMessageSessionId: String? = null
|
||||
sendMessageInRoom(aliceRoomPOV, "Other msg", testHelper)?.let { secondMessage ->
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = bobRoomPov
|
||||
?.timelineService()
|
||||
?.getTimelineEvent(secondMessage)
|
||||
(timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE).also {
|
||||
if (it) {
|
||||
secondAliceMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String
|
||||
Log.v(
|
||||
"#E2E TEST",
|
||||
"Bob can decrypt the message (sid:$secondAliceMessageSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertEquals("No rotation needed session should be the same", firstAliceMessageMegolmSessionId, secondAliceMessageSessionId)
|
||||
Log.v("#E2E TEST ROTATION", "No rotation needed yet")
|
||||
|
||||
// Let's change the room history visibility
|
||||
testHelper.runBlockingTest {
|
||||
aliceRoomPOV.stateService()
|
||||
.sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||
stateKey = "",
|
||||
body = RoomHistoryVisibilityContent(
|
||||
historyVisibilityStr = nextRoomHistoryVisibility.historyVisibilityStr
|
||||
).toContent()
|
||||
)
|
||||
}
|
||||
|
||||
// ensure that the state did synced down
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)?.content
|
||||
?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
|
||||
}
|
||||
}
|
||||
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val roomVisibility = aliceSession.getRoom(e2eRoomID)!!
|
||||
.stateService()
|
||||
.getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)
|
||||
?.content
|
||||
?.toModel<RoomHistoryVisibilityContent>()
|
||||
Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}")
|
||||
roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
|
||||
}
|
||||
}
|
||||
|
||||
var aliceThirdMessageSessionId: String? = null
|
||||
sendMessageInRoom(aliceRoomPOV, "Message after visibility change", testHelper)?.let { thirdMessage ->
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timelineEvent = bobRoomPov
|
||||
?.timelineService()
|
||||
?.getTimelineEvent(thirdMessage)
|
||||
(timelineEvent != null &&
|
||||
timelineEvent.isEncrypted() &&
|
||||
timelineEvent.root.getClearType() == EventType.MESSAGE).also {
|
||||
if (it) {
|
||||
aliceThirdMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
initRoomHistoryVisibility.shouldShareHistory() == nextRoomHistoryVisibility.historyVisibility?.shouldShareHistory() -> {
|
||||
assertEquals("Session shouldn't have been rotated", secondAliceMessageSessionId, aliceThirdMessageSessionId)
|
||||
Log.v("#E2E TEST ROTATION", "Rotation is not needed")
|
||||
}
|
||||
initRoomHistoryVisibility.shouldShareHistory() != nextRoomHistoryVisibility.historyVisibility!!.shouldShareHistory() -> {
|
||||
assertNotEquals("Session should have been rotated", secondAliceMessageSessionId, aliceThirdMessageSessionId)
|
||||
Log.v("#E2E TEST ROTATION", "Rotation is needed!")
|
||||
}
|
||||
}
|
||||
|
||||
cryptoTestData.cleanUp(testHelper)
|
||||
}
|
||||
|
||||
private fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? {
|
||||
return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.eventId
|
||||
}
|
||||
|
||||
private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String, testHelper: CommonTestHelper) {
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
otherAccounts.map {
|
||||
aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership
|
||||
}.all {
|
||||
it == Membership.JOIN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) {
|
||||
testHelper.waitWithLatch { latch ->
|
||||
testHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID)
|
||||
(roomSummary != null && roomSummary.membership == Membership.INVITE).also {
|
||||
if (it) {
|
||||
Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testHelper.runBlockingTest(60_000) {
|
||||
Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
|
||||
try {
|
||||
otherSession.roomService().joinRoom(e2eRoomID)
|
||||
} catch (ex: JoinRoomFailure.JoinedWithTimeout) {
|
||||
// it's ok we will wait after
|
||||
}
|
||||
}
|
||||
|
||||
Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
|
||||
testHelper.waitWithLatch {
|
||||
testHelper.retryPeriodicallyWithLatch(it) {
|
||||
val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID)
|
||||
roomSummary != null && roomSummary.membership == Membership.JOIN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -72,7 +72,7 @@ class PreShareKeysTest : InstrumentedTest {
|
|||
assertNotNull("Bob should have received and decrypted a room key event from alice", bobInboundForAlice)
|
||||
assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId)
|
||||
|
||||
val megolmSessionId = bobInboundForAlice.olmInboundGroupSession!!.sessionIdentifier()
|
||||
val megolmSessionId = bobInboundForAlice.session.sessionIdentifier()
|
||||
|
||||
assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId)
|
||||
|
||||
|
|
|
@ -19,14 +19,14 @@ package org.matrix.android.sdk.internal.crypto.keysbackup
|
|||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestData
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
|
||||
|
||||
/**
|
||||
* Data class to store result of [KeysBackupTestHelper.createKeysBackupScenarioWithPassword]
|
||||
*/
|
||||
internal data class KeysBackupScenarioData(
|
||||
val cryptoTestData: CryptoTestData,
|
||||
val aliceKeys: List<OlmInboundGroupSessionWrapper2>,
|
||||
val aliceKeys: List<MXInboundMegolmSessionWrapper>,
|
||||
val prepareKeysBackupDataResult: PrepareKeysBackupDataResult,
|
||||
val aliceSession2: Session
|
||||
) {
|
||||
|
|
|
@ -301,7 +301,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo
|
||||
|
||||
// - Check encryptGroupSession() returns stg
|
||||
val keyBackupData = keysBackup.encryptGroupSession(session)
|
||||
val keyBackupData = testHelper.runBlockingTest { keysBackup.encryptGroupSession(session) }
|
||||
assertNotNull(keyBackupData)
|
||||
assertNotNull(keyBackupData!!.sessionData)
|
||||
|
||||
|
@ -312,7 +312,7 @@ class KeysBackupTest : InstrumentedTest {
|
|||
val sessionData = keysBackup
|
||||
.decryptKeyBackupData(
|
||||
keyBackupData,
|
||||
session.olmInboundGroupSession!!.sessionIdentifier(),
|
||||
session.safeSessionId!!,
|
||||
cryptoTestData.roomId,
|
||||
decryption!!
|
||||
)
|
||||
|
|
|
@ -187,7 +187,7 @@ internal class KeysBackupTestHelper(
|
|||
// - 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!!)
|
||||
.getInboundGroupSession(aliceKey1.safeSessionId!!, aliceKey1.senderKey!!)
|
||||
Assert.assertNotNull(aliceKey2)
|
||||
assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys())
|
||||
}
|
||||
|
|
|
@ -56,19 +56,17 @@ class SpaceCreationTest : InstrumentedTest {
|
|||
val roomName = "My Space"
|
||||
val topic = "A public space for test"
|
||||
var spaceId: String = ""
|
||||
commonTestHelper.waitWithLatch {
|
||||
commonTestHelper.runBlockingTest {
|
||||
spaceId = session.spaceService().createSpace(roomName, topic, null, true)
|
||||
// wait a bit to let the summary update it self :/
|
||||
it.countDown()
|
||||
}
|
||||
Thread.sleep(4_000)
|
||||
|
||||
val syncedSpace = session.spaceService().getSpace(spaceId)
|
||||
commonTestHelper.waitWithLatch {
|
||||
commonTestHelper.retryPeriodicallyWithLatch(it) {
|
||||
syncedSpace?.asRoom()?.roomSummary()?.name != null
|
||||
session.spaceService().getSpace(spaceId)?.asRoom()?.roomSummary()?.name != null
|
||||
}
|
||||
}
|
||||
|
||||
val syncedSpace = session.spaceService().getSpace(spaceId)
|
||||
assertEquals("Room name should be set", roomName, syncedSpace?.asRoom()?.roomSummary()?.name)
|
||||
assertEquals("Room topic should be set", topic, syncedSpace?.asRoom()?.roomSummary()?.topic)
|
||||
// assertEquals(topic, syncedSpace.asRoom().roomSummary()?., "Room topic should be set")
|
||||
|
|
|
@ -20,7 +20,6 @@ import android.util.Log
|
|||
import androidx.lifecycle.Observer
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Ignore
|
||||
|
@ -62,47 +61,40 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
val spaceName = "My Space"
|
||||
val topic = "A public space for test"
|
||||
var spaceId = ""
|
||||
commonTestHelper.waitWithLatch {
|
||||
commonTestHelper.runBlockingTest {
|
||||
spaceId = session.spaceService().createSpace(spaceName, topic, null, true)
|
||||
it.countDown()
|
||||
}
|
||||
|
||||
val syncedSpace = session.spaceService().getSpace(spaceId)
|
||||
|
||||
var roomId = ""
|
||||
commonTestHelper.waitWithLatch {
|
||||
commonTestHelper.runBlockingTest {
|
||||
roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" })
|
||||
it.countDown()
|
||||
}
|
||||
|
||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||
|
||||
commonTestHelper.waitWithLatch {
|
||||
commonTestHelper.runBlockingTest {
|
||||
syncedSpace!!.addChildren(roomId, viaServers, null, true)
|
||||
it.countDown()
|
||||
}
|
||||
|
||||
commonTestHelper.waitWithLatch {
|
||||
commonTestHelper.runBlockingTest {
|
||||
session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers)
|
||||
it.countDown()
|
||||
}
|
||||
|
||||
Thread.sleep(9000)
|
||||
|
||||
val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents
|
||||
val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true }
|
||||
|
||||
parents?.forEach {
|
||||
Log.d("## TEST", "parent : $it")
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents
|
||||
val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true }
|
||||
parents?.forEach {
|
||||
Log.d("## TEST", "parent : $it")
|
||||
}
|
||||
parents?.size == 1 &&
|
||||
parents.first().roomSummary?.name == spaceName &&
|
||||
canonicalParents?.size == 1 &&
|
||||
canonicalParents.first().roomSummary?.name == spaceName
|
||||
}
|
||||
}
|
||||
|
||||
assertNotNull(parents)
|
||||
assertEquals(1, parents!!.size)
|
||||
assertEquals(spaceName, parents.first().roomSummary?.name)
|
||||
|
||||
assertNotNull(canonicalParents)
|
||||
assertEquals(1, canonicalParents!!.size)
|
||||
assertEquals(spaceName, canonicalParents.first().roomSummary?.name)
|
||||
}
|
||||
|
||||
// @Test
|
||||
|
@ -173,52 +165,55 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
// }
|
||||
|
||||
@Test
|
||||
fun testFilteringBySpace() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
|
||||
fun testFilteringBySpace() = runSessionTest(context()) { commonTestHelper ->
|
||||
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
|
||||
|
||||
val spaceAInfo = createPublicSpace(
|
||||
session, "SpaceA", listOf(
|
||||
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("A2", true, true)
|
||||
)
|
||||
commonTestHelper,
|
||||
session, "SpaceA",
|
||||
listOf(
|
||||
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("A2", true, true)
|
||||
)
|
||||
)
|
||||
|
||||
/* val spaceBInfo = */ createPublicSpace(
|
||||
session, "SpaceB", listOf(
|
||||
Triple("B1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("B2", true, true),
|
||||
Triple("B3", true, true)
|
||||
)
|
||||
commonTestHelper,
|
||||
session, "SpaceB",
|
||||
listOf(
|
||||
Triple("B1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("B2", true, true),
|
||||
Triple("B3", true, true)
|
||||
)
|
||||
)
|
||||
|
||||
val spaceCInfo = createPublicSpace(
|
||||
session, "SpaceC", listOf(
|
||||
Triple("C1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("C2", true, true)
|
||||
)
|
||||
commonTestHelper,
|
||||
session, "SpaceC",
|
||||
listOf(
|
||||
Triple("C1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("C2", true, true)
|
||||
)
|
||||
)
|
||||
|
||||
// add C as a subspace of A
|
||||
val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
|
||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||
commonTestHelper.waitWithLatch {
|
||||
commonTestHelper.runBlockingTest {
|
||||
spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
|
||||
session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
|
||||
it.countDown()
|
||||
}
|
||||
|
||||
// Create orphan rooms
|
||||
|
||||
var orphan1 = ""
|
||||
commonTestHelper.waitWithLatch {
|
||||
commonTestHelper.runBlockingTest {
|
||||
orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" })
|
||||
it.countDown()
|
||||
}
|
||||
|
||||
var orphan2 = ""
|
||||
commonTestHelper.waitWithLatch {
|
||||
commonTestHelper.runBlockingTest {
|
||||
orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" })
|
||||
it.countDown()
|
||||
}
|
||||
|
||||
val allRooms = session.roomService().getRoomSummaries(roomSummaryQueryParams { excludeType = listOf(RoomType.SPACE) })
|
||||
|
@ -240,10 +235,9 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
assertTrue("A1 should be a grand child of A", aChildren.any { it.name == "C2" })
|
||||
|
||||
// Add a non canonical child and check that it does not appear as orphan
|
||||
commonTestHelper.waitWithLatch {
|
||||
commonTestHelper.runBlockingTest {
|
||||
val a3 = session.roomService().createRoom(CreateRoomParams().apply { name = "A3" })
|
||||
spaceA!!.addChildren(a3, viaServers, null, false)
|
||||
it.countDown()
|
||||
}
|
||||
|
||||
Thread.sleep(6_000)
|
||||
|
@ -255,37 +249,39 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
|
||||
@Test
|
||||
@Ignore("This test will be ignored until it is fixed")
|
||||
fun testBreakCycle() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
|
||||
fun testBreakCycle() = runSessionTest(context()) { commonTestHelper ->
|
||||
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
|
||||
|
||||
val spaceAInfo = createPublicSpace(
|
||||
session, "SpaceA", listOf(
|
||||
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("A2", true, true)
|
||||
)
|
||||
commonTestHelper,
|
||||
session, "SpaceA",
|
||||
listOf(
|
||||
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("A2", true, true)
|
||||
)
|
||||
)
|
||||
|
||||
val spaceCInfo = createPublicSpace(
|
||||
session, "SpaceC", listOf(
|
||||
Triple("C1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("C2", true, true)
|
||||
)
|
||||
commonTestHelper,
|
||||
session, "SpaceC",
|
||||
listOf(
|
||||
Triple("C1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("C2", true, true)
|
||||
)
|
||||
)
|
||||
|
||||
// add C as a subspace of A
|
||||
val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
|
||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||
commonTestHelper.waitWithLatch {
|
||||
commonTestHelper.runBlockingTest {
|
||||
spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
|
||||
session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
|
||||
it.countDown()
|
||||
}
|
||||
|
||||
// add back A as subspace of C
|
||||
commonTestHelper.waitWithLatch {
|
||||
commonTestHelper.runBlockingTest {
|
||||
val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId)
|
||||
spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true)
|
||||
it.countDown()
|
||||
}
|
||||
|
||||
// A -> C -> A
|
||||
|
@ -300,37 +296,46 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun testLiveFlatChildren() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
|
||||
fun testLiveFlatChildren() = runSessionTest(context()) { commonTestHelper ->
|
||||
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
|
||||
|
||||
val spaceAInfo = createPublicSpace(
|
||||
session, "SpaceA", listOf(
|
||||
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("A2", true, true)
|
||||
)
|
||||
commonTestHelper,
|
||||
session,
|
||||
"SpaceA",
|
||||
listOf(
|
||||
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("A2", true, true)
|
||||
)
|
||||
)
|
||||
|
||||
val spaceBInfo = createPublicSpace(
|
||||
session, "SpaceB", listOf(
|
||||
Triple("B1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("B2", true, true),
|
||||
Triple("B3", true, true)
|
||||
)
|
||||
commonTestHelper,
|
||||
session,
|
||||
"SpaceB",
|
||||
listOf(
|
||||
Triple("B1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("B2", true, true),
|
||||
Triple("B3", true, true)
|
||||
)
|
||||
)
|
||||
|
||||
// add B as a subspace of A
|
||||
val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
|
||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||
runBlocking {
|
||||
commonTestHelper.runBlockingTest {
|
||||
spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true)
|
||||
session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
|
||||
}
|
||||
|
||||
val spaceCInfo = createPublicSpace(
|
||||
session, "SpaceC", listOf(
|
||||
Triple("C1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("C2", true, true)
|
||||
)
|
||||
commonTestHelper,
|
||||
session,
|
||||
"SpaceC",
|
||||
listOf(
|
||||
Triple("C1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("C2", true, true)
|
||||
)
|
||||
)
|
||||
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
|
@ -348,13 +353,13 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
}
|
||||
}
|
||||
|
||||
flatAChildren.observeForever(childObserver)
|
||||
|
||||
// add C as subspace of B
|
||||
val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
|
||||
spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
|
||||
|
||||
// C1 and C2 should be in flatten child of A now
|
||||
|
||||
flatAChildren.observeForever(childObserver)
|
||||
}
|
||||
|
||||
// Test part one of the rooms
|
||||
|
@ -374,10 +379,10 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
}
|
||||
}
|
||||
|
||||
// part from b room
|
||||
session.roomService().leaveRoom(bRoomId)
|
||||
// The room should have disapear from flat children
|
||||
flatAChildren.observeForever(childObserver)
|
||||
// part from b room
|
||||
session.roomService().leaveRoom(bRoomId)
|
||||
}
|
||||
commonTestHelper.signOutAndClose(session)
|
||||
}
|
||||
|
@ -388,6 +393,7 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
)
|
||||
|
||||
private fun createPublicSpace(
|
||||
commonTestHelper: CommonTestHelper,
|
||||
session: Session,
|
||||
spaceName: String,
|
||||
childInfo: List<Triple<String, Boolean, Boolean?>>
|
||||
|
@ -395,29 +401,27 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
): TestSpaceCreationResult {
|
||||
var spaceId = ""
|
||||
var roomIds: List<String> = emptyList()
|
||||
runSessionTest(context()) { commonTestHelper ->
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
|
||||
val syncedSpace = session.spaceService().getSpace(spaceId)
|
||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||
commonTestHelper.runBlockingTest {
|
||||
spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
|
||||
val syncedSpace = session.spaceService().getSpace(spaceId)
|
||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||
|
||||
roomIds = childInfo.map { entry ->
|
||||
session.roomService().createRoom(CreateRoomParams().apply { name = entry.first })
|
||||
roomIds = childInfo.map { entry ->
|
||||
session.roomService().createRoom(CreateRoomParams().apply { name = entry.first })
|
||||
}
|
||||
roomIds.forEachIndexed { index, roomId ->
|
||||
syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
|
||||
val canonical = childInfo[index].third
|
||||
if (canonical != null) {
|
||||
session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
|
||||
}
|
||||
roomIds.forEachIndexed { index, roomId ->
|
||||
syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
|
||||
val canonical = childInfo[index].third
|
||||
if (canonical != null) {
|
||||
session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
|
||||
}
|
||||
}
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
return TestSpaceCreationResult(spaceId, roomIds)
|
||||
}
|
||||
|
||||
private fun createPrivateSpace(
|
||||
commonTestHelper: CommonTestHelper,
|
||||
session: Session,
|
||||
spaceName: String,
|
||||
childInfo: List<Triple<String, Boolean, Boolean?>>
|
||||
|
@ -425,34 +429,31 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
): TestSpaceCreationResult {
|
||||
var spaceId = ""
|
||||
var roomIds: List<String> = emptyList()
|
||||
runSessionTest(context()) { commonTestHelper ->
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false)
|
||||
val syncedSpace = session.spaceService().getSpace(spaceId)
|
||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||
roomIds =
|
||||
childInfo.map { entry ->
|
||||
val homeServerCapabilities = session
|
||||
.homeServerCapabilitiesService()
|
||||
.getHomeServerCapabilities()
|
||||
session.roomService().createRoom(CreateRoomParams().apply {
|
||||
name = entry.first
|
||||
this.featurePreset = RestrictedRoomPreset(
|
||||
homeServerCapabilities,
|
||||
listOf(
|
||||
RoomJoinRulesAllowEntry.restrictedToRoom(spaceId)
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
roomIds.forEachIndexed { index, roomId ->
|
||||
syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
|
||||
val canonical = childInfo[index].third
|
||||
if (canonical != null) {
|
||||
session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
|
||||
commonTestHelper.runBlockingTest {
|
||||
spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false)
|
||||
val syncedSpace = session.spaceService().getSpace(spaceId)
|
||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||
roomIds =
|
||||
childInfo.map { entry ->
|
||||
val homeServerCapabilities = session
|
||||
.homeServerCapabilitiesService()
|
||||
.getHomeServerCapabilities()
|
||||
session.roomService().createRoom(CreateRoomParams().apply {
|
||||
name = entry.first
|
||||
this.featurePreset = RestrictedRoomPreset(
|
||||
homeServerCapabilities,
|
||||
listOf(
|
||||
RoomJoinRulesAllowEntry.restrictedToRoom(spaceId)
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
roomIds.forEachIndexed { index, roomId ->
|
||||
syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
|
||||
val canonical = childInfo[index].third
|
||||
if (canonical != null) {
|
||||
session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
|
||||
}
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
return TestSpaceCreationResult(spaceId, roomIds)
|
||||
|
@ -463,25 +464,31 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
|
||||
|
||||
/* val spaceAInfo = */ createPublicSpace(
|
||||
session, "SpaceA", listOf(
|
||||
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("A2", true, true)
|
||||
)
|
||||
commonTestHelper,
|
||||
session, "SpaceA",
|
||||
listOf(
|
||||
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("A2", true, true)
|
||||
)
|
||||
)
|
||||
|
||||
val spaceBInfo = createPublicSpace(
|
||||
session, "SpaceB", listOf(
|
||||
Triple("B1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("B2", true, true),
|
||||
Triple("B3", true, true)
|
||||
)
|
||||
commonTestHelper,
|
||||
session, "SpaceB",
|
||||
listOf(
|
||||
Triple("B1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("B2", true, true),
|
||||
Triple("B3", true, true)
|
||||
)
|
||||
)
|
||||
|
||||
val spaceCInfo = createPublicSpace(
|
||||
session, "SpaceC", listOf(
|
||||
Triple("C1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("C2", true, true)
|
||||
)
|
||||
commonTestHelper,
|
||||
session, "SpaceC",
|
||||
listOf(
|
||||
Triple("C1", true /*auto-join*/, true/*canonical*/),
|
||||
Triple("C2", true, true)
|
||||
)
|
||||
)
|
||||
|
||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||
|
@ -490,7 +497,6 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
runBlocking {
|
||||
val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
|
||||
spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
|
||||
Thread.sleep(6_000)
|
||||
}
|
||||
|
||||
// Thread.sleep(4_000)
|
||||
|
@ -501,11 +507,12 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
// + C
|
||||
// + c1, c2
|
||||
|
||||
val rootSpaces = commonTestHelper.runBlockingTest {
|
||||
session.spaceService().getRootSpaceSummaries()
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val rootSpaces = commonTestHelper.runBlockingTest { session.spaceService().getRootSpaceSummaries() }
|
||||
rootSpaces.size == 2
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals("Unexpected number of root spaces ${rootSpaces.map { it.name }}", 2, rootSpaces.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -514,10 +521,12 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true))
|
||||
|
||||
val spaceAInfo = createPrivateSpace(
|
||||
aliceSession, "Private Space A", listOf(
|
||||
Triple("General", true /*suggested*/, true/*canonical*/),
|
||||
Triple("Random", true, true)
|
||||
)
|
||||
commonTestHelper,
|
||||
aliceSession, "Private Space A",
|
||||
listOf(
|
||||
Triple("General", true /*suggested*/, true/*canonical*/),
|
||||
Triple("Random", true, true)
|
||||
)
|
||||
)
|
||||
|
||||
commonTestHelper.runBlockingTest {
|
||||
|
@ -529,10 +538,9 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
}
|
||||
|
||||
var bobRoomId = ""
|
||||
commonTestHelper.waitWithLatch {
|
||||
commonTestHelper.runBlockingTest {
|
||||
bobRoomId = bobSession.roomService().createRoom(CreateRoomParams().apply { name = "A Bob Room" })
|
||||
bobSession.getRoom(bobRoomId)!!.membershipService().invite(aliceSession.myUserId)
|
||||
it.countDown()
|
||||
}
|
||||
|
||||
commonTestHelper.runBlockingTest {
|
||||
|
@ -545,9 +553,8 @@ class SpaceHierarchyTest : InstrumentedTest {
|
|||
}
|
||||
}
|
||||
|
||||
commonTestHelper.waitWithLatch {
|
||||
commonTestHelper.runBlockingTest {
|
||||
bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: ""))
|
||||
it.countDown()
|
||||
}
|
||||
|
||||
commonTestHelper.waitWithLatch { latch ->
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.auth
|
||||
|
||||
enum class LoginType {
|
||||
PASSWORD,
|
||||
SSO,
|
||||
UNSUPPORTED,
|
||||
CUSTOM,
|
||||
DIRECT,
|
||||
UNKNOWN;
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromName(name: String) = when (name) {
|
||||
PASSWORD.name -> PASSWORD
|
||||
SSO.name -> SSO
|
||||
UNSUPPORTED.name -> UNSUPPORTED
|
||||
CUSTOM.name -> CUSTOM
|
||||
DIRECT.name -> DIRECT
|
||||
else -> UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package org.matrix.android.sdk.api.auth.data
|
||||
|
||||
import org.matrix.android.sdk.api.auth.LoginType
|
||||
|
||||
/**
|
||||
* This data class holds necessary data to open a session.
|
||||
* You don't have to manually instantiate it.
|
||||
|
@ -34,7 +36,12 @@ data class SessionParams(
|
|||
/**
|
||||
* Set to false if the current token is not valid anymore. Application should not have to use this info.
|
||||
*/
|
||||
val isTokenValid: Boolean
|
||||
val isTokenValid: Boolean,
|
||||
|
||||
/**
|
||||
* The authentication method that was used to create the session.
|
||||
*/
|
||||
val loginType: LoginType,
|
||||
) {
|
||||
/*
|
||||
* Shortcuts. Usually the application should only need to use these shortcuts
|
||||
|
|
|
@ -38,4 +38,5 @@ data class MXCryptoConfig constructor(
|
|||
* You can limit request only to your sessions by turning this setting to `true`
|
||||
*/
|
||||
val limitRoomKeyRequestsToMyDevices: Boolean = false,
|
||||
)
|
||||
|
||||
)
|
||||
|
|
|
@ -40,6 +40,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic
|
|||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.SessionInfo
|
||||
|
||||
interface CryptoService {
|
||||
|
||||
|
@ -84,6 +85,20 @@ interface CryptoService {
|
|||
|
||||
fun isKeyGossipingEnabled(): Boolean
|
||||
|
||||
/**
|
||||
* As per MSC3061.
|
||||
* If true will make it possible to share part of e2ee room history
|
||||
* on invite depending on the room visibility setting.
|
||||
*/
|
||||
fun enableShareKeyOnInvite(enable: Boolean)
|
||||
|
||||
/**
|
||||
* As per MSC3061.
|
||||
* If true will make it possible to share part of e2ee room history
|
||||
* on invite depending on the room visibility setting.
|
||||
*/
|
||||
fun isShareKeysOnInviteEnabled(): Boolean
|
||||
|
||||
fun setRoomUnBlacklistUnverifiedDevices(roomId: String)
|
||||
|
||||
fun getDeviceTrackingStatus(userId: String): Int
|
||||
|
@ -176,4 +191,9 @@ interface CryptoService {
|
|||
* send, in order to speed up sending of the message.
|
||||
*/
|
||||
fun prepareToEncrypt(roomId: String, callback: MatrixCallback<Unit>)
|
||||
|
||||
/**
|
||||
* Share all inbound sessions of the last chunk messages to the provided userId devices.
|
||||
*/
|
||||
suspend fun sendSharedHistoryKeys(roomId: String, userId: String, sessionInfoSet: Set<SessionInfo>?)
|
||||
}
|
||||
|
|
|
@ -69,5 +69,11 @@ data class ForwardedRoomKeyContent(
|
|||
* private part of this key unless they have done device verification.
|
||||
*/
|
||||
@Json(name = "sender_claimed_ed25519_key")
|
||||
val senderClaimedEd25519Key: String? = null
|
||||
val senderClaimedEd25519Key: String? = null,
|
||||
|
||||
/**
|
||||
* MSC3061 Identifies keys that were sent when the room's visibility setting was set to world_readable or shared.
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3061.shared_history")
|
||||
val sharedHistory: Boolean? = false,
|
||||
)
|
||||
|
|
|
@ -38,5 +38,12 @@ data class RoomKeyContent(
|
|||
|
||||
// should be a Long but it is sometimes a double
|
||||
@Json(name = "chain_index")
|
||||
val chainIndex: Any? = null
|
||||
val chainIndex: Any? = null,
|
||||
|
||||
/**
|
||||
* MSC3061 Identifies keys that were sent when the room's visibility setting was set to world_readable or shared.
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3061.shared_history")
|
||||
val sharedHistory: Boolean? = false
|
||||
|
||||
)
|
||||
|
|
|
@ -48,3 +48,9 @@ enum class RoomHistoryVisibility {
|
|||
*/
|
||||
@Json(name = "joined") JOINED
|
||||
}
|
||||
|
||||
/**
|
||||
* Room history should be shared only if room visibility is world_readable or shared.
|
||||
*/
|
||||
internal fun RoomHistoryVisibility.shouldShareHistory() =
|
||||
this == RoomHistoryVisibility.WORLD_READABLE || this == RoomHistoryVisibility.SHARED
|
||||
|
|
|
@ -83,6 +83,9 @@ internal abstract class AuthModule {
|
|||
@Binds
|
||||
abstract fun bindSessionCreator(creator: DefaultSessionCreator): SessionCreator
|
||||
|
||||
@Binds
|
||||
abstract fun bindSessionParamsCreator(creator: DefaultSessionParamsCreator): SessionParamsCreator
|
||||
|
||||
@Binds
|
||||
abstract fun bindDirectLoginTask(task: DefaultDirectLoginTask): DirectLoginTask
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import okhttp3.OkHttpClient
|
|||
import org.matrix.android.sdk.api.MatrixPatterns
|
||||
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||
import org.matrix.android.sdk.api.auth.LoginType
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.api.auth.data.LoginFlowResult
|
||||
|
@ -361,7 +362,7 @@ internal class DefaultAuthenticationService @Inject constructor(
|
|||
homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||
credentials: Credentials
|
||||
): Session {
|
||||
return sessionCreator.createSession(credentials, homeServerConnectionConfig)
|
||||
return sessionCreator.createSession(credentials, homeServerConnectionConfig, LoginType.SSO)
|
||||
}
|
||||
|
||||
override suspend fun getWellKnownData(
|
||||
|
|
|
@ -16,69 +16,41 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.auth
|
||||
|
||||
import android.net.Uri
|
||||
import org.matrix.android.sdk.api.auth.LoginType
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.internal.SessionManager
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface SessionCreator {
|
||||
suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session
|
||||
|
||||
suspend fun createSession(
|
||||
credentials: Credentials,
|
||||
homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||
loginType: LoginType,
|
||||
): Session
|
||||
}
|
||||
|
||||
internal class DefaultSessionCreator @Inject constructor(
|
||||
private val sessionParamsStore: SessionParamsStore,
|
||||
private val sessionManager: SessionManager,
|
||||
private val pendingSessionStore: PendingSessionStore,
|
||||
private val isValidClientServerApiTask: IsValidClientServerApiTask
|
||||
private val sessionParamsCreator: SessionParamsCreator,
|
||||
) : SessionCreator {
|
||||
|
||||
/**
|
||||
* Credentials can affect the homeServerConnectionConfig, override homeserver url and/or
|
||||
* identity server url if provided in the credentials.
|
||||
*/
|
||||
override suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session {
|
||||
override suspend fun createSession(
|
||||
credentials: Credentials,
|
||||
homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||
loginType: LoginType,
|
||||
): Session {
|
||||
// We can cleanup the pending session params
|
||||
pendingSessionStore.delete()
|
||||
|
||||
val overriddenUrl = credentials.discoveryInformation?.homeServer?.baseURL
|
||||
// remove trailing "/"
|
||||
?.trim { it == '/' }
|
||||
?.takeIf { it.isNotBlank() }
|
||||
// It can be the same value, so in this case, do not check again the validity
|
||||
?.takeIf { it != homeServerConnectionConfig.homeServerUriBase.toString() }
|
||||
?.also { Timber.d("Overriding homeserver url to $it (will check if valid)") }
|
||||
?.let { Uri.parse(it) }
|
||||
?.takeIf {
|
||||
// Validate the URL, if the configuration is wrong server side, do not override
|
||||
tryOrNull {
|
||||
isValidClientServerApiTask.execute(
|
||||
IsValidClientServerApiTask.Params(
|
||||
homeServerConnectionConfig.copy(homeServerUriBase = it)
|
||||
)
|
||||
)
|
||||
.also { Timber.d("Overriding homeserver url: $it") }
|
||||
} ?: true // In case of other error (no network, etc.), consider it is valid...
|
||||
}
|
||||
|
||||
val sessionParams = SessionParams(
|
||||
credentials = credentials,
|
||||
homeServerConnectionConfig = homeServerConnectionConfig.copy(
|
||||
homeServerUriBase = overriddenUrl ?: homeServerConnectionConfig.homeServerUriBase,
|
||||
identityServerUri = credentials.discoveryInformation?.identityServer?.baseURL
|
||||
// remove trailing "/"
|
||||
?.trim { it == '/' }
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.also { Timber.d("Overriding identity server url to $it") }
|
||||
?.let { Uri.parse(it) }
|
||||
?: homeServerConnectionConfig.identityServerUri
|
||||
),
|
||||
isTokenValid = true)
|
||||
|
||||
val sessionParams = sessionParamsCreator.create(credentials, homeServerConnectionConfig, loginType)
|
||||
sessionParamsStore.save(sessionParams)
|
||||
return sessionManager.getOrCreateSession(sessionParams)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.auth
|
||||
|
||||
import android.net.Uri
|
||||
import org.matrix.android.sdk.api.auth.LoginType
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface SessionParamsCreator {
|
||||
|
||||
suspend fun create(
|
||||
credentials: Credentials,
|
||||
homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||
loginType: LoginType,
|
||||
): SessionParams
|
||||
}
|
||||
|
||||
internal class DefaultSessionParamsCreator @Inject constructor(
|
||||
private val isValidClientServerApiTask: IsValidClientServerApiTask
|
||||
) : SessionParamsCreator {
|
||||
|
||||
override suspend fun create(
|
||||
credentials: Credentials,
|
||||
homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||
loginType: LoginType,
|
||||
) = SessionParams(
|
||||
credentials = credentials,
|
||||
homeServerConnectionConfig = homeServerConnectionConfig.overrideWithCredentials(credentials),
|
||||
isTokenValid = true,
|
||||
loginType = loginType,
|
||||
)
|
||||
|
||||
private suspend fun HomeServerConnectionConfig.overrideWithCredentials(credentials: Credentials) = copy(
|
||||
homeServerUriBase = credentials.getHomeServerUri(this) ?: homeServerUriBase,
|
||||
identityServerUri = credentials.getIdentityServerUri() ?: identityServerUri
|
||||
)
|
||||
|
||||
private suspend fun Credentials.getHomeServerUri(homeServerConnectionConfig: HomeServerConnectionConfig) =
|
||||
discoveryInformation?.homeServer?.baseURL
|
||||
?.trim { it == '/' }
|
||||
?.takeIf { it.isNotBlank() }
|
||||
// It can be the same value, so in this case, do not check again the validity
|
||||
?.takeIf { it != homeServerConnectionConfig.homeServerUriBase.toString() }
|
||||
?.also { Timber.d("Overriding homeserver url to $it (will check if valid)") }
|
||||
?.let { Uri.parse(it) }
|
||||
?.takeIf { validateUri(it, homeServerConnectionConfig) }
|
||||
|
||||
private suspend fun validateUri(uri: Uri, homeServerConnectionConfig: HomeServerConnectionConfig) =
|
||||
// Validate the URL, if the configuration is wrong server side, do not override
|
||||
tryOrNull {
|
||||
performClientServerApiValidation(uri, homeServerConnectionConfig)
|
||||
} ?: true // In case of other error (no network, etc.), consider it is valid...
|
||||
|
||||
private suspend fun performClientServerApiValidation(uri: Uri, homeServerConnectionConfig: HomeServerConnectionConfig) =
|
||||
isValidClientServerApiTask.execute(
|
||||
IsValidClientServerApiTask.Params(homeServerConnectionConfig.copy(homeServerUriBase = uri))
|
||||
).also { Timber.d("Overriding homeserver url: $it") }
|
||||
|
||||
private fun Credentials.getIdentityServerUri() = discoveryInformation?.identityServer?.baseURL
|
||||
?.trim { it == '/' }
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.also { Timber.d("Overriding identity server url to $it") }
|
||||
?.let { Uri.parse(it) }
|
||||
}
|
|
@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo001
|
|||
import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo002
|
||||
import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo003
|
||||
import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo004
|
||||
import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo005
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -33,7 +34,7 @@ internal class AuthRealmMigration @Inject constructor() : RealmMigration {
|
|||
override fun equals(other: Any?) = other is AuthRealmMigration
|
||||
override fun hashCode() = 4000
|
||||
|
||||
val schemaVersion = 4L
|
||||
val schemaVersion = 5L
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
|
||||
|
@ -42,5 +43,6 @@ internal class AuthRealmMigration @Inject constructor() : RealmMigration {
|
|||
if (oldVersion < 2) MigrateAuthTo002(realm).perform()
|
||||
if (oldVersion < 3) MigrateAuthTo003(realm).perform()
|
||||
if (oldVersion < 4) MigrateAuthTo004(realm).perform()
|
||||
if (oldVersion < 5) MigrateAuthTo005(realm).perform()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,5 +26,6 @@ internal open class SessionParamsEntity(
|
|||
var homeServerConnectionConfigJson: String = "",
|
||||
// Set to false when the token is invalid and the user has been soft logged out
|
||||
// In case of hard logout, this object is deleted from DB
|
||||
var isTokenValid: Boolean = true
|
||||
var isTokenValid: Boolean = true,
|
||||
var loginType: String = "",
|
||||
) : RealmObject()
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.matrix.android.sdk.internal.auth.db
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import org.matrix.android.sdk.api.auth.LoginType
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
||||
|
@ -37,7 +38,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
|
|||
if (credentials == null || homeServerConnectionConfig == null) {
|
||||
return null
|
||||
}
|
||||
return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid)
|
||||
return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid, LoginType.fromName(entity.loginType))
|
||||
}
|
||||
|
||||
fun map(sessionParams: SessionParams?): SessionParamsEntity? {
|
||||
|
@ -54,7 +55,8 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
|
|||
sessionParams.userId,
|
||||
credentialsJson,
|
||||
homeServerConnectionConfigJson,
|
||||
sessionParams.isTokenValid
|
||||
sessionParams.isTokenValid,
|
||||
sessionParams.loginType.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.auth.db.migration
|
||||
|
||||
import io.realm.DynamicRealm
|
||||
import org.matrix.android.sdk.api.auth.LoginType
|
||||
import org.matrix.android.sdk.internal.auth.db.SessionParamsEntityFields
|
||||
import org.matrix.android.sdk.internal.util.database.RealmMigrator
|
||||
import timber.log.Timber
|
||||
|
||||
internal class MigrateAuthTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) {
|
||||
|
||||
override fun doMigrate(realm: DynamicRealm) {
|
||||
Timber.d("Update SessionParamsEntity to add LoginType")
|
||||
|
||||
realm.schema.get("SessionParamsEntity")
|
||||
?.addField(SessionParamsEntityFields.LOGIN_TYPE, String::class.java)
|
||||
?.setRequired(SessionParamsEntityFields.LOGIN_TYPE, true)
|
||||
?.transform { it.set(SessionParamsEntityFields.LOGIN_TYPE, LoginType.UNKNOWN.name) }
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@
|
|||
package org.matrix.android.sdk.internal.auth.login
|
||||
|
||||
import android.util.Patterns
|
||||
import org.matrix.android.sdk.api.auth.LoginType
|
||||
import org.matrix.android.sdk.api.auth.login.LoginProfileInfo
|
||||
import org.matrix.android.sdk.api.auth.login.LoginWizard
|
||||
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
||||
|
@ -78,7 +79,7 @@ internal class DefaultLoginWizard(
|
|||
authAPI.login(loginParams)
|
||||
}
|
||||
|
||||
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
|
||||
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, LoginType.PASSWORD)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -92,7 +93,7 @@ internal class DefaultLoginWizard(
|
|||
authAPI.login(loginParams)
|
||||
}
|
||||
|
||||
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
|
||||
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, LoginType.SSO)
|
||||
}
|
||||
|
||||
override suspend fun loginCustom(data: JsonDict): Session {
|
||||
|
@ -100,7 +101,7 @@ internal class DefaultLoginWizard(
|
|||
authAPI.login(data)
|
||||
}
|
||||
|
||||
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
|
||||
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, LoginType.CUSTOM)
|
||||
}
|
||||
|
||||
override suspend fun resetPassword(email: String) {
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.auth.login
|
|||
|
||||
import dagger.Lazy
|
||||
import okhttp3.OkHttpClient
|
||||
import org.matrix.android.sdk.api.auth.LoginType
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
|
@ -77,7 +78,7 @@ internal class DefaultDirectLoginTask @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
return sessionCreator.createSession(credentials, params.homeServerConnectionConfig)
|
||||
return sessionCreator.createSession(credentials, params.homeServerConnectionConfig, LoginType.DIRECT)
|
||||
}
|
||||
|
||||
private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient {
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.matrix.android.sdk.internal.auth.registration
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import org.matrix.android.sdk.api.auth.LoginType
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
||||
|
@ -36,9 +37,9 @@ import org.matrix.android.sdk.internal.auth.db.PendingSessionData
|
|||
* This class execute the registration request and is responsible to keep the session of interactive authentication.
|
||||
*/
|
||||
internal class DefaultRegistrationWizard(
|
||||
authAPI: AuthAPI,
|
||||
private val sessionCreator: SessionCreator,
|
||||
private val pendingSessionStore: PendingSessionStore
|
||||
authAPI: AuthAPI,
|
||||
private val sessionCreator: SessionCreator,
|
||||
private val pendingSessionStore: PendingSessionStore
|
||||
) : RegistrationWizard {
|
||||
|
||||
private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
|
||||
|
@ -64,7 +65,7 @@ internal class DefaultRegistrationWizard(
|
|||
|
||||
override suspend fun getRegistrationFlow(): RegistrationResult {
|
||||
val params = RegistrationParams()
|
||||
return performRegistrationRequest(params)
|
||||
return performRegistrationRequest(params, LoginType.PASSWORD)
|
||||
}
|
||||
|
||||
override suspend fun createAccount(
|
||||
|
@ -73,43 +74,43 @@ internal class DefaultRegistrationWizard(
|
|||
initialDeviceDisplayName: String?
|
||||
): RegistrationResult {
|
||||
val params = RegistrationParams(
|
||||
username = userName,
|
||||
password = password,
|
||||
initialDeviceDisplayName = initialDeviceDisplayName
|
||||
username = userName,
|
||||
password = password,
|
||||
initialDeviceDisplayName = initialDeviceDisplayName
|
||||
)
|
||||
return performRegistrationRequest(params)
|
||||
.also {
|
||||
pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true)
|
||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||
}
|
||||
return performRegistrationRequest(params, LoginType.PASSWORD)
|
||||
.also {
|
||||
pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true)
|
||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun performReCaptcha(response: String): RegistrationResult {
|
||||
val safeSession = pendingSessionData.currentSession
|
||||
?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
|
||||
val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response))
|
||||
return performRegistrationRequest(params)
|
||||
return performRegistrationRequest(params, LoginType.PASSWORD)
|
||||
}
|
||||
|
||||
override suspend fun acceptTerms(): RegistrationResult {
|
||||
val safeSession = pendingSessionData.currentSession
|
||||
?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
|
||||
val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession))
|
||||
return performRegistrationRequest(params)
|
||||
return performRegistrationRequest(params, LoginType.PASSWORD)
|
||||
}
|
||||
|
||||
override suspend fun addThreePid(threePid: RegisterThreePid): RegistrationResult {
|
||||
pendingSessionData = pendingSessionData.copy(currentThreePidData = null)
|
||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||
|
||||
return sendThreePid(threePid)
|
||||
}
|
||||
|
||||
override suspend fun sendAgainThreePid(): RegistrationResult {
|
||||
val safeCurrentThreePid = pendingSessionData.currentThreePidData?.threePid
|
||||
?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
|
||||
return sendThreePid(safeCurrentThreePid)
|
||||
}
|
||||
|
@ -125,7 +126,7 @@ internal class DefaultRegistrationWizard(
|
|||
)
|
||||
|
||||
pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1)
|
||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||
|
||||
val params = RegistrationParams(
|
||||
auth = if (threePid is RegisterThreePid.Email) {
|
||||
|
@ -148,17 +149,17 @@ internal class DefaultRegistrationWizard(
|
|||
)
|
||||
// Store data
|
||||
pendingSessionData = pendingSessionData.copy(currentThreePidData = ThreePidData.from(threePid, response, params))
|
||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||
|
||||
// and send the sid a first time
|
||||
return performRegistrationRequest(params)
|
||||
return performRegistrationRequest(params, LoginType.PASSWORD)
|
||||
}
|
||||
|
||||
override suspend fun checkIfEmailHasBeenValidated(delayMillis: Long): RegistrationResult {
|
||||
val safeParam = pendingSessionData.currentThreePidData?.registrationParams
|
||||
?: throw IllegalStateException("developer error, no pending three pid")
|
||||
?: throw IllegalStateException("developer error, no pending three pid")
|
||||
|
||||
return performRegistrationRequest(safeParam, delayMillis)
|
||||
return performRegistrationRequest(safeParam, LoginType.PASSWORD, delayMillis)
|
||||
}
|
||||
|
||||
override suspend fun handleValidateThreePid(code: String): RegistrationResult {
|
||||
|
@ -167,19 +168,19 @@ internal class DefaultRegistrationWizard(
|
|||
|
||||
private suspend fun validateThreePid(code: String): RegistrationResult {
|
||||
val registrationParams = pendingSessionData.currentThreePidData?.registrationParams
|
||||
?: throw IllegalStateException("developer error, no pending three pid")
|
||||
?: throw IllegalStateException("developer error, no pending three pid")
|
||||
val safeCurrentData = pendingSessionData.currentThreePidData ?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url to send the code")
|
||||
val validationBody = ValidationCodeBody(
|
||||
clientSecret = pendingSessionData.clientSecret,
|
||||
sid = safeCurrentData.addThreePidRegistrationResponse.sid,
|
||||
code = code
|
||||
clientSecret = pendingSessionData.clientSecret,
|
||||
sid = safeCurrentData.addThreePidRegistrationResponse.sid,
|
||||
code = code
|
||||
)
|
||||
val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody))
|
||||
if (validationResponse.isSuccess()) {
|
||||
// The entered code is correct
|
||||
// Same than validate email
|
||||
return performRegistrationRequest(registrationParams, 3_000)
|
||||
return performRegistrationRequest(registrationParams, LoginType.PASSWORD, 3_000)
|
||||
} else {
|
||||
// The code is not correct
|
||||
throw Failure.SuccessError
|
||||
|
@ -188,10 +189,10 @@ internal class DefaultRegistrationWizard(
|
|||
|
||||
override suspend fun dummy(): RegistrationResult {
|
||||
val safeSession = pendingSessionData.currentSession
|
||||
?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
|
||||
val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession))
|
||||
return performRegistrationRequest(params)
|
||||
return performRegistrationRequest(params, LoginType.PASSWORD)
|
||||
}
|
||||
|
||||
override suspend fun registrationCustom(
|
||||
|
@ -204,25 +205,28 @@ internal class DefaultRegistrationWizard(
|
|||
mutableParams["session"] = safeSession
|
||||
|
||||
val params = RegistrationCustomParams(auth = mutableParams)
|
||||
return performRegistrationOtherRequest(params)
|
||||
return performRegistrationOtherRequest(LoginType.CUSTOM, params)
|
||||
}
|
||||
|
||||
private suspend fun performRegistrationRequest(
|
||||
registrationParams: RegistrationParams,
|
||||
loginType: LoginType,
|
||||
delayMillis: Long = 0
|
||||
): RegistrationResult {
|
||||
delay(delayMillis)
|
||||
return register { registerTask.execute(RegisterTask.Params(registrationParams)) }
|
||||
return register(loginType) { registerTask.execute(RegisterTask.Params(registrationParams)) }
|
||||
}
|
||||
|
||||
private suspend fun performRegistrationOtherRequest(
|
||||
registrationCustomParams: RegistrationCustomParams
|
||||
loginType: LoginType,
|
||||
registrationCustomParams: RegistrationCustomParams,
|
||||
): RegistrationResult {
|
||||
return register { registerCustomTask.execute(RegisterCustomTask.Params(registrationCustomParams)) }
|
||||
return register(loginType) { registerCustomTask.execute(RegisterCustomTask.Params(registrationCustomParams)) }
|
||||
}
|
||||
|
||||
private suspend fun register(
|
||||
execute: suspend () -> Credentials
|
||||
loginType: LoginType,
|
||||
execute: suspend () -> Credentials,
|
||||
): RegistrationResult {
|
||||
val credentials = try {
|
||||
execute.invoke()
|
||||
|
@ -237,8 +241,7 @@ internal class DefaultRegistrationWizard(
|
|||
}
|
||||
}
|
||||
|
||||
val session =
|
||||
sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
|
||||
val session = sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, loginType)
|
||||
return RegistrationResult.Success(session)
|
||||
}
|
||||
|
||||
|
|
|
@ -71,6 +71,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership
|
|||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||
import org.matrix.android.sdk.api.session.room.model.shouldShareHistory
|
||||
import org.matrix.android.sdk.api.session.sync.model.SyncResponse
|
||||
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
|
||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
||||
|
@ -81,6 +82,7 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFact
|
|||
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXKey.Companion.KEY_SIGNED_CURVE_25519_TYPE
|
||||
import org.matrix.android.sdk.internal.crypto.model.SessionInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.toRest
|
||||
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
|
@ -963,8 +965,12 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) {
|
||||
if (!event.isStateEvent()) return
|
||||
val eventContent = event.content.toModel<RoomHistoryVisibilityContent>()
|
||||
eventContent?.historyVisibility?.let {
|
||||
cryptoStore.setShouldEncryptForInvitedMembers(roomId, it != RoomHistoryVisibility.JOINED)
|
||||
val historyVisibility = eventContent?.historyVisibility
|
||||
if (historyVisibility == null) {
|
||||
cryptoStore.setShouldShareHistory(roomId, false)
|
||||
} else {
|
||||
cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED)
|
||||
cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1111,6 +1117,10 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
|
||||
override fun isKeyGossipingEnabled() = cryptoStore.isKeyGossipingEnabled()
|
||||
|
||||
override fun isShareKeysOnInviteEnabled() = cryptoStore.isShareKeysOnInviteEnabled()
|
||||
|
||||
override fun enableShareKeyOnInvite(enable: Boolean) = cryptoStore.enableShareKeyOnInvite(enable)
|
||||
|
||||
/**
|
||||
* Tells whether the client should ever send encrypted messages to unverified devices.
|
||||
* The default value is false.
|
||||
|
@ -1335,6 +1345,30 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun sendSharedHistoryKeys(roomId: String, userId: String, sessionInfoSet: Set<SessionInfo>?) {
|
||||
deviceListManager.downloadKeys(listOf(userId), false)
|
||||
val userDevices = cryptoStore.getUserDeviceList(userId)
|
||||
val sessionToShare = sessionInfoSet.orEmpty().mapNotNull { sessionInfo ->
|
||||
// Get inbound session from sessionId and sessionKey
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
olmDevice.getInboundGroupSession(
|
||||
sessionId = sessionInfo.sessionId,
|
||||
senderKey = sessionInfo.senderKey,
|
||||
roomId = roomId
|
||||
).takeIf { it.wrapper.sessionData.sharedHistory }
|
||||
}
|
||||
}
|
||||
|
||||
userDevices?.forEach { deviceInfo ->
|
||||
// Lets share the provided inbound sessions for every user device
|
||||
sessionToShare.forEach { inboundGroupSession ->
|
||||
val encryptor = roomEncryptorsStore.get(roomId)
|
||||
encryptor?.shareHistoryKeysWithDevice(inboundGroupSession, deviceInfo)
|
||||
Timber.i("## CRYPTO | Sharing inbound session")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* For test only
|
||||
* ========================================================================================== */
|
||||
|
|
|
@ -23,7 +23,7 @@ import kotlinx.coroutines.sync.Mutex
|
|||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import timber.log.Timber
|
||||
import java.util.Timer
|
||||
|
@ -31,7 +31,7 @@ import java.util.TimerTask
|
|||
import javax.inject.Inject
|
||||
|
||||
internal data class InboundGroupSessionHolder(
|
||||
val wrapper: OlmInboundGroupSessionWrapper2,
|
||||
val wrapper: MXInboundMegolmSessionWrapper,
|
||||
val mutex: Mutex = Mutex()
|
||||
)
|
||||
|
||||
|
@ -58,7 +58,7 @@ internal class InboundGroupSessionStore @Inject constructor(
|
|||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}")
|
||||
store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper })
|
||||
oldValue.wrapper.olmInboundGroupSession?.releaseSession()
|
||||
oldValue.wrapper.session.releaseSession()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ internal class InboundGroupSessionStore @Inject constructor(
|
|||
private val timer = Timer()
|
||||
private var timerTask: TimerTask? = null
|
||||
|
||||
private val dirtySession = mutableListOf<OlmInboundGroupSessionWrapper2>()
|
||||
private val dirtySession = mutableListOf<InboundGroupSessionHolder>()
|
||||
|
||||
@Synchronized
|
||||
fun clear() {
|
||||
|
@ -90,12 +90,12 @@ internal class InboundGroupSessionStore @Inject constructor(
|
|||
@Synchronized
|
||||
fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
|
||||
Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}")
|
||||
dirtySession.remove(old.wrapper)
|
||||
dirtySession.remove(old)
|
||||
store.removeInboundGroupSession(sessionId, senderKey)
|
||||
sessionCache.remove(CacheKey(sessionId, senderKey))
|
||||
|
||||
// release removed session
|
||||
old.wrapper.olmInboundGroupSession?.releaseSession()
|
||||
old.wrapper.session.releaseSession()
|
||||
|
||||
internalStoreGroupSession(new, sessionId, senderKey)
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ internal class InboundGroupSessionStore @Inject constructor(
|
|||
private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
|
||||
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession mark as dirty ${holder.wrapper.roomId}-${holder.wrapper.senderKey}")
|
||||
// We want to batch this a bit for performances
|
||||
dirtySession.add(holder.wrapper)
|
||||
dirtySession.add(holder)
|
||||
|
||||
if (sessionCache[CacheKey(sessionId, senderKey)] == null) {
|
||||
// first time seen, put it in memory cache while waiting for batch insert
|
||||
|
@ -127,12 +127,12 @@ internal class InboundGroupSessionStore @Inject constructor(
|
|||
|
||||
@Synchronized
|
||||
private fun batchSave() {
|
||||
val toSave = mutableListOf<OlmInboundGroupSessionWrapper2>().apply { addAll(dirtySession) }
|
||||
val toSave = mutableListOf<InboundGroupSessionHolder>().apply { addAll(dirtySession) }
|
||||
dirtySession.clear()
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession batching save of ${toSave.size}")
|
||||
tryOrNull {
|
||||
store.storeInboundGroupSessions(toSave)
|
||||
store.storeInboundGroupSessions(toSave.map { it.wrapper })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,8 @@ import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
|
|||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXOutboundSessionInfo
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.SharedWithHelper
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||
import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
|
@ -38,6 +39,7 @@ import org.matrix.android.sdk.internal.util.convertToUTF8
|
|||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
import org.matrix.olm.OlmAccount
|
||||
import org.matrix.olm.OlmException
|
||||
import org.matrix.olm.OlmInboundGroupSession
|
||||
import org.matrix.olm.OlmMessage
|
||||
import org.matrix.olm.OlmOutboundGroupSession
|
||||
import org.matrix.olm.OlmSession
|
||||
|
@ -514,8 +516,9 @@ internal class MXOlmDevice @Inject constructor(
|
|||
return MXOutboundSessionInfo(
|
||||
sessionId = sessionId,
|
||||
sharedWithHelper = SharedWithHelper(roomId, sessionId, store),
|
||||
clock,
|
||||
restoredOutboundGroupSession.creationTime
|
||||
clock = clock,
|
||||
creationTime = restoredOutboundGroupSession.creationTime,
|
||||
sharedHistory = restoredOutboundGroupSession.sharedHistory
|
||||
)
|
||||
}
|
||||
return null
|
||||
|
@ -598,40 +601,47 @@ internal class MXOlmDevice @Inject constructor(
|
|||
* @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us.
|
||||
* @param keysClaimed Other keys the sender claims.
|
||||
* @param exportFormat true if the megolm keys are in export format
|
||||
* @param sharedHistory MSC3061, this key is sharable on invite
|
||||
* @return true if the operation succeeds.
|
||||
*/
|
||||
fun addInboundGroupSession(
|
||||
sessionId: String,
|
||||
sessionKey: String,
|
||||
roomId: String,
|
||||
senderKey: String,
|
||||
forwardingCurve25519KeyChain: List<String>,
|
||||
keysClaimed: Map<String, String>,
|
||||
exportFormat: Boolean
|
||||
): AddSessionResult {
|
||||
val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
|
||||
fun addInboundGroupSession(sessionId: String,
|
||||
sessionKey: String,
|
||||
roomId: String,
|
||||
senderKey: String,
|
||||
forwardingCurve25519KeyChain: List<String>,
|
||||
keysClaimed: Map<String, String>,
|
||||
exportFormat: Boolean,
|
||||
sharedHistory: Boolean): AddSessionResult {
|
||||
val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") {
|
||||
if (exportFormat) {
|
||||
OlmInboundGroupSession.importSession(sessionKey)
|
||||
} else {
|
||||
OlmInboundGroupSession(sessionKey)
|
||||
}
|
||||
}
|
||||
|
||||
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
|
||||
val existingSession = existingSessionHolder?.wrapper
|
||||
// If we have an existing one we should check if the new one is not better
|
||||
if (existingSession != null) {
|
||||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session")
|
||||
try {
|
||||
val existingFirstKnown = existingSession.firstKnownIndex ?: return AddSessionResult.NotImported.also {
|
||||
val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex } ?: return AddSessionResult.NotImported.also {
|
||||
// This is quite unexpected, could throw if native was released?
|
||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
|
||||
candidateSession.olmInboundGroupSession?.releaseSession()
|
||||
candidateSession?.releaseSession()
|
||||
// Probably should discard it?
|
||||
}
|
||||
val newKnownFirstIndex = candidateSession.firstKnownIndex
|
||||
val newKnownFirstIndex = tryOrNull("Failed to get candidate first known index") { candidateSession?.firstKnownIndex }
|
||||
// If our existing session is better we keep it
|
||||
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
|
||||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
|
||||
candidateSession.olmInboundGroupSession?.releaseSession()
|
||||
candidateSession?.releaseSession()
|
||||
return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt())
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
|
||||
candidateSession.olmInboundGroupSession?.releaseSession()
|
||||
candidateSession?.releaseSession()
|
||||
return AddSessionResult.NotImported
|
||||
}
|
||||
}
|
||||
|
@ -639,36 +649,42 @@ internal class MXOlmDevice @Inject constructor(
|
|||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId")
|
||||
|
||||
// sanity check on the new session
|
||||
val candidateOlmInboundSession = candidateSession.olmInboundGroupSession
|
||||
if (null == candidateOlmInboundSession) {
|
||||
if (null == candidateSession) {
|
||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>")
|
||||
return AddSessionResult.NotImported
|
||||
}
|
||||
|
||||
try {
|
||||
if (candidateOlmInboundSession.sessionIdentifier() != sessionId) {
|
||||
if (candidateSession.sessionIdentifier() != sessionId) {
|
||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
||||
candidateOlmInboundSession.releaseSession()
|
||||
candidateSession.releaseSession()
|
||||
return AddSessionResult.NotImported
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
candidateOlmInboundSession.releaseSession()
|
||||
candidateSession.releaseSession()
|
||||
Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed")
|
||||
return AddSessionResult.NotImported
|
||||
}
|
||||
|
||||
candidateSession.senderKey = senderKey
|
||||
candidateSession.roomId = roomId
|
||||
candidateSession.keysClaimed = keysClaimed
|
||||
candidateSession.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain
|
||||
val candidateSessionData = InboundGroupSessionData(
|
||||
senderKey = senderKey,
|
||||
roomId = roomId,
|
||||
keysClaimed = keysClaimed,
|
||||
forwardingCurve25519KeyChain = forwardingCurve25519KeyChain,
|
||||
sharedHistory = sharedHistory,
|
||||
)
|
||||
|
||||
val wrapper = MXInboundMegolmSessionWrapper(
|
||||
candidateSession,
|
||||
candidateSessionData
|
||||
)
|
||||
if (existingSession != null) {
|
||||
inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
|
||||
inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(wrapper), sessionId, senderKey)
|
||||
} else {
|
||||
inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
|
||||
inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(wrapper), sessionId, senderKey)
|
||||
}
|
||||
|
||||
return AddSessionResult.Imported(candidateSession.firstKnownIndex?.toInt() ?: 0)
|
||||
return AddSessionResult.Imported(candidateSession.firstKnownIndex.toInt())
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -677,41 +693,22 @@ internal class MXOlmDevice @Inject constructor(
|
|||
* @param megolmSessionsData the megolm sessions data
|
||||
* @return the successfully imported sessions.
|
||||
*/
|
||||
fun importInboundGroupSessions(megolmSessionsData: List<MegolmSessionData>): List<OlmInboundGroupSessionWrapper2> {
|
||||
val sessions = ArrayList<OlmInboundGroupSessionWrapper2>(megolmSessionsData.size)
|
||||
fun importInboundGroupSessions(megolmSessionsData: List<MegolmSessionData>): List<MXInboundMegolmSessionWrapper> {
|
||||
val sessions = ArrayList<MXInboundMegolmSessionWrapper>(megolmSessionsData.size)
|
||||
|
||||
for (megolmSessionData in megolmSessionsData) {
|
||||
val sessionId = megolmSessionData.sessionId ?: continue
|
||||
val senderKey = megolmSessionData.senderKey ?: continue
|
||||
val roomId = megolmSessionData.roomId
|
||||
|
||||
var candidateSessionToImport: OlmInboundGroupSessionWrapper2? = null
|
||||
|
||||
try {
|
||||
candidateSessionToImport = OlmInboundGroupSessionWrapper2(megolmSessionData)
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
||||
}
|
||||
|
||||
// sanity check
|
||||
if (candidateSessionToImport?.olmInboundGroupSession == null) {
|
||||
Timber.tag(loggerTag.value).e("## importInboundGroupSession : invalid session")
|
||||
continue
|
||||
}
|
||||
|
||||
val candidateOlmInboundGroupSession = candidateSessionToImport.olmInboundGroupSession
|
||||
try {
|
||||
if (candidateOlmInboundGroupSession?.sessionIdentifier() != sessionId) {
|
||||
Timber.tag(loggerTag.value).e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
||||
candidateOlmInboundGroupSession?.releaseSession()
|
||||
continue
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession : sessionIdentifier() failed")
|
||||
candidateOlmInboundGroupSession?.releaseSession()
|
||||
val candidateSessionToImport = try {
|
||||
MXInboundMegolmSessionWrapper.newFromMegolmData(megolmSessionData, true)
|
||||
} catch (e: Throwable) {
|
||||
Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Failed to import session $senderKey/$sessionId")
|
||||
continue
|
||||
}
|
||||
|
||||
val candidateOlmInboundGroupSession = candidateSessionToImport.session
|
||||
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
|
||||
val existingSession = existingSessionHolder?.wrapper
|
||||
|
||||
|
@ -721,16 +718,16 @@ internal class MXOlmDevice @Inject constructor(
|
|||
sessions.add(candidateSessionToImport)
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
||||
val existingFirstKnown = tryOrNull { existingSession.firstKnownIndex }
|
||||
val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.firstKnownIndex }
|
||||
val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex }
|
||||
val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.session.firstKnownIndex }
|
||||
|
||||
if (existingFirstKnown == null || candidateFirstKnownIndex == null) {
|
||||
// should not happen?
|
||||
candidateSessionToImport.olmInboundGroupSession?.releaseSession()
|
||||
candidateSessionToImport.session.releaseSession()
|
||||
Timber.tag(loggerTag.value)
|
||||
.w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex")
|
||||
} else {
|
||||
if (existingFirstKnown <= candidateSessionToImport.firstKnownIndex!!) {
|
||||
if (existingFirstKnown <= candidateFirstKnownIndex) {
|
||||
// Ignore this, keep existing
|
||||
candidateOlmInboundGroupSession.releaseSession()
|
||||
} else {
|
||||
|
@ -774,8 +771,7 @@ internal class MXOlmDevice @Inject constructor(
|
|||
): OlmDecryptionResult {
|
||||
val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId)
|
||||
val wrapper = sessionHolder.wrapper
|
||||
val inboundGroupSession = wrapper.olmInboundGroupSession
|
||||
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null")
|
||||
val inboundGroupSession = wrapper.session
|
||||
if (roomId != wrapper.roomId) {
|
||||
// Check that the room id matches the original one for the session. This stops
|
||||
// the HS pretending a message was targeting a different room.
|
||||
|
@ -822,9 +818,9 @@ internal class MXOlmDevice @Inject constructor(
|
|||
|
||||
return OlmDecryptionResult(
|
||||
payload,
|
||||
wrapper.keysClaimed,
|
||||
wrapper.sessionData.keysClaimed,
|
||||
senderKey,
|
||||
wrapper.forwardingCurve25519KeyChain
|
||||
wrapper.sessionData.forwardingCurve25519KeyChain
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -69,5 +69,13 @@ internal data class MegolmSessionData(
|
|||
* Devices which forwarded this session to us (normally empty).
|
||||
*/
|
||||
@Json(name = "forwarding_curve25519_key_chain")
|
||||
val forwardingCurve25519KeyChain: List<String>? = null
|
||||
val forwardingCurve25519KeyChain: List<String>? = null,
|
||||
|
||||
/**
|
||||
* Flag that indicates whether or not the current inboundSession will be shared to
|
||||
* invited users to decrypt past messages.
|
||||
*/
|
||||
// When this feature lands in spec name = shared_history should be used
|
||||
@Json(name = "org.matrix.msc3061.shared_history")
|
||||
val sharedHistory: Boolean = false,
|
||||
)
|
||||
|
|
|
@ -437,7 +437,10 @@ internal class OutgoingKeyRequestManager @Inject constructor(
|
|||
if (perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)) {
|
||||
// let's see what's the index
|
||||
val knownIndex = tryOrNull {
|
||||
inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "")?.wrapper?.firstKnownIndex
|
||||
inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "")
|
||||
?.wrapper
|
||||
?.session
|
||||
?.firstKnownIndex
|
||||
}
|
||||
if (knownIndex != null && knownIndex <= request.fromIndex) {
|
||||
// we found the key in backup with good enough index, so we can just mark as cancelled, no need to send request
|
||||
|
|
|
@ -84,8 +84,9 @@ internal class MegolmSessionDataImporter @Inject constructor(
|
|||
megolmSessionData.senderKey ?: "",
|
||||
tryOrNull {
|
||||
olmInboundGroupSessionWrappers
|
||||
.firstOrNull { it.olmInboundGroupSession?.sessionIdentifier() == megolmSessionData.sessionId }
|
||||
?.firstKnownIndex?.toInt()
|
||||
.firstOrNull { it.session.sessionIdentifier() == megolmSessionData.sessionId }
|
||||
?.session?.firstKnownIndex
|
||||
?.toInt()
|
||||
} ?: 0
|
||||
)
|
||||
|
||||
|
|
|
@ -16,7 +16,9 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.crypto.algorithms
|
||||
|
||||
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder
|
||||
|
||||
/**
|
||||
* An interface for encrypting data.
|
||||
|
@ -32,4 +34,6 @@ internal interface IMXEncrypting {
|
|||
* @return the encrypted content
|
||||
*/
|
||||
suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content
|
||||
|
||||
suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) {}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||
|
||||
import dagger.Lazy
|
||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
||||
|
@ -41,6 +42,7 @@ internal class MXMegolmDecryption(
|
|||
private val olmDevice: MXOlmDevice,
|
||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val matrixConfiguration: MatrixConfiguration,
|
||||
private val liveEventManager: Lazy<StreamEventsManager>
|
||||
) : IMXDecrypting {
|
||||
|
||||
|
@ -240,13 +242,14 @@ internal class MXMegolmDecryption(
|
|||
|
||||
Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
|
||||
val addSessionResult = olmDevice.addInboundGroupSession(
|
||||
roomKeyContent.sessionId,
|
||||
roomKeyContent.sessionKey,
|
||||
roomKeyContent.roomId,
|
||||
senderKey,
|
||||
forwardingCurve25519KeyChain,
|
||||
keysClaimed,
|
||||
exportFormat
|
||||
sessionId = roomKeyContent.sessionId,
|
||||
sessionKey = roomKeyContent.sessionKey,
|
||||
roomId = roomKeyContent.roomId,
|
||||
senderKey = senderKey,
|
||||
forwardingCurve25519KeyChain = forwardingCurve25519KeyChain,
|
||||
keysClaimed = keysClaimed,
|
||||
exportFormat = exportFormat,
|
||||
sharedHistory = roomKeyContent.getSharedKey()
|
||||
)
|
||||
|
||||
when (addSessionResult) {
|
||||
|
@ -296,6 +299,14 @@ internal class MXMegolmDecryption(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns boolean shared key flag, if enabled with respect to matrix configuration.
|
||||
*/
|
||||
private fun RoomKeyContent.getSharedKey(): Boolean {
|
||||
if (!cryptoStore.isShareKeysOnInviteEnabled()) return false
|
||||
return sharedHistory ?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the some messages can be decrypted with a new session.
|
||||
*
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||
|
||||
import dagger.Lazy
|
||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
|
@ -27,6 +28,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
|
|||
private val olmDevice: MXOlmDevice,
|
||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val matrixConfiguration: MatrixConfiguration,
|
||||
private val eventsManager: Lazy<StreamEventsManager>
|
||||
) {
|
||||
|
||||
|
@ -35,7 +37,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
|
|||
olmDevice,
|
||||
outgoingKeyRequestManager,
|
||||
cryptoStore,
|
||||
eventsManager
|
||||
)
|
||||
matrixConfiguration,
|
||||
eventsManager)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType
|
|||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||
import org.matrix.android.sdk.internal.crypto.DeviceListManager
|
||||
import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder
|
||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
||||
|
@ -151,14 +152,27 @@ internal class MXMegolmEncryption(
|
|||
"ed25519" to olmDevice.deviceEd25519Key!!
|
||||
)
|
||||
|
||||
val sharedHistory = cryptoStore.shouldShareHistory(roomId)
|
||||
Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() as sharedHistory $sharedHistory")
|
||||
olmDevice.addInboundGroupSession(
|
||||
sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!,
|
||||
emptyList(), keysClaimedMap, false
|
||||
sessionId = sessionId!!,
|
||||
sessionKey = olmDevice.getSessionKey(sessionId)!!,
|
||||
roomId = roomId,
|
||||
senderKey = olmDevice.deviceCurve25519Key!!,
|
||||
forwardingCurve25519KeyChain = emptyList(),
|
||||
keysClaimed = keysClaimedMap,
|
||||
exportFormat = false,
|
||||
sharedHistory = sharedHistory
|
||||
)
|
||||
|
||||
defaultKeysBackupService.maybeBackupKeys()
|
||||
|
||||
return MXOutboundSessionInfo(sessionId, SharedWithHelper(roomId, sessionId, cryptoStore), clock)
|
||||
return MXOutboundSessionInfo(
|
||||
sessionId = sessionId,
|
||||
sharedWithHelper = SharedWithHelper(roomId, sessionId, cryptoStore),
|
||||
clock = clock,
|
||||
sharedHistory = sharedHistory
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -172,6 +186,8 @@ internal class MXMegolmEncryption(
|
|||
if (session == null ||
|
||||
// Need to make a brand new session?
|
||||
session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) ||
|
||||
// Is there a room history visibility change since the last outboundSession
|
||||
cryptoStore.shouldShareHistory(roomId) != session.sharedHistory ||
|
||||
// Determine if we have shared with anyone we shouldn't have
|
||||
session.sharedWithTooManyDevices(devicesInRoom)) {
|
||||
Timber.tag(loggerTag.value).d("roomId:$roomId Starting new megolm session because we need to rotate.")
|
||||
|
@ -231,26 +247,27 @@ internal class MXMegolmEncryption(
|
|||
/**
|
||||
* Share the device keys of a an user.
|
||||
*
|
||||
* @param session the session info
|
||||
* @param sessionInfo the session info
|
||||
* @param devicesByUser the devices map
|
||||
*/
|
||||
private suspend fun shareUserDevicesKey(
|
||||
session: MXOutboundSessionInfo,
|
||||
devicesByUser: Map<String, List<CryptoDeviceInfo>>
|
||||
) {
|
||||
val sessionKey = olmDevice.getSessionKey(session.sessionId)
|
||||
val chainIndex = olmDevice.getMessageIndex(session.sessionId)
|
||||
private suspend fun shareUserDevicesKey(sessionInfo: MXOutboundSessionInfo,
|
||||
devicesByUser: Map<String, List<CryptoDeviceInfo>>) {
|
||||
val sessionKey = olmDevice.getSessionKey(sessionInfo.sessionId) ?: return Unit.also {
|
||||
Timber.tag(loggerTag.value).v("shareUserDevicesKey() Failed to share session, failed to export")
|
||||
}
|
||||
val chainIndex = olmDevice.getMessageIndex(sessionInfo.sessionId)
|
||||
|
||||
val submap = HashMap<String, Any>()
|
||||
submap["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM
|
||||
submap["room_id"] = roomId
|
||||
submap["session_id"] = session.sessionId
|
||||
submap["session_key"] = sessionKey!!
|
||||
submap["chain_index"] = chainIndex
|
||||
|
||||
val payload = HashMap<String, Any>()
|
||||
payload["type"] = EventType.ROOM_KEY
|
||||
payload["content"] = submap
|
||||
val payload = mapOf(
|
||||
"type" to EventType.ROOM_KEY,
|
||||
"content" to mapOf(
|
||||
"algorithm" to MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
"room_id" to roomId,
|
||||
"session_id" to sessionInfo.sessionId,
|
||||
"session_key" to sessionKey,
|
||||
"chain_index" to chainIndex,
|
||||
"org.matrix.msc3061.shared_history" to sessionInfo.sharedHistory
|
||||
)
|
||||
)
|
||||
|
||||
var t0 = clock.epochMillis()
|
||||
Timber.tag(loggerTag.value).v("shareUserDevicesKey() : starts")
|
||||
|
@ -292,7 +309,7 @@ internal class MXMegolmEncryption(
|
|||
// for dead devices on every message.
|
||||
for ((_, devicesToShareWith) in devicesByUser) {
|
||||
for (deviceInfo in devicesToShareWith) {
|
||||
session.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex)
|
||||
sessionInfo.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex)
|
||||
// XXX is it needed to add it to the audit trail?
|
||||
// For now decided that no, we are more interested by forward trail
|
||||
}
|
||||
|
@ -300,8 +317,8 @@ internal class MXMegolmEncryption(
|
|||
|
||||
if (haveTargets) {
|
||||
t0 = clock.epochMillis()
|
||||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target")
|
||||
Timber.tag(loggerTag.value).d("sending to device room key for ${session.sessionId} to ${contentMap.toDebugString()}")
|
||||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${sessionInfo.sessionId} : has target")
|
||||
Timber.tag(loggerTag.value).d("sending to device room key for ${sessionInfo.sessionId} to ${contentMap.toDebugString()}")
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
|
||||
try {
|
||||
withContext(coroutineDispatchers.io) {
|
||||
|
@ -310,7 +327,7 @@ internal class MXMegolmEncryption(
|
|||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${clock.epochMillis() - t0} ms")
|
||||
} catch (failure: Throwable) {
|
||||
// What to do here...
|
||||
Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${session.sessionId}>")
|
||||
Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${sessionInfo.sessionId}>")
|
||||
}
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key")
|
||||
|
@ -320,7 +337,7 @@ internal class MXMegolmEncryption(
|
|||
// XXX offload?, as they won't read the message anyhow?
|
||||
notifyKeyWithHeld(
|
||||
noOlmToNotify,
|
||||
session.sessionId,
|
||||
sessionInfo.sessionId,
|
||||
olmDevice.deviceCurve25519Key,
|
||||
WithHeldCode.NO_OLM
|
||||
)
|
||||
|
@ -514,6 +531,51 @@ internal class MXMegolmEncryption(
|
|||
}
|
||||
}
|
||||
|
||||
@Throws
|
||||
override suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) {
|
||||
if (!inboundSessionWrapper.wrapper.sessionData.sharedHistory) throw IllegalArgumentException("This key can't be shared")
|
||||
Timber.tag(loggerTag.value).i("process shareHistoryKeys for ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}")
|
||||
val userId = deviceInfo.userId
|
||||
val deviceId = deviceInfo.deviceId
|
||||
val devicesByUser = mapOf(userId to listOf(deviceInfo))
|
||||
val usersDeviceMap = try {
|
||||
ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.tag(loggerTag.value).i(failure, "process shareHistoryKeys failed to ensure olm")
|
||||
// process anyway?
|
||||
null
|
||||
}
|
||||
val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId)
|
||||
if (olmSessionResult?.sessionId == null) {
|
||||
Timber.tag(loggerTag.value).w("shareHistoryKeys: no session with this device, probably because there were no one-time keys")
|
||||
return
|
||||
}
|
||||
|
||||
val export = inboundSessionWrapper.mutex.withLock {
|
||||
inboundSessionWrapper.wrapper.exportKeys()
|
||||
} ?: return Unit.also {
|
||||
Timber.tag(loggerTag.value).e("shareHistoryKeys: failed to export group session ${inboundSessionWrapper.wrapper.safeSessionId}")
|
||||
}
|
||||
|
||||
val payloadJson = mapOf(
|
||||
"type" to EventType.FORWARDED_ROOM_KEY,
|
||||
"content" to export
|
||||
)
|
||||
|
||||
val encodedPayload =
|
||||
withContext(coroutineDispatchers.computation) {
|
||||
messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||
}
|
||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
||||
Timber.tag(loggerTag.value)
|
||||
.d("shareHistoryKeys() : sending session ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}")
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||
withContext(coroutineDispatchers.io) {
|
||||
sendToDeviceTask.execute(sendToDeviceParams)
|
||||
}
|
||||
}
|
||||
|
||||
data class DeviceInRoomInfo(
|
||||
val allowedDevices: MXUsersDevicesMap<CryptoDeviceInfo> = MXUsersDevicesMap(),
|
||||
val withHeldDevices: MXUsersDevicesMap<WithHeldCode> = MXUsersDevicesMap()
|
||||
|
|
|
@ -28,6 +28,7 @@ internal class MXOutboundSessionInfo(
|
|||
private val clock: Clock,
|
||||
// When the session was created
|
||||
private val creationTime: Long = clock.epochMillis(),
|
||||
val sharedHistory: Boolean = false
|
||||
) {
|
||||
|
||||
// Number of times this session has been used
|
||||
|
|
|
@ -24,8 +24,10 @@ import androidx.annotation.WorkerThread
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||
|
@ -50,6 +52,7 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
|
|||
import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
|
||||
import org.matrix.android.sdk.api.util.awaitCallback
|
||||
import org.matrix.android.sdk.api.util.fromBase64
|
||||
import org.matrix.android.sdk.internal.crypto.InboundGroupSessionStore
|
||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
|
||||
import org.matrix.android.sdk.internal.crypto.ObjectSigner
|
||||
|
@ -71,7 +74,7 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDa
|
|||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask
|
||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
|
@ -118,6 +121,8 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
|
||||
// Task executor
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val matrixConfiguration: MatrixConfiguration,
|
||||
private val inboundGroupSessionStore: InboundGroupSessionStore,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoCoroutineScope: CoroutineScope
|
||||
) : KeysBackupService {
|
||||
|
@ -1316,7 +1321,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
|
||||
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
|
||||
val roomId = olmInboundGroupSessionWrapper.roomId ?: return@forEach
|
||||
val olmInboundGroupSession = olmInboundGroupSessionWrapper.olmInboundGroupSession ?: return@forEach
|
||||
val olmInboundGroupSession = olmInboundGroupSessionWrapper.session
|
||||
|
||||
try {
|
||||
encryptGroupSession(olmInboundGroupSessionWrapper)
|
||||
|
@ -1405,19 +1410,29 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
|
||||
@VisibleForTesting
|
||||
@WorkerThread
|
||||
fun encryptGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2): KeyBackupData? {
|
||||
suspend fun encryptGroupSession(olmInboundGroupSessionWrapper: MXInboundMegolmSessionWrapper): KeyBackupData? {
|
||||
olmInboundGroupSessionWrapper.safeSessionId ?: return null
|
||||
olmInboundGroupSessionWrapper.senderKey ?: return null
|
||||
// Gather information for each key
|
||||
val device = olmInboundGroupSessionWrapper.senderKey?.let { cryptoStore.deviceWithIdentityKey(it) }
|
||||
val device = cryptoStore.deviceWithIdentityKey(olmInboundGroupSessionWrapper.senderKey)
|
||||
|
||||
// Build the m.megolm_backup.v1.curve25519-aes-sha2 data as defined at
|
||||
// https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md#mmegolm_backupv1curve25519-aes-sha2-key-format
|
||||
val sessionData = olmInboundGroupSessionWrapper.exportKeys() ?: return null
|
||||
val sessionData = inboundGroupSessionStore
|
||||
.getInboundGroupSession(olmInboundGroupSessionWrapper.safeSessionId, olmInboundGroupSessionWrapper.senderKey)
|
||||
?.let {
|
||||
withContext(coroutineDispatchers.computation) {
|
||||
it.mutex.withLock { it.wrapper.exportKeys() }
|
||||
}
|
||||
}
|
||||
?: return null
|
||||
val sessionBackupData = mapOf(
|
||||
"algorithm" to sessionData.algorithm,
|
||||
"sender_key" to sessionData.senderKey,
|
||||
"sender_claimed_keys" to sessionData.senderClaimedKeys,
|
||||
"forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain.orEmpty()),
|
||||
"session_key" to sessionData.sessionKey
|
||||
"session_key" to sessionData.sessionKey,
|
||||
"org.matrix.msc3061.shared_history" to sessionData.sharedHistory
|
||||
)
|
||||
|
||||
val json = MoshiProvider.providesMoshi()
|
||||
|
@ -1425,7 +1440,9 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
.toJson(sessionBackupData)
|
||||
|
||||
val encryptedSessionBackupData = try {
|
||||
backupOlmPkEncryption?.encrypt(json)
|
||||
withContext(coroutineDispatchers.computation) {
|
||||
backupOlmPkEncryption?.encrypt(json)
|
||||
}
|
||||
} catch (e: OlmException) {
|
||||
Timber.e(e, "OlmException")
|
||||
null
|
||||
|
@ -1435,14 +1452,14 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
// Build backup data for that key
|
||||
return KeyBackupData(
|
||||
firstMessageIndex = try {
|
||||
olmInboundGroupSessionWrapper.olmInboundGroupSession?.firstKnownIndex ?: 0
|
||||
olmInboundGroupSessionWrapper.session.firstKnownIndex
|
||||
} catch (e: OlmException) {
|
||||
Timber.e(e, "OlmException")
|
||||
0L
|
||||
},
|
||||
forwardedCount = olmInboundGroupSessionWrapper.forwardingCurve25519KeyChain.orEmpty().size,
|
||||
forwardedCount = olmInboundGroupSessionWrapper.sessionData.forwardingCurve25519KeyChain.orEmpty().size,
|
||||
isVerified = device?.isVerified == true,
|
||||
|
||||
sharedHistory = olmInboundGroupSessionWrapper.getSharedKey(),
|
||||
sessionData = mapOf(
|
||||
"ciphertext" to encryptedSessionBackupData.mCipherText,
|
||||
"mac" to encryptedSessionBackupData.mMac,
|
||||
|
@ -1451,6 +1468,14 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns boolean shared key flag, if enabled with respect to matrix configuration.
|
||||
*/
|
||||
private fun MXInboundMegolmSessionWrapper.getSharedKey(): Boolean {
|
||||
if (!cryptoStore.isShareKeysOnInviteEnabled()) return false
|
||||
return sessionData.sharedHistory
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@WorkerThread
|
||||
fun decryptKeyBackupData(keyBackupData: KeyBackupData, sessionId: String, roomId: String, decryption: OlmPkDecryption): MegolmSessionData? {
|
||||
|
|
|
@ -50,5 +50,12 @@ internal data class KeyBackupData(
|
|||
* Algorithm-dependent data.
|
||||
*/
|
||||
@Json(name = "session_data")
|
||||
val sessionData: JsonDict
|
||||
val sessionData: JsonDict,
|
||||
|
||||
/**
|
||||
* Flag that indicates whether or not the current inboundSession will be shared to
|
||||
* invited users to decrypt past messages.
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3061.shared_history")
|
||||
val sharedHistory: Boolean = false
|
||||
)
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class InboundGroupSessionData(
|
||||
|
||||
/** The room in which this session is used. */
|
||||
@Json(name = "room_id")
|
||||
var roomId: String? = null,
|
||||
|
||||
/** The base64-encoded curve25519 key of the sender. */
|
||||
@Json(name = "sender_key")
|
||||
var senderKey: String? = null,
|
||||
|
||||
/** Other keys the sender claims. */
|
||||
@Json(name = "keys_claimed")
|
||||
var keysClaimed: Map<String, String>? = null,
|
||||
|
||||
/** Devices which forwarded this session to us (normally emty). */
|
||||
@Json(name = "forwarding_curve25519_key_chain")
|
||||
var forwardingCurve25519KeyChain: List<String>? = emptyList(),
|
||||
|
||||
/** Not yet used, will be in backup v2
|
||||
val untrusted?: Boolean = false */
|
||||
|
||||
/**
|
||||
* Flag that indicates whether or not the current inboundSession will be shared to
|
||||
* invited users to decrypt past messages.
|
||||
*/
|
||||
@Json(name = "shared_history")
|
||||
val sharedHistory: Boolean = false,
|
||||
|
||||
)
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto.model
|
||||
|
||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
|
||||
import org.matrix.olm.OlmInboundGroupSession
|
||||
import timber.log.Timber
|
||||
|
||||
data class MXInboundMegolmSessionWrapper(
|
||||
// olm object
|
||||
val session: OlmInboundGroupSession,
|
||||
// data about the session
|
||||
val sessionData: InboundGroupSessionData
|
||||
) {
|
||||
// shortcut
|
||||
val roomId = sessionData.roomId
|
||||
val senderKey = sessionData.senderKey
|
||||
val safeSessionId = tryOrNull("Fail to get megolm session Id") { session.sessionIdentifier() }
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
internal fun exportKeys(index: Long? = null): MegolmSessionData? {
|
||||
return try {
|
||||
val keysClaimed = sessionData.keysClaimed ?: return null
|
||||
val wantedIndex = index ?: session.firstKnownIndex
|
||||
|
||||
MegolmSessionData(
|
||||
senderClaimedEd25519Key = sessionData.keysClaimed?.get("ed25519"),
|
||||
forwardingCurve25519KeyChain = sessionData.forwardingCurve25519KeyChain?.toList().orEmpty(),
|
||||
sessionKey = session.export(wantedIndex),
|
||||
senderClaimedKeys = keysClaimed,
|
||||
roomId = sessionData.roomId,
|
||||
sessionId = session.sessionIdentifier(),
|
||||
senderKey = senderKey,
|
||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
sharedHistory = sessionData.sharedHistory
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## Failed to export megolm : sessionID ${tryOrNull { session.sessionIdentifier() }} failed")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* @exportFormat true if the megolm keys are in export format
|
||||
* (ie, they lack an ed25519 signature)
|
||||
*/
|
||||
@Throws
|
||||
internal fun newFromMegolmData(megolmSessionData: MegolmSessionData, exportFormat: Boolean): MXInboundMegolmSessionWrapper {
|
||||
val exportedKey = megolmSessionData.sessionKey ?: throw IllegalArgumentException("key data not found")
|
||||
val inboundSession = if (exportFormat) {
|
||||
OlmInboundGroupSession.importSession(exportedKey)
|
||||
} else {
|
||||
OlmInboundGroupSession(exportedKey)
|
||||
}
|
||||
.also {
|
||||
if (it.sessionIdentifier() != megolmSessionData.sessionId) {
|
||||
it.releaseSession()
|
||||
throw IllegalStateException("Mismatched group session Id")
|
||||
}
|
||||
}
|
||||
val data = InboundGroupSessionData(
|
||||
roomId = megolmSessionData.roomId,
|
||||
senderKey = megolmSessionData.senderKey,
|
||||
keysClaimed = megolmSessionData.senderClaimedKeys,
|
||||
forwardingCurve25519KeyChain = megolmSessionData.forwardingCurve25519KeyChain,
|
||||
sharedHistory = megolmSessionData.sharedHistory,
|
||||
)
|
||||
|
||||
return MXInboundMegolmSessionWrapper(
|
||||
inboundSession,
|
||||
data
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,6 +26,8 @@ import java.io.Serializable
|
|||
* This class adds more context to a OlmInboundGroupSession object.
|
||||
* This allows additional checks. The class implements Serializable so that the context can be stored.
|
||||
*/
|
||||
// Note used anymore, just for database migration
|
||||
// Deprecated("Use MXInboundMegolmSessionWrapper")
|
||||
internal class OlmInboundGroupSessionWrapper2 : Serializable {
|
||||
|
||||
// The associated olm inbound group session.
|
||||
|
|
|
@ -20,5 +20,9 @@ import org.matrix.olm.OlmOutboundGroupSession
|
|||
|
||||
internal data class OutboundGroupSessionWrapper(
|
||||
val outboundGroupSession: OlmOutboundGroupSession,
|
||||
val creationTime: Long
|
||||
val creationTime: Long,
|
||||
/**
|
||||
* As per MSC 3061, declares if this key could be shared when inviting a new user to the room.
|
||||
*/
|
||||
val sharedHistory: Boolean = false
|
||||
)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto.model
|
||||
|
||||
data class SessionInfo(
|
||||
val sessionId: String,
|
||||
val senderKey: String
|
||||
)
|
|
@ -35,7 +35,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
|
|||
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
|
||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||
|
@ -64,7 +64,15 @@ internal interface IMXCryptoStore {
|
|||
*
|
||||
* @return the list of all known group sessions, to export them.
|
||||
*/
|
||||
fun getInboundGroupSessions(): List<OlmInboundGroupSessionWrapper2>
|
||||
fun getInboundGroupSessions(): List<MXInboundMegolmSessionWrapper>
|
||||
|
||||
/**
|
||||
* Retrieve the known inbound group sessions for the specified room.
|
||||
*
|
||||
* @param roomId The roomId that the sessions will be returned
|
||||
* @return the list of all known group sessions, for the provided roomId
|
||||
*/
|
||||
fun getInboundGroupSessions(roomId: String): List<MXInboundMegolmSessionWrapper>
|
||||
|
||||
/**
|
||||
* @return true to unilaterally blacklist all unverified devices.
|
||||
|
@ -90,6 +98,20 @@ internal interface IMXCryptoStore {
|
|||
|
||||
fun isKeyGossipingEnabled(): Boolean
|
||||
|
||||
/**
|
||||
* As per MSC3061.
|
||||
* If true will make it possible to share part of e2ee room history
|
||||
* on invite depending on the room visibility setting.
|
||||
*/
|
||||
fun enableShareKeyOnInvite(enable: Boolean)
|
||||
|
||||
/**
|
||||
* As per MSC3061.
|
||||
* If true will make it possible to share part of e2ee room history
|
||||
* on invite depending on the room visibility setting.
|
||||
*/
|
||||
fun isShareKeysOnInviteEnabled(): Boolean
|
||||
|
||||
/**
|
||||
* Provides the rooms ids list in which the messages are not encrypted for the unverified devices.
|
||||
*
|
||||
|
@ -250,6 +272,17 @@ internal interface IMXCryptoStore {
|
|||
|
||||
fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean)
|
||||
|
||||
fun shouldShareHistory(roomId: String): Boolean
|
||||
|
||||
/**
|
||||
* Sets a boolean flag that will determine whether or not room history (existing inbound sessions)
|
||||
* will be shared to new user invites.
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @param shouldShareHistory The boolean flag
|
||||
*/
|
||||
fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean)
|
||||
|
||||
/**
|
||||
* Store a session between the logged-in user and another device.
|
||||
*
|
||||
|
@ -290,7 +323,7 @@ internal interface IMXCryptoStore {
|
|||
*
|
||||
* @param sessions the inbound group sessions to store.
|
||||
*/
|
||||
fun storeInboundGroupSessions(sessions: List<OlmInboundGroupSessionWrapper2>)
|
||||
fun storeInboundGroupSessions(sessions: List<MXInboundMegolmSessionWrapper>)
|
||||
|
||||
/**
|
||||
* Retrieve an inbound group session.
|
||||
|
@ -299,7 +332,17 @@ internal interface IMXCryptoStore {
|
|||
* @param senderKey the base64-encoded curve25519 key of the sender.
|
||||
* @return an inbound group session.
|
||||
*/
|
||||
fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2?
|
||||
fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper?
|
||||
|
||||
/**
|
||||
* Retrieve an inbound group session, filtering shared history.
|
||||
*
|
||||
* @param sessionId the session identifier.
|
||||
* @param senderKey the base64-encoded curve25519 key of the sender.
|
||||
* @param sharedHistory filter inbound session with respect to shared history field
|
||||
* @return an inbound group session.
|
||||
*/
|
||||
fun getInboundGroupSession(sessionId: String, senderKey: String, sharedHistory: Boolean): MXInboundMegolmSessionWrapper?
|
||||
|
||||
/**
|
||||
* Get the current outbound group session for this encrypted room.
|
||||
|
@ -333,7 +376,7 @@ internal interface IMXCryptoStore {
|
|||
*
|
||||
* @param olmInboundGroupSessionWrappers the sessions
|
||||
*/
|
||||
fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<OlmInboundGroupSessionWrapper2>)
|
||||
fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<MXInboundMegolmSessionWrapper>)
|
||||
|
||||
/**
|
||||
* Retrieve inbound group sessions that are not yet backed up.
|
||||
|
@ -341,7 +384,7 @@ internal interface IMXCryptoStore {
|
|||
* @param limit the maximum number of sessions to return.
|
||||
* @return an array of non backed up inbound group sessions.
|
||||
*/
|
||||
fun inboundGroupSessionsToBackup(limit: Int): List<OlmInboundGroupSessionWrapper2>
|
||||
fun inboundGroupSessionsToBackup(limit: Int): List<MXInboundMegolmSessionWrapper>
|
||||
|
||||
/**
|
||||
* Number of stored inbound group sessions.
|
||||
|
|
|
@ -50,7 +50,7 @@ import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldCo
|
|||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||
|
@ -657,12 +657,28 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
?: false
|
||||
}
|
||||
|
||||
override fun shouldShareHistory(roomId: String): Boolean {
|
||||
if (!isShareKeysOnInviteEnabled()) return false
|
||||
return doWithRealm(realmConfiguration) {
|
||||
CryptoRoomEntity.getById(it, roomId)?.shouldShareHistory
|
||||
}
|
||||
?: false
|
||||
}
|
||||
|
||||
override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) {
|
||||
doRealmTransaction(realmConfiguration) {
|
||||
CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers
|
||||
}
|
||||
}
|
||||
|
||||
override fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) {
|
||||
Timber.tag(loggerTag.value)
|
||||
.v("setShouldShareHistory for room $roomId is $shouldShareHistory")
|
||||
doRealmTransaction(realmConfiguration) {
|
||||
CryptoRoomEntity.getOrCreate(it, roomId).shouldShareHistory = shouldShareHistory
|
||||
}
|
||||
}
|
||||
|
||||
override fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) {
|
||||
var sessionIdentifier: String? = null
|
||||
|
||||
|
@ -727,54 +743,55 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun storeInboundGroupSessions(sessions: List<OlmInboundGroupSessionWrapper2>) {
|
||||
override fun storeInboundGroupSessions(sessions: List<MXInboundMegolmSessionWrapper>) {
|
||||
if (sessions.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
sessions.forEach { session ->
|
||||
var sessionIdentifier: String? = null
|
||||
sessions.forEach { wrapper ->
|
||||
|
||||
try {
|
||||
sessionIdentifier = session.olmInboundGroupSession?.sessionIdentifier()
|
||||
val sessionIdentifier = try {
|
||||
wrapper.session.sessionIdentifier()
|
||||
} catch (e: OlmException) {
|
||||
Timber.e(e, "## storeInboundGroupSession() : sessionIdentifier failed")
|
||||
return@forEach
|
||||
}
|
||||
|
||||
if (sessionIdentifier != null) {
|
||||
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey)
|
||||
// val shouldShareHistory = session.roomId?.let { roomId ->
|
||||
// CryptoRoomEntity.getById(realm, roomId)?.shouldShareHistory
|
||||
// } ?: false
|
||||
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, wrapper.sessionData.senderKey)
|
||||
|
||||
val existing = realm.where<OlmInboundGroupSessionEntity>()
|
||||
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
|
||||
.findFirst()
|
||||
|
||||
if (existing != null) {
|
||||
// we want to keep the existing backup status
|
||||
existing.putInboundGroupSession(session)
|
||||
} else {
|
||||
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
|
||||
primaryKey = key
|
||||
sessionId = sessionIdentifier
|
||||
senderKey = session.senderKey
|
||||
putInboundGroupSession(session)
|
||||
}
|
||||
|
||||
realm.insertOrUpdate(realmOlmInboundGroupSession)
|
||||
}
|
||||
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
|
||||
primaryKey = key
|
||||
store(wrapper)
|
||||
}
|
||||
Timber.i("## CRYPTO | shouldShareHistory: ${wrapper.sessionData.sharedHistory} for $key")
|
||||
realm.insertOrUpdate(realmOlmInboundGroupSession)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? {
|
||||
override fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper? {
|
||||
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
|
||||
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<OlmInboundGroupSessionEntity>()
|
||||
return doWithRealm(realmConfiguration) { realm ->
|
||||
realm.where<OlmInboundGroupSessionEntity>()
|
||||
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
|
||||
.findFirst()
|
||||
?.getInboundGroupSession()
|
||||
?.toModel()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getInboundGroupSession(sessionId: String, senderKey: String, sharedHistory: Boolean): MXInboundMegolmSessionWrapper? {
|
||||
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<OlmInboundGroupSessionEntity>()
|
||||
.equalTo(OlmInboundGroupSessionEntityFields.SHARED_HISTORY, sharedHistory)
|
||||
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
|
||||
.findFirst()
|
||||
?.toModel()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -786,7 +803,8 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
entity.getOutboundGroupSession()?.let {
|
||||
OutboundGroupSessionWrapper(
|
||||
it,
|
||||
entity.creationTime ?: 0
|
||||
entity.creationTime ?: 0,
|
||||
entity.shouldShareHistory
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -806,6 +824,8 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
if (outboundGroupSession != null) {
|
||||
val info = realm.createObject(OutboundGroupSessionInfoEntity::class.java).apply {
|
||||
creationTime = clock.epochMillis()
|
||||
// Store the room history visibility on the outbound session creation
|
||||
shouldShareHistory = entity.shouldShareHistory
|
||||
putOutboundGroupSession(outboundGroupSession)
|
||||
}
|
||||
entity.outboundSessionInfo = info
|
||||
|
@ -814,17 +834,32 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// override fun needsRotationDueToVisibilityChange(roomId: String): Boolean {
|
||||
// return doWithRealm(realmConfiguration) { realm ->
|
||||
// CryptoRoomEntity.getById(realm, roomId)?.let { entity ->
|
||||
// entity.shouldShareHistory != entity.outboundSessionInfo?.shouldShareHistory
|
||||
// }
|
||||
// } ?: false
|
||||
// }
|
||||
|
||||
/**
|
||||
* Note: the result will be only use to export all the keys and not to use the OlmInboundGroupSessionWrapper2,
|
||||
* so there is no need to use or update `inboundGroupSessionToRelease` for native memory management.
|
||||
*/
|
||||
override fun getInboundGroupSessions(): List<OlmInboundGroupSessionWrapper2> {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<OlmInboundGroupSessionEntity>()
|
||||
override fun getInboundGroupSessions(): List<MXInboundMegolmSessionWrapper> {
|
||||
return doWithRealm(realmConfiguration) { realm ->
|
||||
realm.where<OlmInboundGroupSessionEntity>()
|
||||
.findAll()
|
||||
.mapNotNull { inboundGroupSessionEntity ->
|
||||
inboundGroupSessionEntity.getInboundGroupSession()
|
||||
}
|
||||
.mapNotNull { it.toModel() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun getInboundGroupSessions(roomId: String): List<MXInboundMegolmSessionWrapper> {
|
||||
return doWithRealm(realmConfiguration) { realm ->
|
||||
realm.where<OlmInboundGroupSessionEntity>()
|
||||
.equalTo(OlmInboundGroupSessionEntityFields.ROOM_ID, roomId)
|
||||
.findAll()
|
||||
.mapNotNull { it.toModel() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -885,7 +920,7 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<OlmInboundGroupSessionWrapper2>) {
|
||||
override fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<MXInboundMegolmSessionWrapper>) {
|
||||
if (olmInboundGroupSessionWrappers.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
@ -893,10 +928,13 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
doRealmTransaction(realmConfiguration) { realm ->
|
||||
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
|
||||
try {
|
||||
val sessionIdentifier = olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier()
|
||||
val sessionIdentifier =
|
||||
tryOrNull("Failed to get session identifier") {
|
||||
olmInboundGroupSessionWrapper.session.sessionIdentifier()
|
||||
} ?: return@forEach
|
||||
val key = OlmInboundGroupSessionEntity.createPrimaryKey(
|
||||
sessionIdentifier,
|
||||
olmInboundGroupSessionWrapper.senderKey
|
||||
olmInboundGroupSessionWrapper.sessionData.senderKey
|
||||
)
|
||||
|
||||
val existing = realm.where<OlmInboundGroupSessionEntity>()
|
||||
|
@ -909,9 +947,7 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
// ... might be in cache but not yet persisted, create a record to persist backedup state
|
||||
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
|
||||
primaryKey = key
|
||||
sessionId = sessionIdentifier
|
||||
senderKey = olmInboundGroupSessionWrapper.senderKey
|
||||
putInboundGroupSession(olmInboundGroupSessionWrapper)
|
||||
store(olmInboundGroupSessionWrapper)
|
||||
backedUp = true
|
||||
}
|
||||
|
||||
|
@ -924,15 +960,13 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun inboundGroupSessionsToBackup(limit: Int): List<OlmInboundGroupSessionWrapper2> {
|
||||
override fun inboundGroupSessionsToBackup(limit: Int): List<MXInboundMegolmSessionWrapper> {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<OlmInboundGroupSessionEntity>()
|
||||
.equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false)
|
||||
.limit(limit.toLong())
|
||||
.findAll()
|
||||
.mapNotNull { inboundGroupSession ->
|
||||
inboundGroupSession.getInboundGroupSession()
|
||||
}
|
||||
.mapNotNull { it.toModel() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -973,6 +1007,18 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
} ?: false
|
||||
}
|
||||
|
||||
override fun isShareKeysOnInviteEnabled(): Boolean {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<CryptoMetadataEntity>().findFirst()?.enableKeyForwardingOnInvite
|
||||
} ?: false
|
||||
}
|
||||
|
||||
override fun enableShareKeyOnInvite(enable: Boolean) {
|
||||
doRealmTransaction(realmConfiguration) {
|
||||
it.where<CryptoMetadataEntity>().findFirst()?.enableKeyForwardingOnInvite = enable
|
||||
}
|
||||
}
|
||||
|
||||
override fun setDeviceKeysUploaded(uploaded: Boolean) {
|
||||
doRealmTransaction(realmConfiguration) {
|
||||
it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer = uploaded
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo
|
|||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
@ -51,7 +52,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(
|
|||
// 0, 1, 2: legacy Riot-Android
|
||||
// 3: migrate to RiotX schema
|
||||
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
|
||||
val schemaVersion = 16L
|
||||
val schemaVersion = 17L
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion")
|
||||
|
@ -72,5 +73,6 @@ internal class RealmCryptoStoreMigration @Inject constructor(
|
|||
if (oldVersion < 14) MigrateCryptoTo014(realm).perform()
|
||||
if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
|
||||
if (oldVersion < 16) MigrateCryptoTo016(realm).perform()
|
||||
if (oldVersion < 17) MigrateCryptoTo017(realm).perform()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.crypto.store.db.migration
|
||||
|
||||
import io.realm.DynamicRealm
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntityFields
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import org.matrix.android.sdk.internal.util.database.RealmMigrator
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Version 17L enhance OlmInboundGroupSessionEntity to support shared history for MSC3061.
|
||||
* Also migrates how megolm session are stored to avoid additional serialized frozen class.
|
||||
*/
|
||||
internal class MigrateCryptoTo017(realm: DynamicRealm) : RealmMigrator(realm, 17) {
|
||||
|
||||
override fun doMigrate(realm: DynamicRealm) {
|
||||
realm.schema.get("CryptoRoomEntity")
|
||||
?.addField(CryptoRoomEntityFields.SHOULD_SHARE_HISTORY, Boolean::class.java)?.transform {
|
||||
// We don't have access to the session database to check for the state here and set the good value.
|
||||
// But for now as it's behind a lab flag, will set to false and force initial sync when enabled
|
||||
it.setBoolean(CryptoRoomEntityFields.SHOULD_SHARE_HISTORY, false)
|
||||
}
|
||||
|
||||
realm.schema.get("OutboundGroupSessionInfoEntity")
|
||||
?.addField(OutboundGroupSessionInfoEntityFields.SHOULD_SHARE_HISTORY, Boolean::class.java)?.transform {
|
||||
// We don't have access to the session database to check for the state here and set the good value.
|
||||
// But for now as it's behind a lab flag, will set to false and force initial sync when enabled
|
||||
it.setBoolean(OutboundGroupSessionInfoEntityFields.SHOULD_SHARE_HISTORY, false)
|
||||
}
|
||||
|
||||
realm.schema.get("CryptoMetadataEntity")
|
||||
?.addField(CryptoMetadataEntityFields.ENABLE_KEY_FORWARDING_ON_INVITE, Boolean::class.java)
|
||||
?.transform { obj ->
|
||||
// default to false
|
||||
obj.setBoolean(CryptoMetadataEntityFields.ENABLE_KEY_FORWARDING_ON_INVITE, false)
|
||||
}
|
||||
|
||||
val moshiAdapter = MoshiProvider.providesMoshi().adapter(InboundGroupSessionData::class.java)
|
||||
|
||||
realm.schema.get("OlmInboundGroupSessionEntity")
|
||||
?.addField(OlmInboundGroupSessionEntityFields.SHARED_HISTORY, Boolean::class.java)
|
||||
?.addField(OlmInboundGroupSessionEntityFields.ROOM_ID, String::class.java)
|
||||
?.addField(OlmInboundGroupSessionEntityFields.INBOUND_GROUP_SESSION_DATA_JSON, String::class.java)
|
||||
?.addField(OlmInboundGroupSessionEntityFields.SERIALIZED_OLM_INBOUND_GROUP_SESSION, String::class.java)
|
||||
?.transform { dynamicObject ->
|
||||
try {
|
||||
// we want to convert the old wrapper frozen class into a
|
||||
// map of sessionData & the pickled session herself
|
||||
dynamicObject.getString(OlmInboundGroupSessionEntityFields.OLM_INBOUND_GROUP_SESSION_DATA)?.let { oldData ->
|
||||
val oldWrapper = tryOrNull("Failed to convert megolm inbound group data") {
|
||||
@Suppress("DEPRECATION")
|
||||
deserializeFromRealm<org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2?>(oldData)
|
||||
}
|
||||
val groupSession = oldWrapper?.olmInboundGroupSession
|
||||
?: return@transform Unit.also {
|
||||
Timber.w("Failed to migrate megolm session, no olmInboundGroupSession")
|
||||
}
|
||||
// now convert to new data
|
||||
val data = InboundGroupSessionData(
|
||||
senderKey = oldWrapper.senderKey,
|
||||
roomId = oldWrapper.roomId,
|
||||
keysClaimed = oldWrapper.keysClaimed,
|
||||
forwardingCurve25519KeyChain = oldWrapper.forwardingCurve25519KeyChain,
|
||||
sharedHistory = false,
|
||||
)
|
||||
|
||||
dynamicObject.setString(OlmInboundGroupSessionEntityFields.INBOUND_GROUP_SESSION_DATA_JSON, moshiAdapter.toJson(data))
|
||||
dynamicObject.setString(OlmInboundGroupSessionEntityFields.SERIALIZED_OLM_INBOUND_GROUP_SESSION, serializeForRealm(groupSession))
|
||||
|
||||
// denormalized fields
|
||||
dynamicObject.setString(OlmInboundGroupSessionEntityFields.ROOM_ID, oldWrapper.roomId)
|
||||
dynamicObject.setBoolean(OlmInboundGroupSessionEntityFields.SHARED_HISTORY, false)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "Failed to migrate megolm session")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -35,6 +35,11 @@ internal open class CryptoMetadataEntity(
|
|||
var globalBlacklistUnverifiedDevices: Boolean = false,
|
||||
// setting to enable or disable key gossiping
|
||||
var globalEnableKeyGossiping: Boolean = true,
|
||||
|
||||
// MSC3061: Sharing room keys for past messages
|
||||
// If set to true key history will be shared to invited users with respect to room setting
|
||||
var enableKeyForwardingOnInvite: Boolean = false,
|
||||
|
||||
// The keys backup version currently used. Null means no backup.
|
||||
var backupVersion: String? = null,
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ internal open class CryptoRoomEntity(
|
|||
var algorithm: String? = null,
|
||||
var shouldEncryptForInvitedMembers: Boolean? = null,
|
||||
var blacklistUnverifiedDevices: Boolean = false,
|
||||
// Determines whether or not room history should be shared on new member invites
|
||||
var shouldShareHistory: Boolean = false,
|
||||
// Store the current outbound session for this room,
|
||||
// to avoid re-create and re-share at each startup (if rotation not needed..)
|
||||
// This is specific to megolm but not sure how to model it better
|
||||
|
|
|
@ -18,9 +18,12 @@ package org.matrix.android.sdk.internal.crypto.store.db.model
|
|||
|
||||
import io.realm.RealmObject
|
||||
import io.realm.annotations.PrimaryKey
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
|
||||
import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData
|
||||
import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import org.matrix.olm.OlmInboundGroupSession
|
||||
import timber.log.Timber
|
||||
|
||||
internal fun OlmInboundGroupSessionEntity.Companion.createPrimaryKey(sessionId: String?, senderKey: String?) = "$sessionId|$senderKey"
|
||||
|
@ -28,27 +31,83 @@ internal fun OlmInboundGroupSessionEntity.Companion.createPrimaryKey(sessionId:
|
|||
internal open class OlmInboundGroupSessionEntity(
|
||||
// Combined value to build a primary key
|
||||
@PrimaryKey var primaryKey: String? = null,
|
||||
|
||||
// denormalization for faster querying (these fields are in the inboundGroupSessionDataJson)
|
||||
var sessionId: String? = null,
|
||||
var senderKey: String? = null,
|
||||
// olmInboundGroupSessionData contains Json
|
||||
var roomId: String? = null,
|
||||
|
||||
// Deprecated, used for migration / olmInboundGroupSessionData contains Json
|
||||
// keep it in case of problem to have a chance to recover
|
||||
var olmInboundGroupSessionData: String? = null,
|
||||
|
||||
// Stores the session data in an extensible format
|
||||
// to allow to store data not yet supported for later use
|
||||
var inboundGroupSessionDataJson: String? = null,
|
||||
|
||||
// The pickled session
|
||||
var serializedOlmInboundGroupSession: String? = null,
|
||||
|
||||
// Flag that indicates whether or not the current inboundSession will be shared to
|
||||
// invited users to decrypt past messages
|
||||
var sharedHistory: Boolean = false,
|
||||
// Indicate if the key has been backed up to the homeserver
|
||||
var backedUp: Boolean = false
|
||||
) :
|
||||
RealmObject() {
|
||||
|
||||
fun getInboundGroupSession(): OlmInboundGroupSessionWrapper2? {
|
||||
fun store(wrapper: MXInboundMegolmSessionWrapper) {
|
||||
this.serializedOlmInboundGroupSession = serializeForRealm(wrapper.session)
|
||||
this.inboundGroupSessionDataJson = adapter.toJson(wrapper.sessionData)
|
||||
this.roomId = wrapper.sessionData.roomId
|
||||
this.senderKey = wrapper.sessionData.senderKey
|
||||
this.sessionId = wrapper.session.sessionIdentifier()
|
||||
this.sharedHistory = wrapper.sessionData.sharedHistory
|
||||
}
|
||||
// fun getInboundGroupSession(): OlmInboundGroupSessionWrapper2? {
|
||||
// return try {
|
||||
// deserializeFromRealm<OlmInboundGroupSessionWrapper2?>(olmInboundGroupSessionData)
|
||||
// } catch (failure: Throwable) {
|
||||
// Timber.e(failure, "## Deserialization failure")
|
||||
// return null
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2?) {
|
||||
// olmInboundGroupSessionData = serializeForRealm(olmInboundGroupSessionWrapper)
|
||||
// }
|
||||
|
||||
fun getOlmGroupSession(): OlmInboundGroupSession? {
|
||||
return try {
|
||||
deserializeFromRealm<OlmInboundGroupSessionWrapper2?>(olmInboundGroupSessionData)
|
||||
deserializeFromRealm(serializedOlmInboundGroupSession)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "## Deserialization failure")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2?) {
|
||||
olmInboundGroupSessionData = serializeForRealm(olmInboundGroupSessionWrapper)
|
||||
fun getData(): InboundGroupSessionData? {
|
||||
return try {
|
||||
inboundGroupSessionDataJson?.let {
|
||||
adapter.fromJson(it)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "## Deserialization failure")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
companion object
|
||||
fun toModel(): MXInboundMegolmSessionWrapper? {
|
||||
val data = getData() ?: return null
|
||||
val session = getOlmGroupSession() ?: return null
|
||||
return MXInboundMegolmSessionWrapper(
|
||||
session = session,
|
||||
sessionData = data
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val adapter = MoshiProvider.providesMoshi()
|
||||
.adapter(InboundGroupSessionData::class.java)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,8 @@ import timber.log.Timber
|
|||
|
||||
internal open class OutboundGroupSessionInfoEntity(
|
||||
var serializedOutboundSessionData: String? = null,
|
||||
var creationTime: Long? = null
|
||||
var creationTime: Long? = null,
|
||||
var shouldShareHistory: Boolean = false
|
||||
) : RealmObject() {
|
||||
|
||||
fun getOutboundGroupSession(): OlmOutboundGroupSession? {
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
*/
|
||||
package org.matrix.android.sdk.internal.crypto.tasks
|
||||
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||
|
@ -48,8 +47,12 @@ internal class DefaultSendEventTask @Inject constructor(
|
|||
params.event.roomId
|
||||
?.takeIf { params.encrypt }
|
||||
?.let { roomId ->
|
||||
tryOrNull {
|
||||
try {
|
||||
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
|
||||
} catch (failure: Throwable) {
|
||||
// send any way?
|
||||
// the result is that some users won't probably be able to decrypt :/
|
||||
Timber.w(failure, "SendEvent: failed to load members in room ${params.event.roomId}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,11 @@ package org.matrix.android.sdk.internal.database.helper
|
|||
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||
import org.matrix.android.sdk.internal.crypto.model.SessionInfo
|
||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
||||
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
|
||||
|
@ -31,6 +35,7 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFie
|
|||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
|
||||
import org.matrix.android.sdk.internal.database.query.find
|
||||
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
|
||||
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
|
||||
|
@ -180,3 +185,12 @@ internal fun ChunkEntity.isMoreRecentThan(chunkToCheck: ChunkEntity): Boolean {
|
|||
// We don't know, so we assume it's false
|
||||
return false
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.Companion.findLatestSessionInfo(realm: Realm, roomId: String): Set<SessionInfo>? =
|
||||
ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.mapNotNull { timelineEvent ->
|
||||
timelineEvent?.root?.asDomain()?.content?.toModel<EncryptedEventContent>()?.let { content ->
|
||||
content.sessionId ?: return@mapNotNull null
|
||||
content.senderKey ?: return@mapNotNull null
|
||||
SessionInfo(content.sessionId, content.senderKey)
|
||||
}
|
||||
}?.toSet()
|
||||
|
|
|
@ -21,6 +21,7 @@ import com.squareup.moshi.Moshi
|
|||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import okhttp3.ConnectionSpec
|
||||
import okhttp3.Dispatcher
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
|
@ -73,7 +74,9 @@ internal object NetworkModule {
|
|||
apiInterceptor: ApiInterceptor
|
||||
): OkHttpClient {
|
||||
val spec = ConnectionSpec.Builder(matrixConfiguration.connectionSpec).build()
|
||||
|
||||
val dispatcher = Dispatcher().apply {
|
||||
maxRequestsPerHost = 20
|
||||
}
|
||||
return OkHttpClient.Builder()
|
||||
// workaround for #4669
|
||||
.protocols(listOf(Protocol.HTTP_1_1))
|
||||
|
@ -94,6 +97,7 @@ internal object NetworkModule {
|
|||
addInterceptor(curlLoggingInterceptor)
|
||||
}
|
||||
}
|
||||
.dispatcher(dispatcher)
|
||||
.connectionSpecs(Collections.singletonList(spec))
|
||||
.applyMatrixConfiguration(matrixConfiguration)
|
||||
.build()
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
|||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.matrix.android.sdk.api.auth.LoginType
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.auth.data.DiscoveryInformation
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
|
@ -145,7 +146,8 @@ internal class DefaultLegacySessionImporter @Inject constructor(
|
|||
forceUsageTlsVersions = legacyConfig.forceUsageOfTlsVersions()
|
||||
),
|
||||
// If token is not valid, this boolean will be updated later
|
||||
isTokenValid = true
|
||||
isTokenValid = true,
|
||||
loginType = LoginType.UNKNOWN,
|
||||
)
|
||||
|
||||
Timber.d("Migration: save session")
|
||||
|
|
|
@ -70,7 +70,15 @@ internal class PreferredNetworkCallbackStrategy @Inject constructor(context: Con
|
|||
|
||||
override fun register(hasChanged: () -> Unit) {
|
||||
hasChangedCallback = hasChanged
|
||||
conn.registerDefaultNetworkCallback(networkCallback)
|
||||
// Add a try catch for safety
|
||||
// XXX: It happens when running all tests in CI, at some points we reach a limit here causing TooManyRequestsException
|
||||
// and crashing the sync thread. We might have problem here, would need some investigation
|
||||
// for now adding a catch to allow CI to continue running
|
||||
try {
|
||||
conn.registerDefaultNetworkCallback(networkCallback)
|
||||
} catch (t: Throwable) {
|
||||
Timber.e(t, "Unable to register default network callback")
|
||||
}
|
||||
}
|
||||
|
||||
override fun unregister() {
|
||||
|
|
|
@ -17,18 +17,23 @@
|
|||
package org.matrix.android.sdk.internal.session.room.membership
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.otaliastudios.opengl.core.use
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.api.session.room.members.MembershipService
|
||||
import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.internal.database.helper.findLatestSessionInfo
|
||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
||||
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
|
||||
|
@ -50,8 +55,10 @@ internal class DefaultMembershipService @AssistedInject constructor(
|
|||
private val inviteThreePidTask: InviteThreePidTask,
|
||||
private val membershipAdminTask: MembershipAdminTask,
|
||||
private val roomDataSource: RoomDataSource,
|
||||
private val cryptoService: CryptoService,
|
||||
@UserId
|
||||
private val userId: String,
|
||||
private val matrixConfiguration: MatrixConfiguration,
|
||||
private val queryStringValueProcessor: QueryStringValueProcessor
|
||||
) : MembershipService {
|
||||
|
||||
|
@ -139,10 +146,20 @@ internal class DefaultMembershipService @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override suspend fun invite(userId: String, reason: String?) {
|
||||
sendShareHistoryKeysIfNeeded(userId)
|
||||
val params = InviteTask.Params(roomId, userId, reason)
|
||||
inviteTask.execute(params)
|
||||
}
|
||||
|
||||
private suspend fun sendShareHistoryKeysIfNeeded(userId: String) {
|
||||
if (!cryptoService.isShareKeysOnInviteEnabled()) return
|
||||
// TODO not sure it's the right way to get the latest messages in a room
|
||||
val sessionInfo = Realm.getInstance(monarchy.realmConfiguration).use {
|
||||
ChunkEntity.findLatestSessionInfo(it, roomId)
|
||||
}
|
||||
cryptoService.sendSharedHistoryKeys(roomId, userId, sessionInfo)
|
||||
}
|
||||
|
||||
override suspend fun invite3pid(threePid: ThreePid) {
|
||||
val params = InviteThreePidTask.Params(roomId, threePid)
|
||||
return inviteThreePidTask.execute(params)
|
||||
|
|
|
@ -20,6 +20,7 @@ import io.realm.Realm
|
|||
import io.realm.RealmConfiguration
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.android.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
|
@ -116,6 +117,7 @@ internal class DefaultTimeline(
|
|||
)
|
||||
|
||||
private var strategy: LoadTimelineStrategy = buildStrategy(LoadTimelineStrategy.Mode.Live)
|
||||
private var startTimelineJob: Job? = null
|
||||
|
||||
override val isLive: Boolean
|
||||
get() = !getPaginationState(Timeline.Direction.FORWARDS).hasMoreToLoad
|
||||
|
@ -143,7 +145,7 @@ internal class DefaultTimeline(
|
|||
timelineScope.launch {
|
||||
loadRoomMembersIfNeeded()
|
||||
}
|
||||
timelineScope.launch {
|
||||
startTimelineJob = timelineScope.launch {
|
||||
sequencer.post {
|
||||
if (isStarted.compareAndSet(false, true)) {
|
||||
isFromThreadTimeline = rootThreadEventId != null
|
||||
|
@ -174,8 +176,10 @@ internal class DefaultTimeline(
|
|||
|
||||
override fun restartWithEventId(eventId: String?) {
|
||||
timelineScope.launch {
|
||||
openAround(eventId, rootThreadEventId)
|
||||
postSnapshot()
|
||||
sequencer.post {
|
||||
openAround(eventId, rootThreadEventId)
|
||||
postSnapshot()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -185,6 +189,7 @@ internal class DefaultTimeline(
|
|||
|
||||
override fun paginate(direction: Timeline.Direction, count: Int) {
|
||||
timelineScope.launch {
|
||||
startTimelineJob?.join()
|
||||
val postSnapshot = loadMore(count, direction, fetchOnServerIfNeeded = true)
|
||||
if (postSnapshot) {
|
||||
postSnapshot()
|
||||
|
@ -193,6 +198,7 @@ internal class DefaultTimeline(
|
|||
}
|
||||
|
||||
override suspend fun awaitPaginate(direction: Timeline.Direction, count: Int): List<TimelineEvent> {
|
||||
startTimelineJob?.join()
|
||||
withContext(timelineDispatcher) {
|
||||
loadMore(count, direction, fetchOnServerIfNeeded = true)
|
||||
}
|
||||
|
@ -279,6 +285,7 @@ internal class DefaultTimeline(
|
|||
direction = Timeline.Direction.BACKWARDS,
|
||||
fetchOnServerIfNeeded = false
|
||||
)
|
||||
|
||||
Timber.v("$baseLogMessage finished")
|
||||
}
|
||||
|
||||
|
@ -312,9 +319,11 @@ internal class DefaultTimeline(
|
|||
|
||||
private fun onLimitedTimeline() {
|
||||
timelineScope.launch {
|
||||
initPaginationStates(null)
|
||||
loadMore(settings.initialSize, Timeline.Direction.BACKWARDS, false)
|
||||
postSnapshot()
|
||||
sequencer.post {
|
||||
initPaginationStates(null)
|
||||
loadMore(settings.initialSize, Timeline.Direction.BACKWARDS, false)
|
||||
postSnapshot()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import io.realm.Realm
|
|||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmResults
|
||||
import io.realm.kotlin.createObject
|
||||
import io.realm.kotlin.executeTransactionAwait
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
|
@ -265,7 +266,7 @@ internal class LoadTimelineStrategy constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getChunkEntity(realm: Realm): RealmResults<ChunkEntity> {
|
||||
private suspend fun getChunkEntity(realm: Realm): RealmResults<ChunkEntity> {
|
||||
return when (mode) {
|
||||
is Mode.Live -> {
|
||||
ChunkEntity.where(realm, roomId)
|
||||
|
@ -289,8 +290,8 @@ internal class LoadTimelineStrategy constructor(
|
|||
* Clear any existing thread chunk entity and create a new one, with the
|
||||
* rootThreadEventId included.
|
||||
*/
|
||||
private fun recreateThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
|
||||
realm.executeTransaction {
|
||||
private suspend fun recreateThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
|
||||
realm.executeTransactionAwait {
|
||||
// Lets delete the chunk and start a new one
|
||||
ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
|
||||
Timber.i("###THREADS LoadTimelineStrategy [onStart] thread chunk cleared..")
|
||||
|
@ -309,8 +310,8 @@ internal class LoadTimelineStrategy constructor(
|
|||
/**
|
||||
* Clear any existing thread chunk.
|
||||
*/
|
||||
private fun clearThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
|
||||
realm.executeTransaction {
|
||||
private suspend fun clearThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
|
||||
realm.executeTransactionAwait {
|
||||
ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
|
||||
Timber.i("###THREADS LoadTimelineStrategy [onStop] thread chunk cleared..")
|
||||
}
|
||||
|
|
|
@ -490,38 +490,11 @@ internal class TimelineChunk(
|
|||
private fun handleDatabaseChangeSet(results: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||
val insertions = changeSet.insertionRanges
|
||||
for (range in insertions) {
|
||||
// Check if the insertion's displayIndices match our expectations - or skip this insertion.
|
||||
// Inconsistencies (missing messages) can happen otherwise if we get insertions before having loaded all timeline events of the chunk.
|
||||
if (builtEvents.isNotEmpty()) {
|
||||
// Check consistency to item before insertions
|
||||
if (range.startIndex > 0) {
|
||||
val firstInsertion = results[range.startIndex]!!
|
||||
val lastBeforeInsertion = builtEvents[range.startIndex - 1]
|
||||
if (firstInsertion.displayIndex + 1 != lastBeforeInsertion.displayIndex) {
|
||||
Timber.i(
|
||||
"handleDatabaseChangeSet: skip insertion at ${range.startIndex}/${builtEvents.size}, " +
|
||||
"displayIndex mismatch at ${range.startIndex}: ${firstInsertion.displayIndex} -> ${lastBeforeInsertion.displayIndex}"
|
||||
)
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Check consistency to item after insertions
|
||||
if (range.startIndex < builtEvents.size) {
|
||||
val lastInsertion = results[range.startIndex + range.length - 1]!!
|
||||
val firstAfterInsertion = builtEvents[range.startIndex]
|
||||
if (firstAfterInsertion.displayIndex + 1 != lastInsertion.displayIndex) {
|
||||
Timber.i(
|
||||
"handleDatabaseChangeSet: skip insertion at ${range.startIndex}/${builtEvents.size}, " +
|
||||
"displayIndex mismatch at ${range.startIndex + range.length}: " +
|
||||
"${firstAfterInsertion.displayIndex} -> ${lastInsertion.displayIndex}"
|
||||
)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!validateInsertion(range, results)) continue
|
||||
val newItems = results
|
||||
.subList(range.startIndex, range.startIndex + range.length)
|
||||
.map { it.buildAndDecryptIfNeeded() }
|
||||
|
||||
builtEventsIndexes.entries.filter { it.value >= range.startIndex }.forEach { it.setValue(it.value + range.length) }
|
||||
newItems.mapIndexed { index, timelineEvent ->
|
||||
if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) {
|
||||
|
@ -536,12 +509,9 @@ internal class TimelineChunk(
|
|||
for (range in modifications) {
|
||||
for (modificationIndex in (range.startIndex until range.startIndex + range.length)) {
|
||||
val updatedEntity = results[modificationIndex] ?: continue
|
||||
val displayIndex = builtEventsIndexes[updatedEntity.eventId]
|
||||
if (displayIndex == null) {
|
||||
continue
|
||||
}
|
||||
val builtEventIndex = builtEventsIndexes[updatedEntity.eventId] ?: continue
|
||||
try {
|
||||
builtEvents[displayIndex] = updatedEntity.buildAndDecryptIfNeeded()
|
||||
builtEvents[builtEventIndex] = updatedEntity.buildAndDecryptIfNeeded()
|
||||
} catch (failure: Throwable) {
|
||||
Timber.v("Fail to update items at index: $modificationIndex")
|
||||
}
|
||||
|
@ -558,6 +528,21 @@ internal class TimelineChunk(
|
|||
}
|
||||
}
|
||||
|
||||
private fun validateInsertion(range: OrderedCollectionChangeSet.Range, results: RealmResults<TimelineEventEntity>): Boolean {
|
||||
// Insertion can only happen from LastForward chunk after a sync.
|
||||
if (isLastForward.get()) {
|
||||
val firstBuiltEvent = builtEvents.firstOrNull()
|
||||
if (firstBuiltEvent != null) {
|
||||
val lastInsertion = results[range.startIndex + range.length - 1] ?: return false
|
||||
if (firstBuiltEvent.displayIndex + 1 != lastInsertion.displayIndex) {
|
||||
Timber.v("There is no continuation in the chunk, chunk is not fully loaded yet, skip insert.")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getNextDisplayIndex(direction: Timeline.Direction): Int? {
|
||||
if (timelineEventEntities.isEmpty()) {
|
||||
return null
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.util
|
|||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
|
||||
internal interface BackgroundDetectionObserver : DefaultLifecycleObserver {
|
||||
val isInBackground: Boolean
|
||||
|
@ -37,7 +38,7 @@ internal class DefaultBackgroundDetectionObserver : BackgroundDetectionObserver
|
|||
override var isInBackground: Boolean = true
|
||||
private set
|
||||
|
||||
private val listeners = LinkedHashSet<BackgroundDetectionObserver.Listener>()
|
||||
private val listeners = CopyOnWriteArraySet<BackgroundDetectionObserver.Listener>()
|
||||
|
||||
override fun register(listener: BackgroundDetectionObserver.Listener) {
|
||||
listeners.add(listener)
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.auth.db.migration
|
||||
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.test.fakes.internal.auth.db.migration.Fake005MigrationRealm
|
||||
|
||||
class MigrateAuthTo005Test {
|
||||
|
||||
private val fakeRealm = Fake005MigrationRealm()
|
||||
private val migrator = MigrateAuthTo005(fakeRealm.instance)
|
||||
|
||||
@Test
|
||||
fun `when doMigrate, then LoginType field added`() {
|
||||
migrator.doMigrate(fakeRealm.instance)
|
||||
|
||||
fakeRealm.verifyLoginTypeAdded()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.internal.auth.login
|
||||
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.amshove.kluent.shouldNotBeEqualTo
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.auth.LoginType
|
||||
|
||||
class LoginTypeTest {
|
||||
|
||||
@Test
|
||||
fun `when getting type fromName, then map correctly`() {
|
||||
LoginType.fromName(LoginType.PASSWORD.name) shouldBeEqualTo LoginType.PASSWORD
|
||||
LoginType.fromName(LoginType.SSO.name) shouldBeEqualTo LoginType.SSO
|
||||
LoginType.fromName(LoginType.UNSUPPORTED.name) shouldBeEqualTo LoginType.UNSUPPORTED
|
||||
LoginType.fromName(LoginType.CUSTOM.name) shouldBeEqualTo LoginType.CUSTOM
|
||||
LoginType.fromName(LoginType.DIRECT.name) shouldBeEqualTo LoginType.DIRECT
|
||||
LoginType.fromName(LoginType.UNKNOWN.name) shouldBeEqualTo LoginType.UNKNOWN
|
||||
}
|
||||
|
||||
@Test // The failure of this test means that an existing type has not been correctly added to fromValue
|
||||
fun `given non-unknown type name, when getting type fromName, then type is not UNKNOWN`() {
|
||||
val types = LoginType.values()
|
||||
|
||||
types.forEach { type ->
|
||||
if (type != LoginType.UNKNOWN) {
|
||||
LoginType.fromName(type.name) shouldNotBeEqualTo LoginType.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.test.fakes.api
|
||||
|
||||
import io.mockk.mockk
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
|
||||
class FakeSession {
|
||||
|
||||
val instance: Session = mockk()
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.test.fakes.internal
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.internal.SessionManager
|
||||
import org.matrix.android.sdk.test.fakes.api.FakeSession
|
||||
|
||||
internal class FakeSessionManager {
|
||||
|
||||
val instance: SessionManager = mockk()
|
||||
|
||||
init {
|
||||
every { instance.getOrCreateSession(any()) } returns fakeSession.instance
|
||||
}
|
||||
|
||||
fun assertSessionCreatedWithParams(session: Session, sessionParams: SessionParams) {
|
||||
verify { instance.getOrCreateSession(sessionParams) }
|
||||
|
||||
session shouldBeEqualTo fakeSession.instance
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val fakeSession = FakeSession()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.test.fakes.internal.auth
|
||||
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.internal.auth.IsValidClientServerApiTask
|
||||
import org.matrix.android.sdk.internal.auth.IsValidClientServerApiTask.Params
|
||||
|
||||
internal class FakeIsValidClientServerApiTask {
|
||||
|
||||
init {
|
||||
coEvery { instance.execute(any()) } returns true
|
||||
}
|
||||
|
||||
val instance: IsValidClientServerApiTask = mockk()
|
||||
|
||||
fun givenValidationFails() {
|
||||
coEvery { instance.execute(any()) } returns false
|
||||
}
|
||||
|
||||
fun verifyExecutionWithConfig(config: HomeServerConnectionConfig) {
|
||||
coVerify { instance.execute(Params(config)) }
|
||||
}
|
||||
|
||||
fun verifyNoExecution() {
|
||||
coVerify(inverse = true) { instance.execute(any()) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.test.fakes.internal.auth
|
||||
|
||||
import io.mockk.coJustRun
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import org.matrix.android.sdk.internal.auth.PendingSessionStore
|
||||
|
||||
internal class FakePendingSessionStore {
|
||||
|
||||
val instance: PendingSessionStore = mockk()
|
||||
|
||||
init {
|
||||
coJustRun { instance.delete() }
|
||||
}
|
||||
|
||||
fun verifyPendingSessionDataCleared() {
|
||||
coVerify { instance.delete() }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.matrix.android.sdk.test.fakes.internal.auth
|
||||
|
||||
import android.net.Uri
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import org.matrix.android.sdk.api.auth.LoginType
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.internal.auth.SessionParamsCreator
|
||||
import org.matrix.android.sdk.test.fixtures.SessionParamsFixture.aSessionParams
|
||||
|
||||
internal class FakeSessionParamsCreator {
|
||||
|
||||
val instance: SessionParamsCreator = mockk()
|
||||
|
||||
init {
|
||||
mockkStatic(Uri::class)
|
||||
every { Uri.parse(any()) } returns mockk()
|
||||
coEvery { instance.create(any(), any(), any()) } returns sessionParams
|
||||
}
|
||||
|
||||
fun verifyCreatedWithParameters(
|
||||
credentials: Credentials,
|
||||
homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||
loginType: LoginType,
|
||||
) {
|
||||
coVerify { instance.create(credentials, homeServerConnectionConfig, loginType) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
val sessionParams = aSessionParams()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.test.fakes.internal.auth
|
||||
|
||||
import io.mockk.coJustRun
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
||||
import org.matrix.android.sdk.internal.auth.SessionParamsStore
|
||||
|
||||
internal class FakeSessionParamsStore {
|
||||
|
||||
val instance: SessionParamsStore = mockk()
|
||||
|
||||
init {
|
||||
coJustRun { instance.save(any()) }
|
||||
}
|
||||
|
||||
fun verifyParamsSaved(sessionParams: SessionParams) {
|
||||
coVerify { instance.save(sessionParams) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.test.fakes.internal.auth.db.migration
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verifyOrder
|
||||
import io.realm.DynamicRealm
|
||||
import io.realm.RealmObjectSchema
|
||||
import io.realm.RealmSchema
|
||||
import org.matrix.android.sdk.internal.auth.db.SessionParamsEntityFields
|
||||
|
||||
class Fake005MigrationRealm {
|
||||
|
||||
val instance: DynamicRealm = mockk()
|
||||
|
||||
private val schema: RealmSchema = mockk()
|
||||
private val objectSchema: RealmObjectSchema = mockk()
|
||||
|
||||
init {
|
||||
every { instance.schema } returns schema
|
||||
every { schema.get("SessionParamsEntity") } returns objectSchema
|
||||
every { objectSchema.addField(any(), any()) } returns objectSchema
|
||||
every { objectSchema.setRequired(any(), any()) } returns objectSchema
|
||||
every { objectSchema.transform(any()) } returns objectSchema
|
||||
}
|
||||
|
||||
fun verifyLoginTypeAdded() {
|
||||
verifyLoginTypeFieldAddedAndTransformed()
|
||||
}
|
||||
|
||||
private fun verifyLoginTypeFieldAddedAndTransformed() {
|
||||
verifyOrder {
|
||||
objectSchema["SessionParamsEntity"]
|
||||
objectSchema.addField(SessionParamsEntityFields.LOGIN_TYPE, String::class.java)
|
||||
objectSchema.setRequired(SessionParamsEntityFields.LOGIN_TYPE, true)
|
||||
objectSchema.transform(any())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeSessionParamsMapperMoshi.Companion.sessionParams
|
||||
import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeSessionParamsMapperMoshi.Companion.sessionParamsEntity
|
||||
import org.matrix.android.sdk.test.fixtures.CredentialsFixture.aCredentials
|
||||
|
||||
internal class FakeCredentialsJsonAdapter {
|
||||
|
||||
val instance: JsonAdapter<Credentials> = mockk()
|
||||
|
||||
init {
|
||||
every { instance.fromJson(sessionParamsEntity.credentialsJson) } returns credentials
|
||||
every { instance.toJson(sessionParams.credentials) } returns CREDENTIALS_JSON
|
||||
}
|
||||
|
||||
fun givenNullDeserialization() {
|
||||
every { instance.fromJson(sessionParamsEntity.credentialsJson) } returns null
|
||||
}
|
||||
|
||||
fun givenNullSerialization() {
|
||||
every { instance.toJson(credentials) } returns null
|
||||
}
|
||||
|
||||
companion object {
|
||||
val credentials = aCredentials()
|
||||
const val CREDENTIALS_JSON = "credentials_json"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeSessionParamsMapperMoshi.Companion.sessionParams
|
||||
import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeSessionParamsMapperMoshi.Companion.sessionParamsEntity
|
||||
|
||||
internal class FakeHomeServerConnectionConfigJsonAdapter {
|
||||
|
||||
val instance: JsonAdapter<HomeServerConnectionConfig> = mockk()
|
||||
|
||||
init {
|
||||
every { instance.fromJson(sessionParamsEntity.homeServerConnectionConfigJson) } returns homeServerConnectionConfig
|
||||
every { instance.toJson(sessionParams.homeServerConnectionConfig) } returns HOME_SERVER_CONNECTION_CONFIG_JSON
|
||||
}
|
||||
|
||||
fun givenNullDeserialization() {
|
||||
every { instance.fromJson(sessionParamsEntity.credentialsJson) } returns null
|
||||
}
|
||||
|
||||
fun givenNullSerialization() {
|
||||
every { instance.toJson(homeServerConnectionConfig) } returns null
|
||||
}
|
||||
|
||||
companion object {
|
||||
val homeServerConnectionConfig = HomeServerConnectionConfig.Builder().withHomeServerUri("homeserver").build()
|
||||
const val HOME_SERVER_CONNECTION_CONFIG_JSON = "home_server_connection_config_json"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams
|
||||
|
||||
import android.net.Uri
|
||||
import com.squareup.moshi.Moshi
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.amshove.kluent.shouldBeNull
|
||||
import org.matrix.android.sdk.api.auth.LoginType
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
||||
import org.matrix.android.sdk.api.auth.data.sessionId
|
||||
import org.matrix.android.sdk.internal.auth.db.SessionParamsEntity
|
||||
import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeCredentialsJsonAdapter.Companion.CREDENTIALS_JSON
|
||||
import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeCredentialsJsonAdapter.Companion.credentials
|
||||
import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeHomeServerConnectionConfigJsonAdapter.Companion.HOME_SERVER_CONNECTION_CONFIG_JSON
|
||||
import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeHomeServerConnectionConfigJsonAdapter.Companion.homeServerConnectionConfig
|
||||
import org.matrix.android.sdk.test.fixtures.SessionParamsEntityFixture.aSessionParamsEntity
|
||||
import org.matrix.android.sdk.test.fixtures.SessionParamsFixture.aSessionParams
|
||||
|
||||
internal class FakeSessionParamsMapperMoshi {
|
||||
|
||||
val instance: Moshi = mockk()
|
||||
private val credentialsJsonAdapter = FakeCredentialsJsonAdapter()
|
||||
private val homeServerConnectionConfigAdapter = FakeHomeServerConnectionConfigJsonAdapter()
|
||||
|
||||
init {
|
||||
mockkStatic(Uri::class)
|
||||
every { Uri.parse(any()) } returns mockk()
|
||||
every { instance.adapter(Credentials::class.java) } returns credentialsJsonAdapter.instance
|
||||
every { instance.adapter(HomeServerConnectionConfig::class.java) } returns homeServerConnectionConfigAdapter.instance
|
||||
}
|
||||
|
||||
fun assertSessionParamsWasMappedSuccessfully(sessionParams: SessionParams?) {
|
||||
sessionParams shouldBeEqualTo SessionParams(
|
||||
credentials,
|
||||
homeServerConnectionConfig,
|
||||
sessionParamsEntity.isTokenValid,
|
||||
LoginType.fromName(sessionParamsEntity.loginType)
|
||||
)
|
||||
}
|
||||
|
||||
fun assertSessionParamsIsNull(sessionParams: SessionParams?) {
|
||||
sessionParams.shouldBeNull()
|
||||
}
|
||||
|
||||
fun assertSessionParamsEntityWasMappedSuccessfully(sessionParamsEntity: SessionParamsEntity?) {
|
||||
sessionParamsEntity shouldBeEqualTo SessionParamsEntity(
|
||||
sessionParams.credentials.sessionId(),
|
||||
sessionParams.userId,
|
||||
CREDENTIALS_JSON,
|
||||
HOME_SERVER_CONNECTION_CONFIG_JSON,
|
||||
sessionParams.isTokenValid,
|
||||
sessionParams.loginType.name,
|
||||
)
|
||||
}
|
||||
|
||||
fun assertSessionParamsEntityIsNull(sessionParamsEntity: SessionParamsEntity?) {
|
||||
sessionParamsEntity.shouldBeNull()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val sessionParams = aSessionParams()
|
||||
val sessionParamsEntity = aSessionParamsEntity()
|
||||
val nullSessionParams: SessionParams? = null
|
||||
val nullSessionParamsEntity: SessionParamsEntity? = null
|
||||
}
|
||||
}
|
38
matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/CredentialsFixture.kt
vendored
Normal file
38
matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/CredentialsFixture.kt
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.test.fixtures
|
||||
|
||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||
import org.matrix.android.sdk.api.auth.data.DiscoveryInformation
|
||||
|
||||
object CredentialsFixture {
|
||||
fun aCredentials(
|
||||
userId: String = "",
|
||||
accessToken: String = "",
|
||||
refreshToken: String? = null,
|
||||
homeServer: String? = null,
|
||||
deviceId: String? = null,
|
||||
discoveryInformation: DiscoveryInformation? = null,
|
||||
) = Credentials(
|
||||
userId,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
homeServer,
|
||||
deviceId,
|
||||
discoveryInformation,
|
||||
)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue