mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 11:59:12 +03:00
Merge branch 'develop' into feature/fre/start_dm_on_first_msg
* develop: (156 commits) adding test case for showing html entities are processed adding tests around the event html rendering - the test helper is a little hacky in order to covert the spans to something human readable removing extra line adding changelog entry overriding the default list handler with an implementation that takes into account the initial starting position trigger CI Use executeTransactionAwait (need realm refresh in this case) Bump flipper from 0.152.0 to 0.153.0 Use executeTransactionAwait (need realm refresh in this case) generating 1.4.27 changelog and updating version Fixing crash when sharing plain text, such as a url Fix crashes when opening Thread (#6463) Timeline: fix validation of timeline event changes Fix ConcurrentModificationException on BackgroundDetectionObserver Fix crashes when opening Thread (#6463) suppressing unused string resource Changelog Fix ConcurrentModificationException on BackgroundDetectionObserver Fix typo adding changelog entry ...
This commit is contained in:
commit
68bd55fef5
209 changed files with 4860 additions and 1154 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)
|
Changes in Element v1.4.26 (2022-06-30)
|
||||||
=======================================
|
=======================================
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ buildscript {
|
||||||
classpath libs.gradle.gradlePlugin
|
classpath libs.gradle.gradlePlugin
|
||||||
classpath libs.gradle.kotlinPlugin
|
classpath libs.gradle.kotlinPlugin
|
||||||
classpath libs.gradle.hiltPlugin
|
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 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.4.0.2513'
|
||||||
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5'
|
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5'
|
||||||
classpath "com.likethesalad.android:stem-plugin:2.1.1"
|
classpath "com.likethesalad.android:stem-plugin:2.1.1"
|
||||||
|
|
1
changelog.d/4777.bugfix
Normal file
1
changelog.d/4777.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fixes numbered lists always starting from 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/6401.feature
Normal file
1
changelog.d/6401.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[Location sharing] - Reply action on a live message
|
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*',
|
'**/*Activity*',
|
||||||
'**/*Fragment*',
|
'**/*Fragment*',
|
||||||
'**/*Application*',
|
'**/*Application*',
|
||||||
|
'**/*AndroidService*',
|
||||||
|
|
||||||
// We would like to exclude android widgets as well but our naming is inconsistent
|
// We would like to exclude android widgets as well but our naming is inconsistent
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,9 @@ def retrofit = "2.9.0"
|
||||||
def arrow = "0.8.2"
|
def arrow = "0.8.2"
|
||||||
def markwon = "4.6.2"
|
def markwon = "4.6.2"
|
||||||
def moshi = "1.13.0"
|
def moshi = "1.13.0"
|
||||||
def lifecycle = "2.4.1"
|
def lifecycle = "2.5.0"
|
||||||
def flowBinding = "1.2.0"
|
def flowBinding = "1.2.0"
|
||||||
def flipper = "0.151.1"
|
def flipper = "0.153.0"
|
||||||
def epoxy = "4.6.2"
|
def epoxy = "4.6.2"
|
||||||
def mavericks = "2.7.0"
|
def mavericks = "2.7.0"
|
||||||
def glide = "4.13.2"
|
def glide = "4.13.2"
|
||||||
|
@ -29,7 +29,7 @@ def bigImageViewer = "1.8.1"
|
||||||
def jjwt = "0.11.5"
|
def jjwt = "0.11.5"
|
||||||
def vanniktechEmoji = "0.15.0"
|
def vanniktechEmoji = "0.15.0"
|
||||||
|
|
||||||
def fragment = "1.4.1"
|
def fragment = "1.5.0"
|
||||||
|
|
||||||
// Testing
|
// 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
|
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 : [
|
androidx : [
|
||||||
'annotation' : "androidx.annotation:annotation:1.4.0",
|
'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",
|
'annotations' : "androidx.annotation:annotation:1.3.0",
|
||||||
'appCompat' : "androidx.appcompat:appcompat:1.4.2",
|
'appCompat' : "androidx.appcompat:appcompat:1.4.2",
|
||||||
'biometric' : "androidx.biometric:biometric:1.1.0",
|
'biometric' : "androidx.biometric:biometric:1.1.0",
|
||||||
|
|
|
@ -191,7 +191,7 @@ Examples of prefixes:
|
||||||
- `[Bugfix]`
|
- `[Bugfix]`
|
||||||
- etc.
|
- 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
|
##### 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 {
|
return Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
|
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single)
|
||||||
type = "video/*, image/*"
|
type = "*/*"
|
||||||
val mimeTypes = arrayOf("image/*", "video/*")
|
val mimeTypes = arrayOf("image/*", "video/*")
|
||||||
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
|
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation libs.androidx.appCompat
|
implementation libs.androidx.appCompat
|
||||||
|
implementation libs.androidx.fragmentKtx
|
||||||
implementation libs.google.material
|
implementation libs.google.material
|
||||||
// Pref theme
|
// Pref theme
|
||||||
implementation libs.androidx.preferenceKtx
|
implementation libs.androidx.preferenceKtx
|
||||||
|
|
|
@ -18,8 +18,12 @@ package im.vector.lib.ui.styles.debug
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import im.vector.lib.ui.styles.R
|
import im.vector.lib.ui.styles.R
|
||||||
|
@ -31,6 +35,7 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
setupMenu()
|
||||||
val views = ActivityDebugMaterialThemeBinding.inflate(layoutInflater)
|
val views = ActivityDebugMaterialThemeBinding.inflate(layoutInflater)
|
||||||
setContentView(views.root)
|
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) {
|
private fun showTestDialog(theme: Int) {
|
||||||
MaterialAlertDialogBuilder(this, theme)
|
MaterialAlertDialogBuilder(this, theme)
|
||||||
.setTitle("Dialog title")
|
.setTitle("Dialog title")
|
||||||
|
@ -82,9 +108,4 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() {
|
||||||
.setNeutralButton("Neutral", null)
|
.setNeutralButton("Neutral", null)
|
||||||
.show()
|
.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.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
|
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
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.Session
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
|
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.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.model.message.MessageContent
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
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 org.matrix.android.sdk.api.session.sync.SyncState
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.CancellationException
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@ -54,7 +61,7 @@ import java.util.concurrent.TimeUnit
|
||||||
* This class exposes methods to be used in common cases
|
* This class exposes methods to be used in common cases
|
||||||
* Registration, login, Sync, Sending messages...
|
* Registration, login, Sync, Sending messages...
|
||||||
*/
|
*/
|
||||||
class CommonTestHelper private constructor(context: Context) {
|
class CommonTestHelper internal constructor(context: Context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) {
|
internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) {
|
||||||
|
@ -241,6 +248,37 @@ class CommonTestHelper private constructor(context: Context) {
|
||||||
return sentEvents
|
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
|
* Reply in a thread
|
||||||
* @param room the room where to send the messages
|
* @param room the room where to send the messages
|
||||||
|
@ -285,6 +323,8 @@ class CommonTestHelper private constructor(context: Context) {
|
||||||
)
|
)
|
||||||
assertNotNull(session)
|
assertNotNull(session)
|
||||||
return session.also {
|
return session.also {
|
||||||
|
// most of the test was created pre-MSC3061 so ensure compatibility
|
||||||
|
it.cryptoService().enableShareKeyOnInvite(false)
|
||||||
trackedSessions.add(session)
|
trackedSessions.add(session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -428,16 +468,26 @@ class CommonTestHelper private constructor(context: Context) {
|
||||||
* @param latch
|
* @param latch
|
||||||
* @throws InterruptedException
|
* @throws InterruptedException
|
||||||
*/
|
*/
|
||||||
fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis) {
|
fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis, job: Job? = null) {
|
||||||
assertTrue(
|
assertTrue(
|
||||||
"Timed out after " + timeout + "ms waiting for something to happen. See stacktrace for cause.",
|
"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)) {
|
suspend fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
|
||||||
while (true) {
|
while (true) {
|
||||||
delay(1000)
|
try {
|
||||||
|
delay(1000)
|
||||||
|
} catch (ex: CancellationException) {
|
||||||
|
// the job was canceled, just stop
|
||||||
|
return
|
||||||
|
}
|
||||||
if (condition()) {
|
if (condition()) {
|
||||||
latch.countDown()
|
latch.countDown()
|
||||||
return
|
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) {
|
fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, dispatcher: CoroutineDispatcher = Dispatchers.Main, block: suspend (CountDownLatch) -> Unit) {
|
||||||
val latch = CountDownLatch(1)
|
val latch = CountDownLatch(1)
|
||||||
coroutineScope.launch(dispatcher) {
|
val job = coroutineScope.launch(dispatcher) {
|
||||||
block(latch)
|
block(latch)
|
||||||
}
|
}
|
||||||
await(latch, timeout)
|
await(latch, timeout, job)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> runBlockingTest(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T {
|
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.getRoom
|
||||||
import org.matrix.android.sdk.api.session.room.Room
|
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.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.RoomSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||||
|
@ -76,11 +77,14 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
|
||||||
/**
|
/**
|
||||||
* @return alice session
|
* @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 aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
|
||||||
|
|
||||||
val roomId = testHelper.runBlockingTest {
|
val roomId = testHelper.runBlockingTest {
|
||||||
aliceSession.roomService().createRoom(CreateRoomParams().apply { name = "MyRoom" })
|
aliceSession.roomService().createRoom(CreateRoomParams().apply {
|
||||||
|
historyVisibility = roomHistoryVisibility
|
||||||
|
name = "MyRoom"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (encryptedRoom) {
|
if (encryptedRoom) {
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
|
@ -104,8 +108,8 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
|
||||||
/**
|
/**
|
||||||
* @return alice and bob sessions
|
* @return alice and bob sessions
|
||||||
*/
|
*/
|
||||||
fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true): CryptoTestData {
|
fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData {
|
||||||
val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom)
|
val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom, roomHistoryVisibility)
|
||||||
val aliceSession = cryptoTestData.firstSession
|
val aliceSession = cryptoTestData.firstSession
|
||||||
val aliceRoomId = cryptoTestData.roomId
|
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.amshove.kluent.internal.assertEquals
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.FixMethodOrder
|
import org.junit.FixMethodOrder
|
||||||
import org.junit.Ignore
|
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
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.content.WithHeldCode
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.getRoom
|
import org.matrix.android.sdk.api.session.getRoom
|
||||||
import org.matrix.android.sdk.api.session.getRoomSummary
|
|
||||||
import org.matrix.android.sdk.api.session.room.Room
|
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.getTimelineEvent
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
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.model.message.MessageContent
|
||||||
|
@ -67,10 +64,10 @@ import org.matrix.android.sdk.common.TestMatrixCallback
|
||||||
import org.matrix.android.sdk.mustFail
|
import org.matrix.android.sdk.mustFail
|
||||||
import java.util.concurrent.CountDownLatch
|
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)
|
@RunWith(JUnit4::class)
|
||||||
@FixMethodOrder(MethodSorters.JVM)
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
@LargeTest
|
@LargeTest
|
||||||
@Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.")
|
|
||||||
class E2eeSanityTests : InstrumentedTest {
|
class E2eeSanityTests : InstrumentedTest {
|
||||||
|
|
||||||
@get:Rule val rule = RetryTestRule(3)
|
@get:Rule val rule = RetryTestRule(3)
|
||||||
|
@ -115,7 +112,7 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
|
|
||||||
// All user should accept invite
|
// All user should accept invite
|
||||||
otherAccounts.forEach { otherSession ->
|
otherAccounts.forEach { otherSession ->
|
||||||
waitForAndAcceptInviteInRoom(testHelper, otherSession, e2eRoomID)
|
testHelper.waitForAndAcceptInviteInRoom(otherSession, e2eRoomID)
|
||||||
Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
|
Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,7 +153,7 @@ class E2eeSanityTests : InstrumentedTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
newAccount.forEach {
|
newAccount.forEach {
|
||||||
waitForAndAcceptInviteInRoom(testHelper, it, e2eRoomID)
|
testHelper.waitForAndAcceptInviteInRoom(it, e2eRoomID)
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureMembersHaveJoined(testHelper, aliceSession, newAccount, 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) {
|
private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List<String>, session: Session, e2eRoomID: String) {
|
||||||
testHelper.waitWithLatch { latch ->
|
testHelper.waitWithLatch { latch ->
|
||||||
sentEventIds.forEach { sentEventId ->
|
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)
|
assertNotNull("Bob should have received and decrypted a room key event from alice", bobInboundForAlice)
|
||||||
assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId)
|
assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId)
|
||||||
|
|
||||||
val megolmSessionId = bobInboundForAlice.olmInboundGroupSession!!.sessionIdentifier()
|
val megolmSessionId = bobInboundForAlice.session.sessionIdentifier()
|
||||||
|
|
||||||
assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId)
|
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.api.session.Session
|
||||||
import org.matrix.android.sdk.common.CommonTestHelper
|
import org.matrix.android.sdk.common.CommonTestHelper
|
||||||
import org.matrix.android.sdk.common.CryptoTestData
|
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]
|
* Data class to store result of [KeysBackupTestHelper.createKeysBackupScenarioWithPassword]
|
||||||
*/
|
*/
|
||||||
internal data class KeysBackupScenarioData(
|
internal data class KeysBackupScenarioData(
|
||||||
val cryptoTestData: CryptoTestData,
|
val cryptoTestData: CryptoTestData,
|
||||||
val aliceKeys: List<OlmInboundGroupSessionWrapper2>,
|
val aliceKeys: List<MXInboundMegolmSessionWrapper>,
|
||||||
val prepareKeysBackupDataResult: PrepareKeysBackupDataResult,
|
val prepareKeysBackupDataResult: PrepareKeysBackupDataResult,
|
||||||
val aliceSession2: Session
|
val aliceSession2: Session
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -301,7 +301,7 @@ class KeysBackupTest : InstrumentedTest {
|
||||||
val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo
|
val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo
|
||||||
|
|
||||||
// - Check encryptGroupSession() returns stg
|
// - Check encryptGroupSession() returns stg
|
||||||
val keyBackupData = keysBackup.encryptGroupSession(session)
|
val keyBackupData = testHelper.runBlockingTest { keysBackup.encryptGroupSession(session) }
|
||||||
assertNotNull(keyBackupData)
|
assertNotNull(keyBackupData)
|
||||||
assertNotNull(keyBackupData!!.sessionData)
|
assertNotNull(keyBackupData!!.sessionData)
|
||||||
|
|
||||||
|
@ -312,7 +312,7 @@ class KeysBackupTest : InstrumentedTest {
|
||||||
val sessionData = keysBackup
|
val sessionData = keysBackup
|
||||||
.decryptKeyBackupData(
|
.decryptKeyBackupData(
|
||||||
keyBackupData,
|
keyBackupData,
|
||||||
session.olmInboundGroupSession!!.sessionIdentifier(),
|
session.safeSessionId!!,
|
||||||
cryptoTestData.roomId,
|
cryptoTestData.roomId,
|
||||||
decryption!!
|
decryption!!
|
||||||
)
|
)
|
||||||
|
|
|
@ -187,7 +187,7 @@ internal class KeysBackupTestHelper(
|
||||||
// - Alice must have the same keys on both devices
|
// - Alice must have the same keys on both devices
|
||||||
for (aliceKey1 in testData.aliceKeys) {
|
for (aliceKey1 in testData.aliceKeys) {
|
||||||
val aliceKey2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
|
val aliceKey2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
|
||||||
.getInboundGroupSession(aliceKey1.olmInboundGroupSession!!.sessionIdentifier(), aliceKey1.senderKey!!)
|
.getInboundGroupSession(aliceKey1.safeSessionId!!, aliceKey1.senderKey!!)
|
||||||
Assert.assertNotNull(aliceKey2)
|
Assert.assertNotNull(aliceKey2)
|
||||||
assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys())
|
assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys())
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,19 +56,17 @@ class SpaceCreationTest : InstrumentedTest {
|
||||||
val roomName = "My Space"
|
val roomName = "My Space"
|
||||||
val topic = "A public space for test"
|
val topic = "A public space for test"
|
||||||
var spaceId: String = ""
|
var spaceId: String = ""
|
||||||
commonTestHelper.waitWithLatch {
|
commonTestHelper.runBlockingTest {
|
||||||
spaceId = session.spaceService().createSpace(roomName, topic, null, true)
|
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.waitWithLatch {
|
||||||
commonTestHelper.retryPeriodicallyWithLatch(it) {
|
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 name should be set", roomName, syncedSpace?.asRoom()?.roomSummary()?.name)
|
||||||
assertEquals("Room topic should be set", topic, syncedSpace?.asRoom()?.roomSummary()?.topic)
|
assertEquals("Room topic should be set", topic, syncedSpace?.asRoom()?.roomSummary()?.topic)
|
||||||
// assertEquals(topic, syncedSpace.asRoom().roomSummary()?., "Room topic should be set")
|
// assertEquals(topic, syncedSpace.asRoom().roomSummary()?., "Room topic should be set")
|
||||||
|
|
|
@ -20,7 +20,6 @@ import android.util.Log
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotNull
|
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.FixMethodOrder
|
import org.junit.FixMethodOrder
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
|
@ -62,47 +61,40 @@ class SpaceHierarchyTest : InstrumentedTest {
|
||||||
val spaceName = "My Space"
|
val spaceName = "My Space"
|
||||||
val topic = "A public space for test"
|
val topic = "A public space for test"
|
||||||
var spaceId = ""
|
var spaceId = ""
|
||||||
commonTestHelper.waitWithLatch {
|
commonTestHelper.runBlockingTest {
|
||||||
spaceId = session.spaceService().createSpace(spaceName, topic, null, true)
|
spaceId = session.spaceService().createSpace(spaceName, topic, null, true)
|
||||||
it.countDown()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val syncedSpace = session.spaceService().getSpace(spaceId)
|
val syncedSpace = session.spaceService().getSpace(spaceId)
|
||||||
|
|
||||||
var roomId = ""
|
var roomId = ""
|
||||||
commonTestHelper.waitWithLatch {
|
commonTestHelper.runBlockingTest {
|
||||||
roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" })
|
roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" })
|
||||||
it.countDown()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||||
|
|
||||||
commonTestHelper.waitWithLatch {
|
commonTestHelper.runBlockingTest {
|
||||||
syncedSpace!!.addChildren(roomId, viaServers, null, true)
|
syncedSpace!!.addChildren(roomId, viaServers, null, true)
|
||||||
it.countDown()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commonTestHelper.waitWithLatch {
|
commonTestHelper.runBlockingTest {
|
||||||
session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers)
|
session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers)
|
||||||
it.countDown()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Thread.sleep(9000)
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
|
commonTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents
|
val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents
|
||||||
val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true }
|
val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true }
|
||||||
|
parents?.forEach {
|
||||||
parents?.forEach {
|
Log.d("## TEST", "parent : $it")
|
||||||
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
|
// @Test
|
||||||
|
@ -173,52 +165,55 @@ class SpaceHierarchyTest : InstrumentedTest {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testFilteringBySpace() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
|
fun testFilteringBySpace() = runSessionTest(context()) { commonTestHelper ->
|
||||||
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
|
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
|
||||||
|
|
||||||
val spaceAInfo = createPublicSpace(
|
val spaceAInfo = createPublicSpace(
|
||||||
session, "SpaceA", listOf(
|
commonTestHelper,
|
||||||
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
session, "SpaceA",
|
||||||
Triple("A2", true, true)
|
listOf(
|
||||||
)
|
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
||||||
|
Triple("A2", true, true)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
/* val spaceBInfo = */ createPublicSpace(
|
/* val spaceBInfo = */ createPublicSpace(
|
||||||
session, "SpaceB", listOf(
|
commonTestHelper,
|
||||||
Triple("B1", true /*auto-join*/, true/*canonical*/),
|
session, "SpaceB",
|
||||||
Triple("B2", true, true),
|
listOf(
|
||||||
Triple("B3", true, true)
|
Triple("B1", true /*auto-join*/, true/*canonical*/),
|
||||||
)
|
Triple("B2", true, true),
|
||||||
|
Triple("B3", true, true)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val spaceCInfo = createPublicSpace(
|
val spaceCInfo = createPublicSpace(
|
||||||
session, "SpaceC", listOf(
|
commonTestHelper,
|
||||||
Triple("C1", true /*auto-join*/, true/*canonical*/),
|
session, "SpaceC",
|
||||||
Triple("C2", true, true)
|
listOf(
|
||||||
)
|
Triple("C1", true /*auto-join*/, true/*canonical*/),
|
||||||
|
Triple("C2", true, true)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// add C as a subspace of A
|
// add C as a subspace of A
|
||||||
val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
|
val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
|
||||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||||
commonTestHelper.waitWithLatch {
|
commonTestHelper.runBlockingTest {
|
||||||
spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
|
spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
|
||||||
session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
|
session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
|
||||||
it.countDown()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create orphan rooms
|
// Create orphan rooms
|
||||||
|
|
||||||
var orphan1 = ""
|
var orphan1 = ""
|
||||||
commonTestHelper.waitWithLatch {
|
commonTestHelper.runBlockingTest {
|
||||||
orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" })
|
orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" })
|
||||||
it.countDown()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var orphan2 = ""
|
var orphan2 = ""
|
||||||
commonTestHelper.waitWithLatch {
|
commonTestHelper.runBlockingTest {
|
||||||
orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" })
|
orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" })
|
||||||
it.countDown()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val allRooms = session.roomService().getRoomSummaries(roomSummaryQueryParams { excludeType = listOf(RoomType.SPACE) })
|
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" })
|
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
|
// 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" })
|
val a3 = session.roomService().createRoom(CreateRoomParams().apply { name = "A3" })
|
||||||
spaceA!!.addChildren(a3, viaServers, null, false)
|
spaceA!!.addChildren(a3, viaServers, null, false)
|
||||||
it.countDown()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Thread.sleep(6_000)
|
Thread.sleep(6_000)
|
||||||
|
@ -255,37 +249,39 @@ class SpaceHierarchyTest : InstrumentedTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore("This test will be ignored until it is fixed")
|
@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 session = commonTestHelper.createAccount("John", SessionTestParams(true))
|
||||||
|
|
||||||
val spaceAInfo = createPublicSpace(
|
val spaceAInfo = createPublicSpace(
|
||||||
session, "SpaceA", listOf(
|
commonTestHelper,
|
||||||
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
session, "SpaceA",
|
||||||
Triple("A2", true, true)
|
listOf(
|
||||||
)
|
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
||||||
|
Triple("A2", true, true)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val spaceCInfo = createPublicSpace(
|
val spaceCInfo = createPublicSpace(
|
||||||
session, "SpaceC", listOf(
|
commonTestHelper,
|
||||||
Triple("C1", true /*auto-join*/, true/*canonical*/),
|
session, "SpaceC",
|
||||||
Triple("C2", true, true)
|
listOf(
|
||||||
)
|
Triple("C1", true /*auto-join*/, true/*canonical*/),
|
||||||
|
Triple("C2", true, true)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// add C as a subspace of A
|
// add C as a subspace of A
|
||||||
val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
|
val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
|
||||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||||
commonTestHelper.waitWithLatch {
|
commonTestHelper.runBlockingTest {
|
||||||
spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
|
spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
|
||||||
session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
|
session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
|
||||||
it.countDown()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add back A as subspace of C
|
// add back A as subspace of C
|
||||||
commonTestHelper.waitWithLatch {
|
commonTestHelper.runBlockingTest {
|
||||||
val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId)
|
val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId)
|
||||||
spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true)
|
spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true)
|
||||||
it.countDown()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A -> C -> A
|
// A -> C -> A
|
||||||
|
@ -300,37 +296,46 @@ class SpaceHierarchyTest : InstrumentedTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testLiveFlatChildren() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
|
fun testLiveFlatChildren() = runSessionTest(context()) { commonTestHelper ->
|
||||||
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
|
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
|
||||||
|
|
||||||
val spaceAInfo = createPublicSpace(
|
val spaceAInfo = createPublicSpace(
|
||||||
session, "SpaceA", listOf(
|
commonTestHelper,
|
||||||
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
session,
|
||||||
Triple("A2", true, true)
|
"SpaceA",
|
||||||
)
|
listOf(
|
||||||
|
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
||||||
|
Triple("A2", true, true)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val spaceBInfo = createPublicSpace(
|
val spaceBInfo = createPublicSpace(
|
||||||
session, "SpaceB", listOf(
|
commonTestHelper,
|
||||||
Triple("B1", true /*auto-join*/, true/*canonical*/),
|
session,
|
||||||
Triple("B2", true, true),
|
"SpaceB",
|
||||||
Triple("B3", true, true)
|
listOf(
|
||||||
)
|
Triple("B1", true /*auto-join*/, true/*canonical*/),
|
||||||
|
Triple("B2", true, true),
|
||||||
|
Triple("B3", true, true)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// add B as a subspace of A
|
// add B as a subspace of A
|
||||||
val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
|
val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
|
||||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||||
runBlocking {
|
commonTestHelper.runBlockingTest {
|
||||||
spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true)
|
spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true)
|
||||||
session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
|
session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
|
||||||
}
|
}
|
||||||
|
|
||||||
val spaceCInfo = createPublicSpace(
|
val spaceCInfo = createPublicSpace(
|
||||||
session, "SpaceC", listOf(
|
commonTestHelper,
|
||||||
Triple("C1", true /*auto-join*/, true/*canonical*/),
|
session,
|
||||||
Triple("C2", true, true)
|
"SpaceC",
|
||||||
)
|
listOf(
|
||||||
|
Triple("C1", true /*auto-join*/, true/*canonical*/),
|
||||||
|
Triple("C2", true, true)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
commonTestHelper.waitWithLatch { latch ->
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
|
@ -348,13 +353,13 @@ class SpaceHierarchyTest : InstrumentedTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flatAChildren.observeForever(childObserver)
|
||||||
|
|
||||||
// add C as subspace of B
|
// add C as subspace of B
|
||||||
val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
|
val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
|
||||||
spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
|
spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
|
||||||
|
|
||||||
// C1 and C2 should be in flatten child of A now
|
// C1 and C2 should be in flatten child of A now
|
||||||
|
|
||||||
flatAChildren.observeForever(childObserver)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test part one of the rooms
|
// 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
|
// The room should have disapear from flat children
|
||||||
flatAChildren.observeForever(childObserver)
|
flatAChildren.observeForever(childObserver)
|
||||||
|
// part from b room
|
||||||
|
session.roomService().leaveRoom(bRoomId)
|
||||||
}
|
}
|
||||||
commonTestHelper.signOutAndClose(session)
|
commonTestHelper.signOutAndClose(session)
|
||||||
}
|
}
|
||||||
|
@ -388,6 +393,7 @@ class SpaceHierarchyTest : InstrumentedTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun createPublicSpace(
|
private fun createPublicSpace(
|
||||||
|
commonTestHelper: CommonTestHelper,
|
||||||
session: Session,
|
session: Session,
|
||||||
spaceName: String,
|
spaceName: String,
|
||||||
childInfo: List<Triple<String, Boolean, Boolean?>>
|
childInfo: List<Triple<String, Boolean, Boolean?>>
|
||||||
|
@ -395,29 +401,27 @@ class SpaceHierarchyTest : InstrumentedTest {
|
||||||
): TestSpaceCreationResult {
|
): TestSpaceCreationResult {
|
||||||
var spaceId = ""
|
var spaceId = ""
|
||||||
var roomIds: List<String> = emptyList()
|
var roomIds: List<String> = emptyList()
|
||||||
runSessionTest(context()) { commonTestHelper ->
|
commonTestHelper.runBlockingTest {
|
||||||
commonTestHelper.waitWithLatch { latch ->
|
spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
|
||||||
spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
|
val syncedSpace = session.spaceService().getSpace(spaceId)
|
||||||
val syncedSpace = session.spaceService().getSpace(spaceId)
|
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
|
||||||
|
|
||||||
roomIds = childInfo.map { entry ->
|
roomIds = childInfo.map { entry ->
|
||||||
session.roomService().createRoom(CreateRoomParams().apply { name = entry.first })
|
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)
|
return TestSpaceCreationResult(spaceId, roomIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createPrivateSpace(
|
private fun createPrivateSpace(
|
||||||
|
commonTestHelper: CommonTestHelper,
|
||||||
session: Session,
|
session: Session,
|
||||||
spaceName: String,
|
spaceName: String,
|
||||||
childInfo: List<Triple<String, Boolean, Boolean?>>
|
childInfo: List<Triple<String, Boolean, Boolean?>>
|
||||||
|
@ -425,34 +429,31 @@ class SpaceHierarchyTest : InstrumentedTest {
|
||||||
): TestSpaceCreationResult {
|
): TestSpaceCreationResult {
|
||||||
var spaceId = ""
|
var spaceId = ""
|
||||||
var roomIds: List<String> = emptyList()
|
var roomIds: List<String> = emptyList()
|
||||||
runSessionTest(context()) { commonTestHelper ->
|
commonTestHelper.runBlockingTest {
|
||||||
commonTestHelper.waitWithLatch { latch ->
|
spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false)
|
||||||
spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false)
|
val syncedSpace = session.spaceService().getSpace(spaceId)
|
||||||
val syncedSpace = session.spaceService().getSpace(spaceId)
|
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
roomIds =
|
||||||
roomIds =
|
childInfo.map { entry ->
|
||||||
childInfo.map { entry ->
|
val homeServerCapabilities = session
|
||||||
val homeServerCapabilities = session
|
.homeServerCapabilitiesService()
|
||||||
.homeServerCapabilitiesService()
|
.getHomeServerCapabilities()
|
||||||
.getHomeServerCapabilities()
|
session.roomService().createRoom(CreateRoomParams().apply {
|
||||||
session.roomService().createRoom(CreateRoomParams().apply {
|
name = entry.first
|
||||||
name = entry.first
|
this.featurePreset = RestrictedRoomPreset(
|
||||||
this.featurePreset = RestrictedRoomPreset(
|
homeServerCapabilities,
|
||||||
homeServerCapabilities,
|
listOf(
|
||||||
listOf(
|
RoomJoinRulesAllowEntry.restrictedToRoom(spaceId)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
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)
|
return TestSpaceCreationResult(spaceId, roomIds)
|
||||||
|
@ -463,25 +464,31 @@ class SpaceHierarchyTest : InstrumentedTest {
|
||||||
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
|
val session = commonTestHelper.createAccount("John", SessionTestParams(true))
|
||||||
|
|
||||||
/* val spaceAInfo = */ createPublicSpace(
|
/* val spaceAInfo = */ createPublicSpace(
|
||||||
session, "SpaceA", listOf(
|
commonTestHelper,
|
||||||
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
session, "SpaceA",
|
||||||
Triple("A2", true, true)
|
listOf(
|
||||||
)
|
Triple("A1", true /*auto-join*/, true/*canonical*/),
|
||||||
|
Triple("A2", true, true)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val spaceBInfo = createPublicSpace(
|
val spaceBInfo = createPublicSpace(
|
||||||
session, "SpaceB", listOf(
|
commonTestHelper,
|
||||||
Triple("B1", true /*auto-join*/, true/*canonical*/),
|
session, "SpaceB",
|
||||||
Triple("B2", true, true),
|
listOf(
|
||||||
Triple("B3", true, true)
|
Triple("B1", true /*auto-join*/, true/*canonical*/),
|
||||||
)
|
Triple("B2", true, true),
|
||||||
|
Triple("B3", true, true)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val spaceCInfo = createPublicSpace(
|
val spaceCInfo = createPublicSpace(
|
||||||
session, "SpaceC", listOf(
|
commonTestHelper,
|
||||||
Triple("C1", true /*auto-join*/, true/*canonical*/),
|
session, "SpaceC",
|
||||||
Triple("C2", true, true)
|
listOf(
|
||||||
)
|
Triple("C1", true /*auto-join*/, true/*canonical*/),
|
||||||
|
Triple("C2", true, true)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
|
||||||
|
@ -490,7 +497,6 @@ class SpaceHierarchyTest : InstrumentedTest {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
|
val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
|
||||||
spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
|
spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
|
||||||
Thread.sleep(6_000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thread.sleep(4_000)
|
// Thread.sleep(4_000)
|
||||||
|
@ -501,11 +507,12 @@ class SpaceHierarchyTest : InstrumentedTest {
|
||||||
// + C
|
// + C
|
||||||
// + c1, c2
|
// + c1, c2
|
||||||
|
|
||||||
val rootSpaces = commonTestHelper.runBlockingTest {
|
commonTestHelper.waitWithLatch { latch ->
|
||||||
session.spaceService().getRootSpaceSummaries()
|
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
|
@Test
|
||||||
|
@ -514,10 +521,12 @@ class SpaceHierarchyTest : InstrumentedTest {
|
||||||
val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true))
|
val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true))
|
||||||
|
|
||||||
val spaceAInfo = createPrivateSpace(
|
val spaceAInfo = createPrivateSpace(
|
||||||
aliceSession, "Private Space A", listOf(
|
commonTestHelper,
|
||||||
Triple("General", true /*suggested*/, true/*canonical*/),
|
aliceSession, "Private Space A",
|
||||||
Triple("Random", true, true)
|
listOf(
|
||||||
)
|
Triple("General", true /*suggested*/, true/*canonical*/),
|
||||||
|
Triple("Random", true, true)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
commonTestHelper.runBlockingTest {
|
commonTestHelper.runBlockingTest {
|
||||||
|
@ -529,10 +538,9 @@ class SpaceHierarchyTest : InstrumentedTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
var bobRoomId = ""
|
var bobRoomId = ""
|
||||||
commonTestHelper.waitWithLatch {
|
commonTestHelper.runBlockingTest {
|
||||||
bobRoomId = bobSession.roomService().createRoom(CreateRoomParams().apply { name = "A Bob Room" })
|
bobRoomId = bobSession.roomService().createRoom(CreateRoomParams().apply { name = "A Bob Room" })
|
||||||
bobSession.getRoom(bobRoomId)!!.membershipService().invite(aliceSession.myUserId)
|
bobSession.getRoom(bobRoomId)!!.membershipService().invite(aliceSession.myUserId)
|
||||||
it.countDown()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commonTestHelper.runBlockingTest {
|
commonTestHelper.runBlockingTest {
|
||||||
|
@ -545,9 +553,8 @@ class SpaceHierarchyTest : InstrumentedTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commonTestHelper.waitWithLatch {
|
commonTestHelper.runBlockingTest {
|
||||||
bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: ""))
|
bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: ""))
|
||||||
it.countDown()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commonTestHelper.waitWithLatch { latch ->
|
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
|
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.
|
* This data class holds necessary data to open a session.
|
||||||
* You don't have to manually instantiate it.
|
* 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.
|
* 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
|
* 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`
|
* You can limit request only to your sessions by turning this setting to `true`
|
||||||
*/
|
*/
|
||||||
val limitRoomKeyRequestsToMyDevices: Boolean = false,
|
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.Content
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
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.RoomKeyWithHeldContent
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.SessionInfo
|
||||||
|
|
||||||
interface CryptoService {
|
interface CryptoService {
|
||||||
|
|
||||||
|
@ -84,6 +85,20 @@ interface CryptoService {
|
||||||
|
|
||||||
fun isKeyGossipingEnabled(): Boolean
|
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 setRoomUnBlacklistUnverifiedDevices(roomId: String)
|
||||||
|
|
||||||
fun getDeviceTrackingStatus(userId: String): Int
|
fun getDeviceTrackingStatus(userId: String): Int
|
||||||
|
@ -176,4 +191,9 @@ interface CryptoService {
|
||||||
* send, in order to speed up sending of the message.
|
* send, in order to speed up sending of the message.
|
||||||
*/
|
*/
|
||||||
fun prepareToEncrypt(roomId: String, callback: MatrixCallback<Unit>)
|
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.
|
* private part of this key unless they have done device verification.
|
||||||
*/
|
*/
|
||||||
@Json(name = "sender_claimed_ed25519_key")
|
@Json(name = "sender_claimed_ed25519_key")
|
||||||
val senderClaimedEd25519Key: String? = null
|
val senderClaimedEd25519Key: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
// should be a Long but it is sometimes a double
|
||||||
@Json(name = "chain_index")
|
@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,9 +48,10 @@ interface LocationSharingService {
|
||||||
/**
|
/**
|
||||||
* Starts sharing live location in the room.
|
* Starts sharing live location in the room.
|
||||||
* @param timeoutMillis timeout of the live in milliseconds
|
* @param timeoutMillis timeout of the live in milliseconds
|
||||||
|
* @param description description of the live for text fallback
|
||||||
* @return the result of the update of the live
|
* @return the result of the update of the live
|
||||||
*/
|
*/
|
||||||
suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult
|
suspend fun startLiveLocationShare(timeoutMillis: Long, description: String): UpdateLiveLocationShareResult
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops sharing live location in the room.
|
* Stops sharing live location in the room.
|
||||||
|
|
|
@ -48,3 +48,9 @@ enum class RoomHistoryVisibility {
|
||||||
*/
|
*/
|
||||||
@Json(name = "joined") JOINED
|
@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
|
@Binds
|
||||||
abstract fun bindSessionCreator(creator: DefaultSessionCreator): SessionCreator
|
abstract fun bindSessionCreator(creator: DefaultSessionCreator): SessionCreator
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindSessionParamsCreator(creator: DefaultSessionParamsCreator): SessionParamsCreator
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindDirectLoginTask(task: DefaultDirectLoginTask): DirectLoginTask
|
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
|
||||||
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
|
||||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
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.Credentials
|
||||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||||
import org.matrix.android.sdk.api.auth.data.LoginFlowResult
|
import org.matrix.android.sdk.api.auth.data.LoginFlowResult
|
||||||
|
@ -361,7 +362,7 @@ internal class DefaultAuthenticationService @Inject constructor(
|
||||||
homeServerConnectionConfig: HomeServerConnectionConfig,
|
homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||||
credentials: Credentials
|
credentials: Credentials
|
||||||
): Session {
|
): Session {
|
||||||
return sessionCreator.createSession(credentials, homeServerConnectionConfig)
|
return sessionCreator.createSession(credentials, homeServerConnectionConfig, LoginType.SSO)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getWellKnownData(
|
override suspend fun getWellKnownData(
|
||||||
|
|
|
@ -16,69 +16,41 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.auth
|
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.Credentials
|
||||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.internal.SessionManager
|
import org.matrix.android.sdk.internal.SessionManager
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal interface SessionCreator {
|
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(
|
internal class DefaultSessionCreator @Inject constructor(
|
||||||
private val sessionParamsStore: SessionParamsStore,
|
private val sessionParamsStore: SessionParamsStore,
|
||||||
private val sessionManager: SessionManager,
|
private val sessionManager: SessionManager,
|
||||||
private val pendingSessionStore: PendingSessionStore,
|
private val pendingSessionStore: PendingSessionStore,
|
||||||
private val isValidClientServerApiTask: IsValidClientServerApiTask
|
private val sessionParamsCreator: SessionParamsCreator,
|
||||||
) : SessionCreator {
|
) : SessionCreator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Credentials can affect the homeServerConnectionConfig, override homeserver url and/or
|
* Credentials can affect the homeServerConnectionConfig, override homeserver url and/or
|
||||||
* identity server url if provided in the credentials.
|
* 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
|
// We can cleanup the pending session params
|
||||||
pendingSessionStore.delete()
|
pendingSessionStore.delete()
|
||||||
|
val sessionParams = sessionParamsCreator.create(credentials, homeServerConnectionConfig, loginType)
|
||||||
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)
|
|
||||||
|
|
||||||
sessionParamsStore.save(sessionParams)
|
sessionParamsStore.save(sessionParams)
|
||||||
return sessionManager.getOrCreateSession(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.MigrateAuthTo002
|
||||||
import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo003
|
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.MigrateAuthTo004
|
||||||
|
import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo005
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ internal class AuthRealmMigration @Inject constructor() : RealmMigration {
|
||||||
override fun equals(other: Any?) = other is AuthRealmMigration
|
override fun equals(other: Any?) = other is AuthRealmMigration
|
||||||
override fun hashCode() = 4000
|
override fun hashCode() = 4000
|
||||||
|
|
||||||
val schemaVersion = 4L
|
val schemaVersion = 5L
|
||||||
|
|
||||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||||
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
|
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 < 2) MigrateAuthTo002(realm).perform()
|
||||||
if (oldVersion < 3) MigrateAuthTo003(realm).perform()
|
if (oldVersion < 3) MigrateAuthTo003(realm).perform()
|
||||||
if (oldVersion < 4) MigrateAuthTo004(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 = "",
|
var homeServerConnectionConfigJson: String = "",
|
||||||
// Set to false when the token is invalid and the user has been soft logged out
|
// 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
|
// In case of hard logout, this object is deleted from DB
|
||||||
var isTokenValid: Boolean = true
|
var isTokenValid: Boolean = true,
|
||||||
|
var loginType: String = "",
|
||||||
) : RealmObject()
|
) : RealmObject()
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package org.matrix.android.sdk.internal.auth.db
|
package org.matrix.android.sdk.internal.auth.db
|
||||||
|
|
||||||
import com.squareup.moshi.Moshi
|
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.Credentials
|
||||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
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) {
|
if (credentials == null || homeServerConnectionConfig == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid)
|
return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid, LoginType.fromName(entity.loginType))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun map(sessionParams: SessionParams?): SessionParamsEntity? {
|
fun map(sessionParams: SessionParams?): SessionParamsEntity? {
|
||||||
|
@ -54,7 +55,8 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
|
||||||
sessionParams.userId,
|
sessionParams.userId,
|
||||||
credentialsJson,
|
credentialsJson,
|
||||||
homeServerConnectionConfigJson,
|
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
|
package org.matrix.android.sdk.internal.auth.login
|
||||||
|
|
||||||
import android.util.Patterns
|
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.LoginProfileInfo
|
||||||
import org.matrix.android.sdk.api.auth.login.LoginWizard
|
import org.matrix.android.sdk.api.auth.login.LoginWizard
|
||||||
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
||||||
|
@ -78,7 +79,7 @@ internal class DefaultLoginWizard(
|
||||||
authAPI.login(loginParams)
|
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)
|
authAPI.login(loginParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
|
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, LoginType.SSO)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun loginCustom(data: JsonDict): Session {
|
override suspend fun loginCustom(data: JsonDict): Session {
|
||||||
|
@ -100,7 +101,7 @@ internal class DefaultLoginWizard(
|
||||||
authAPI.login(data)
|
authAPI.login(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
|
return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, LoginType.CUSTOM)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun resetPassword(email: String) {
|
override suspend fun resetPassword(email: String) {
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.auth.login
|
||||||
|
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import okhttp3.OkHttpClient
|
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.auth.data.HomeServerConnectionConfig
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.session.Session
|
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 {
|
private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient {
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package org.matrix.android.sdk.internal.auth.registration
|
package org.matrix.android.sdk.internal.auth.registration
|
||||||
|
|
||||||
import kotlinx.coroutines.delay
|
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.Credentials
|
||||||
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
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.
|
* This class execute the registration request and is responsible to keep the session of interactive authentication.
|
||||||
*/
|
*/
|
||||||
internal class DefaultRegistrationWizard(
|
internal class DefaultRegistrationWizard(
|
||||||
authAPI: AuthAPI,
|
authAPI: AuthAPI,
|
||||||
private val sessionCreator: SessionCreator,
|
private val sessionCreator: SessionCreator,
|
||||||
private val pendingSessionStore: PendingSessionStore
|
private val pendingSessionStore: PendingSessionStore
|
||||||
) : RegistrationWizard {
|
) : RegistrationWizard {
|
||||||
|
|
||||||
private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
|
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 {
|
override suspend fun getRegistrationFlow(): RegistrationResult {
|
||||||
val params = RegistrationParams()
|
val params = RegistrationParams()
|
||||||
return performRegistrationRequest(params)
|
return performRegistrationRequest(params, LoginType.PASSWORD)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun createAccount(
|
override suspend fun createAccount(
|
||||||
|
@ -73,43 +74,43 @@ internal class DefaultRegistrationWizard(
|
||||||
initialDeviceDisplayName: String?
|
initialDeviceDisplayName: String?
|
||||||
): RegistrationResult {
|
): RegistrationResult {
|
||||||
val params = RegistrationParams(
|
val params = RegistrationParams(
|
||||||
username = userName,
|
username = userName,
|
||||||
password = password,
|
password = password,
|
||||||
initialDeviceDisplayName = initialDeviceDisplayName
|
initialDeviceDisplayName = initialDeviceDisplayName
|
||||||
)
|
)
|
||||||
return performRegistrationRequest(params)
|
return performRegistrationRequest(params, LoginType.PASSWORD)
|
||||||
.also {
|
.also {
|
||||||
pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true)
|
pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true)
|
||||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun performReCaptcha(response: String): RegistrationResult {
|
override suspend fun performReCaptcha(response: String): RegistrationResult {
|
||||||
val safeSession = pendingSessionData.currentSession
|
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))
|
val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response))
|
||||||
return performRegistrationRequest(params)
|
return performRegistrationRequest(params, LoginType.PASSWORD)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun acceptTerms(): RegistrationResult {
|
override suspend fun acceptTerms(): RegistrationResult {
|
||||||
val safeSession = pendingSessionData.currentSession
|
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))
|
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 {
|
override suspend fun addThreePid(threePid: RegisterThreePid): RegistrationResult {
|
||||||
pendingSessionData = pendingSessionData.copy(currentThreePidData = null)
|
pendingSessionData = pendingSessionData.copy(currentThreePidData = null)
|
||||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||||
|
|
||||||
return sendThreePid(threePid)
|
return sendThreePid(threePid)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun sendAgainThreePid(): RegistrationResult {
|
override suspend fun sendAgainThreePid(): RegistrationResult {
|
||||||
val safeCurrentThreePid = pendingSessionData.currentThreePidData?.threePid
|
val safeCurrentThreePid = pendingSessionData.currentThreePidData?.threePid
|
||||||
?: throw IllegalStateException("developer error, call createAccount() method first")
|
?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||||
|
|
||||||
return sendThreePid(safeCurrentThreePid)
|
return sendThreePid(safeCurrentThreePid)
|
||||||
}
|
}
|
||||||
|
@ -125,7 +126,7 @@ internal class DefaultRegistrationWizard(
|
||||||
)
|
)
|
||||||
|
|
||||||
pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1)
|
pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1)
|
||||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||||
|
|
||||||
val params = RegistrationParams(
|
val params = RegistrationParams(
|
||||||
auth = if (threePid is RegisterThreePid.Email) {
|
auth = if (threePid is RegisterThreePid.Email) {
|
||||||
|
@ -148,17 +149,17 @@ internal class DefaultRegistrationWizard(
|
||||||
)
|
)
|
||||||
// Store data
|
// Store data
|
||||||
pendingSessionData = pendingSessionData.copy(currentThreePidData = ThreePidData.from(threePid, response, params))
|
pendingSessionData = pendingSessionData.copy(currentThreePidData = ThreePidData.from(threePid, response, params))
|
||||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||||
|
|
||||||
// and send the sid a first time
|
// and send the sid a first time
|
||||||
return performRegistrationRequest(params)
|
return performRegistrationRequest(params, LoginType.PASSWORD)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun checkIfEmailHasBeenValidated(delayMillis: Long): RegistrationResult {
|
override suspend fun checkIfEmailHasBeenValidated(delayMillis: Long): RegistrationResult {
|
||||||
val safeParam = pendingSessionData.currentThreePidData?.registrationParams
|
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 {
|
override suspend fun handleValidateThreePid(code: String): RegistrationResult {
|
||||||
|
@ -167,19 +168,19 @@ internal class DefaultRegistrationWizard(
|
||||||
|
|
||||||
private suspend fun validateThreePid(code: String): RegistrationResult {
|
private suspend fun validateThreePid(code: String): RegistrationResult {
|
||||||
val registrationParams = pendingSessionData.currentThreePidData?.registrationParams
|
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 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 url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url to send the code")
|
||||||
val validationBody = ValidationCodeBody(
|
val validationBody = ValidationCodeBody(
|
||||||
clientSecret = pendingSessionData.clientSecret,
|
clientSecret = pendingSessionData.clientSecret,
|
||||||
sid = safeCurrentData.addThreePidRegistrationResponse.sid,
|
sid = safeCurrentData.addThreePidRegistrationResponse.sid,
|
||||||
code = code
|
code = code
|
||||||
)
|
)
|
||||||
val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody))
|
val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody))
|
||||||
if (validationResponse.isSuccess()) {
|
if (validationResponse.isSuccess()) {
|
||||||
// The entered code is correct
|
// The entered code is correct
|
||||||
// Same than validate email
|
// Same than validate email
|
||||||
return performRegistrationRequest(registrationParams, 3_000)
|
return performRegistrationRequest(registrationParams, LoginType.PASSWORD, 3_000)
|
||||||
} else {
|
} else {
|
||||||
// The code is not correct
|
// The code is not correct
|
||||||
throw Failure.SuccessError
|
throw Failure.SuccessError
|
||||||
|
@ -188,10 +189,10 @@ internal class DefaultRegistrationWizard(
|
||||||
|
|
||||||
override suspend fun dummy(): RegistrationResult {
|
override suspend fun dummy(): RegistrationResult {
|
||||||
val safeSession = pendingSessionData.currentSession
|
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))
|
val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession))
|
||||||
return performRegistrationRequest(params)
|
return performRegistrationRequest(params, LoginType.PASSWORD)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun registrationCustom(
|
override suspend fun registrationCustom(
|
||||||
|
@ -204,25 +205,28 @@ internal class DefaultRegistrationWizard(
|
||||||
mutableParams["session"] = safeSession
|
mutableParams["session"] = safeSession
|
||||||
|
|
||||||
val params = RegistrationCustomParams(auth = mutableParams)
|
val params = RegistrationCustomParams(auth = mutableParams)
|
||||||
return performRegistrationOtherRequest(params)
|
return performRegistrationOtherRequest(LoginType.CUSTOM, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun performRegistrationRequest(
|
private suspend fun performRegistrationRequest(
|
||||||
registrationParams: RegistrationParams,
|
registrationParams: RegistrationParams,
|
||||||
|
loginType: LoginType,
|
||||||
delayMillis: Long = 0
|
delayMillis: Long = 0
|
||||||
): RegistrationResult {
|
): RegistrationResult {
|
||||||
delay(delayMillis)
|
delay(delayMillis)
|
||||||
return register { registerTask.execute(RegisterTask.Params(registrationParams)) }
|
return register(loginType) { registerTask.execute(RegisterTask.Params(registrationParams)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun performRegistrationOtherRequest(
|
private suspend fun performRegistrationOtherRequest(
|
||||||
registrationCustomParams: RegistrationCustomParams
|
loginType: LoginType,
|
||||||
|
registrationCustomParams: RegistrationCustomParams,
|
||||||
): RegistrationResult {
|
): RegistrationResult {
|
||||||
return register { registerCustomTask.execute(RegisterCustomTask.Params(registrationCustomParams)) }
|
return register(loginType) { registerCustomTask.execute(RegisterCustomTask.Params(registrationCustomParams)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun register(
|
private suspend fun register(
|
||||||
execute: suspend () -> Credentials
|
loginType: LoginType,
|
||||||
|
execute: suspend () -> Credentials,
|
||||||
): RegistrationResult {
|
): RegistrationResult {
|
||||||
val credentials = try {
|
val credentials = try {
|
||||||
execute.invoke()
|
execute.invoke()
|
||||||
|
@ -237,8 +241,7 @@ internal class DefaultRegistrationWizard(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val session =
|
val session = sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, loginType)
|
||||||
sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
|
|
||||||
return RegistrationResult.Success(session)
|
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.RoomHistoryVisibility
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
|
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.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.api.session.sync.model.SyncResponse
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
|
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
|
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.crosssigning.DefaultCrossSigningService
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||||
import org.matrix.android.sdk.internal.crypto.model.MXKey.Companion.KEY_SIGNED_CURVE_25519_TYPE
|
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.model.toRest
|
||||||
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
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) {
|
private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) {
|
||||||
if (!event.isStateEvent()) return
|
if (!event.isStateEvent()) return
|
||||||
val eventContent = event.content.toModel<RoomHistoryVisibilityContent>()
|
val eventContent = event.content.toModel<RoomHistoryVisibilityContent>()
|
||||||
eventContent?.historyVisibility?.let {
|
val historyVisibility = eventContent?.historyVisibility
|
||||||
cryptoStore.setShouldEncryptForInvitedMembers(roomId, it != RoomHistoryVisibility.JOINED)
|
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 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.
|
* Tells whether the client should ever send encrypted messages to unverified devices.
|
||||||
* The default value is false.
|
* 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
|
* 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.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
import org.matrix.android.sdk.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.IMXCryptoStore
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.Timer
|
import java.util.Timer
|
||||||
|
@ -31,7 +31,7 @@ import java.util.TimerTask
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal data class InboundGroupSessionHolder(
|
internal data class InboundGroupSessionHolder(
|
||||||
val wrapper: OlmInboundGroupSessionWrapper2,
|
val wrapper: MXInboundMegolmSessionWrapper,
|
||||||
val mutex: Mutex = Mutex()
|
val mutex: Mutex = Mutex()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ internal class InboundGroupSessionStore @Inject constructor(
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}")
|
Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}")
|
||||||
store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper })
|
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 val timer = Timer()
|
||||||
private var timerTask: TimerTask? = null
|
private var timerTask: TimerTask? = null
|
||||||
|
|
||||||
private val dirtySession = mutableListOf<OlmInboundGroupSessionWrapper2>()
|
private val dirtySession = mutableListOf<InboundGroupSessionHolder>()
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun clear() {
|
fun clear() {
|
||||||
|
@ -90,12 +90,12 @@ internal class InboundGroupSessionStore @Inject constructor(
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
|
fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
|
||||||
Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}")
|
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)
|
store.removeInboundGroupSession(sessionId, senderKey)
|
||||||
sessionCache.remove(CacheKey(sessionId, senderKey))
|
sessionCache.remove(CacheKey(sessionId, senderKey))
|
||||||
|
|
||||||
// release removed session
|
// release removed session
|
||||||
old.wrapper.olmInboundGroupSession?.releaseSession()
|
old.wrapper.session.releaseSession()
|
||||||
|
|
||||||
internalStoreGroupSession(new, sessionId, senderKey)
|
internalStoreGroupSession(new, sessionId, senderKey)
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,7 @@ internal class InboundGroupSessionStore @Inject constructor(
|
||||||
private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
|
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}")
|
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
|
// We want to batch this a bit for performances
|
||||||
dirtySession.add(holder.wrapper)
|
dirtySession.add(holder)
|
||||||
|
|
||||||
if (sessionCache[CacheKey(sessionId, senderKey)] == null) {
|
if (sessionCache[CacheKey(sessionId, senderKey)] == null) {
|
||||||
// first time seen, put it in memory cache while waiting for batch insert
|
// first time seen, put it in memory cache while waiting for batch insert
|
||||||
|
@ -127,12 +127,12 @@ internal class InboundGroupSessionStore @Inject constructor(
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun batchSave() {
|
private fun batchSave() {
|
||||||
val toSave = mutableListOf<OlmInboundGroupSessionWrapper2>().apply { addAll(dirtySession) }
|
val toSave = mutableListOf<InboundGroupSessionHolder>().apply { addAll(dirtySession) }
|
||||||
dirtySession.clear()
|
dirtySession.clear()
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession batching save of ${toSave.size}")
|
Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession batching save of ${toSave.size}")
|
||||||
tryOrNull {
|
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.api.util.JsonDict
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXOutboundSessionInfo
|
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.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.model.OlmSessionWrapper
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
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.android.sdk.internal.util.time.Clock
|
||||||
import org.matrix.olm.OlmAccount
|
import org.matrix.olm.OlmAccount
|
||||||
import org.matrix.olm.OlmException
|
import org.matrix.olm.OlmException
|
||||||
|
import org.matrix.olm.OlmInboundGroupSession
|
||||||
import org.matrix.olm.OlmMessage
|
import org.matrix.olm.OlmMessage
|
||||||
import org.matrix.olm.OlmOutboundGroupSession
|
import org.matrix.olm.OlmOutboundGroupSession
|
||||||
import org.matrix.olm.OlmSession
|
import org.matrix.olm.OlmSession
|
||||||
|
@ -514,8 +516,9 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
return MXOutboundSessionInfo(
|
return MXOutboundSessionInfo(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
sharedWithHelper = SharedWithHelper(roomId, sessionId, store),
|
sharedWithHelper = SharedWithHelper(roomId, sessionId, store),
|
||||||
clock,
|
clock = clock,
|
||||||
restoredOutboundGroupSession.creationTime
|
creationTime = restoredOutboundGroupSession.creationTime,
|
||||||
|
sharedHistory = restoredOutboundGroupSession.sharedHistory
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
@ -598,40 +601,47 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
* @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us.
|
* @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us.
|
||||||
* @param keysClaimed Other keys the sender claims.
|
* @param keysClaimed Other keys the sender claims.
|
||||||
* @param exportFormat true if the megolm keys are in export format
|
* @param exportFormat true if the megolm keys are in export format
|
||||||
|
* @param sharedHistory MSC3061, this key is sharable on invite
|
||||||
* @return true if the operation succeeds.
|
* @return true if the operation succeeds.
|
||||||
*/
|
*/
|
||||||
fun addInboundGroupSession(
|
fun addInboundGroupSession(sessionId: String,
|
||||||
sessionId: String,
|
sessionKey: String,
|
||||||
sessionKey: String,
|
roomId: String,
|
||||||
roomId: String,
|
senderKey: String,
|
||||||
senderKey: String,
|
forwardingCurve25519KeyChain: List<String>,
|
||||||
forwardingCurve25519KeyChain: List<String>,
|
keysClaimed: Map<String, String>,
|
||||||
keysClaimed: Map<String, String>,
|
exportFormat: Boolean,
|
||||||
exportFormat: Boolean
|
sharedHistory: Boolean): AddSessionResult {
|
||||||
): AddSessionResult {
|
val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") {
|
||||||
val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
|
if (exportFormat) {
|
||||||
|
OlmInboundGroupSession.importSession(sessionKey)
|
||||||
|
} else {
|
||||||
|
OlmInboundGroupSession(sessionKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
|
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
|
||||||
val existingSession = existingSessionHolder?.wrapper
|
val existingSession = existingSessionHolder?.wrapper
|
||||||
// If we have an existing one we should check if the new one is not better
|
// If we have an existing one we should check if the new one is not better
|
||||||
if (existingSession != null) {
|
if (existingSession != null) {
|
||||||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session")
|
Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session")
|
||||||
try {
|
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?
|
// This is quite unexpected, could throw if native was released?
|
||||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
|
Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
|
||||||
candidateSession.olmInboundGroupSession?.releaseSession()
|
candidateSession?.releaseSession()
|
||||||
// Probably should discard it?
|
// 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 our existing session is better we keep it
|
||||||
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
|
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
|
||||||
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
|
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
|
||||||
candidateSession.olmInboundGroupSession?.releaseSession()
|
candidateSession?.releaseSession()
|
||||||
return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt())
|
return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt())
|
||||||
}
|
}
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
|
Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
|
||||||
candidateSession.olmInboundGroupSession?.releaseSession()
|
candidateSession?.releaseSession()
|
||||||
return AddSessionResult.NotImported
|
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")
|
Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId")
|
||||||
|
|
||||||
// sanity check on the new session
|
// sanity check on the new session
|
||||||
val candidateOlmInboundSession = candidateSession.olmInboundGroupSession
|
if (null == candidateSession) {
|
||||||
if (null == candidateOlmInboundSession) {
|
|
||||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>")
|
Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>")
|
||||||
return AddSessionResult.NotImported
|
return AddSessionResult.NotImported
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (candidateOlmInboundSession.sessionIdentifier() != sessionId) {
|
if (candidateSession.sessionIdentifier() != sessionId) {
|
||||||
Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
||||||
candidateOlmInboundSession.releaseSession()
|
candidateSession.releaseSession()
|
||||||
return AddSessionResult.NotImported
|
return AddSessionResult.NotImported
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
candidateOlmInboundSession.releaseSession()
|
candidateSession.releaseSession()
|
||||||
Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed")
|
Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed")
|
||||||
return AddSessionResult.NotImported
|
return AddSessionResult.NotImported
|
||||||
}
|
}
|
||||||
|
|
||||||
candidateSession.senderKey = senderKey
|
val candidateSessionData = InboundGroupSessionData(
|
||||||
candidateSession.roomId = roomId
|
senderKey = senderKey,
|
||||||
candidateSession.keysClaimed = keysClaimed
|
roomId = roomId,
|
||||||
candidateSession.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain
|
keysClaimed = keysClaimed,
|
||||||
|
forwardingCurve25519KeyChain = forwardingCurve25519KeyChain,
|
||||||
|
sharedHistory = sharedHistory,
|
||||||
|
)
|
||||||
|
|
||||||
|
val wrapper = MXInboundMegolmSessionWrapper(
|
||||||
|
candidateSession,
|
||||||
|
candidateSessionData
|
||||||
|
)
|
||||||
if (existingSession != null) {
|
if (existingSession != null) {
|
||||||
inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
|
inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(wrapper), sessionId, senderKey)
|
||||||
} else {
|
} 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
|
* @param megolmSessionsData the megolm sessions data
|
||||||
* @return the successfully imported sessions.
|
* @return the successfully imported sessions.
|
||||||
*/
|
*/
|
||||||
fun importInboundGroupSessions(megolmSessionsData: List<MegolmSessionData>): List<OlmInboundGroupSessionWrapper2> {
|
fun importInboundGroupSessions(megolmSessionsData: List<MegolmSessionData>): List<MXInboundMegolmSessionWrapper> {
|
||||||
val sessions = ArrayList<OlmInboundGroupSessionWrapper2>(megolmSessionsData.size)
|
val sessions = ArrayList<MXInboundMegolmSessionWrapper>(megolmSessionsData.size)
|
||||||
|
|
||||||
for (megolmSessionData in megolmSessionsData) {
|
for (megolmSessionData in megolmSessionsData) {
|
||||||
val sessionId = megolmSessionData.sessionId ?: continue
|
val sessionId = megolmSessionData.sessionId ?: continue
|
||||||
val senderKey = megolmSessionData.senderKey ?: continue
|
val senderKey = megolmSessionData.senderKey ?: continue
|
||||||
val roomId = megolmSessionData.roomId
|
val roomId = megolmSessionData.roomId
|
||||||
|
|
||||||
var candidateSessionToImport: OlmInboundGroupSessionWrapper2? = null
|
val candidateSessionToImport = try {
|
||||||
|
MXInboundMegolmSessionWrapper.newFromMegolmData(megolmSessionData, true)
|
||||||
try {
|
} catch (e: Throwable) {
|
||||||
candidateSessionToImport = OlmInboundGroupSessionWrapper2(megolmSessionData)
|
Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Failed to import session $senderKey/$sessionId")
|
||||||
} 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()
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val candidateOlmInboundGroupSession = candidateSessionToImport.session
|
||||||
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
|
val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
|
||||||
val existingSession = existingSessionHolder?.wrapper
|
val existingSession = existingSessionHolder?.wrapper
|
||||||
|
|
||||||
|
@ -721,16 +718,16 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
sessions.add(candidateSessionToImport)
|
sessions.add(candidateSessionToImport)
|
||||||
} else {
|
} else {
|
||||||
Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
||||||
val existingFirstKnown = tryOrNull { existingSession.firstKnownIndex }
|
val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex }
|
||||||
val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.firstKnownIndex }
|
val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.session.firstKnownIndex }
|
||||||
|
|
||||||
if (existingFirstKnown == null || candidateFirstKnownIndex == null) {
|
if (existingFirstKnown == null || candidateFirstKnownIndex == null) {
|
||||||
// should not happen?
|
// should not happen?
|
||||||
candidateSessionToImport.olmInboundGroupSession?.releaseSession()
|
candidateSessionToImport.session.releaseSession()
|
||||||
Timber.tag(loggerTag.value)
|
Timber.tag(loggerTag.value)
|
||||||
.w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex")
|
.w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex")
|
||||||
} else {
|
} else {
|
||||||
if (existingFirstKnown <= candidateSessionToImport.firstKnownIndex!!) {
|
if (existingFirstKnown <= candidateFirstKnownIndex) {
|
||||||
// Ignore this, keep existing
|
// Ignore this, keep existing
|
||||||
candidateOlmInboundGroupSession.releaseSession()
|
candidateOlmInboundGroupSession.releaseSession()
|
||||||
} else {
|
} else {
|
||||||
|
@ -774,8 +771,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
): OlmDecryptionResult {
|
): OlmDecryptionResult {
|
||||||
val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId)
|
val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId)
|
||||||
val wrapper = sessionHolder.wrapper
|
val wrapper = sessionHolder.wrapper
|
||||||
val inboundGroupSession = wrapper.olmInboundGroupSession
|
val inboundGroupSession = wrapper.session
|
||||||
?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null")
|
|
||||||
if (roomId != wrapper.roomId) {
|
if (roomId != wrapper.roomId) {
|
||||||
// Check that the room id matches the original one for the session. This stops
|
// Check that the room id matches the original one for the session. This stops
|
||||||
// the HS pretending a message was targeting a different room.
|
// the HS pretending a message was targeting a different room.
|
||||||
|
@ -822,9 +818,9 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
|
|
||||||
return OlmDecryptionResult(
|
return OlmDecryptionResult(
|
||||||
payload,
|
payload,
|
||||||
wrapper.keysClaimed,
|
wrapper.sessionData.keysClaimed,
|
||||||
senderKey,
|
senderKey,
|
||||||
wrapper.forwardingCurve25519KeyChain
|
wrapper.sessionData.forwardingCurve25519KeyChain
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,5 +69,13 @@ internal data class MegolmSessionData(
|
||||||
* Devices which forwarded this session to us (normally empty).
|
* Devices which forwarded this session to us (normally empty).
|
||||||
*/
|
*/
|
||||||
@Json(name = "forwarding_curve25519_key_chain")
|
@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)) {
|
if (perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)) {
|
||||||
// let's see what's the index
|
// let's see what's the index
|
||||||
val knownIndex = tryOrNull {
|
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) {
|
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
|
// 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 ?: "",
|
megolmSessionData.senderKey ?: "",
|
||||||
tryOrNull {
|
tryOrNull {
|
||||||
olmInboundGroupSessionWrappers
|
olmInboundGroupSessionWrappers
|
||||||
.firstOrNull { it.olmInboundGroupSession?.sessionIdentifier() == megolmSessionData.sessionId }
|
.firstOrNull { it.session.sessionIdentifier() == megolmSessionData.sessionId }
|
||||||
?.firstKnownIndex?.toInt()
|
?.session?.firstKnownIndex
|
||||||
|
?.toInt()
|
||||||
} ?: 0
|
} ?: 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms
|
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.api.session.events.model.Content
|
||||||
|
import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An interface for encrypting data.
|
* An interface for encrypting data.
|
||||||
|
@ -32,4 +34,6 @@ internal interface IMXEncrypting {
|
||||||
* @return the encrypted content
|
* @return the encrypted content
|
||||||
*/
|
*/
|
||||||
suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content
|
suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content
|
||||||
|
|
||||||
|
suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||||
|
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
|
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
import org.matrix.android.sdk.api.session.crypto.NewSessionListener
|
||||||
|
@ -41,6 +42,7 @@ internal class MXMegolmDecryption(
|
||||||
private val olmDevice: MXOlmDevice,
|
private val olmDevice: MXOlmDevice,
|
||||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
|
private val matrixConfiguration: MatrixConfiguration,
|
||||||
private val liveEventManager: Lazy<StreamEventsManager>
|
private val liveEventManager: Lazy<StreamEventsManager>
|
||||||
) : IMXDecrypting {
|
) : IMXDecrypting {
|
||||||
|
|
||||||
|
@ -240,13 +242,14 @@ internal class MXMegolmDecryption(
|
||||||
|
|
||||||
Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
|
Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
|
||||||
val addSessionResult = olmDevice.addInboundGroupSession(
|
val addSessionResult = olmDevice.addInboundGroupSession(
|
||||||
roomKeyContent.sessionId,
|
sessionId = roomKeyContent.sessionId,
|
||||||
roomKeyContent.sessionKey,
|
sessionKey = roomKeyContent.sessionKey,
|
||||||
roomKeyContent.roomId,
|
roomId = roomKeyContent.roomId,
|
||||||
senderKey,
|
senderKey = senderKey,
|
||||||
forwardingCurve25519KeyChain,
|
forwardingCurve25519KeyChain = forwardingCurve25519KeyChain,
|
||||||
keysClaimed,
|
keysClaimed = keysClaimed,
|
||||||
exportFormat
|
exportFormat = exportFormat,
|
||||||
|
sharedHistory = roomKeyContent.getSharedKey()
|
||||||
)
|
)
|
||||||
|
|
||||||
when (addSessionResult) {
|
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.
|
* 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
|
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
|
||||||
|
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
|
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
||||||
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
|
@ -27,6 +28,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
|
||||||
private val olmDevice: MXOlmDevice,
|
private val olmDevice: MXOlmDevice,
|
||||||
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
|
private val matrixConfiguration: MatrixConfiguration,
|
||||||
private val eventsManager: Lazy<StreamEventsManager>
|
private val eventsManager: Lazy<StreamEventsManager>
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -35,7 +37,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
|
||||||
olmDevice,
|
olmDevice,
|
||||||
outgoingKeyRequestManager,
|
outgoingKeyRequestManager,
|
||||||
cryptoStore,
|
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.RoomKeyWithHeldContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
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.DeviceListManager
|
||||||
|
import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder
|
||||||
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
|
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.EnsureOlmSessionsForDevicesAction
|
||||||
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
|
||||||
|
@ -151,14 +152,27 @@ internal class MXMegolmEncryption(
|
||||||
"ed25519" to olmDevice.deviceEd25519Key!!
|
"ed25519" to olmDevice.deviceEd25519Key!!
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val sharedHistory = cryptoStore.shouldShareHistory(roomId)
|
||||||
|
Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() as sharedHistory $sharedHistory")
|
||||||
olmDevice.addInboundGroupSession(
|
olmDevice.addInboundGroupSession(
|
||||||
sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!,
|
sessionId = sessionId!!,
|
||||||
emptyList(), keysClaimedMap, false
|
sessionKey = olmDevice.getSessionKey(sessionId)!!,
|
||||||
|
roomId = roomId,
|
||||||
|
senderKey = olmDevice.deviceCurve25519Key!!,
|
||||||
|
forwardingCurve25519KeyChain = emptyList(),
|
||||||
|
keysClaimed = keysClaimedMap,
|
||||||
|
exportFormat = false,
|
||||||
|
sharedHistory = sharedHistory
|
||||||
)
|
)
|
||||||
|
|
||||||
defaultKeysBackupService.maybeBackupKeys()
|
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 ||
|
if (session == null ||
|
||||||
// Need to make a brand new session?
|
// Need to make a brand new session?
|
||||||
session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) ||
|
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
|
// Determine if we have shared with anyone we shouldn't have
|
||||||
session.sharedWithTooManyDevices(devicesInRoom)) {
|
session.sharedWithTooManyDevices(devicesInRoom)) {
|
||||||
Timber.tag(loggerTag.value).d("roomId:$roomId Starting new megolm session because we need to rotate.")
|
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.
|
* Share the device keys of a an user.
|
||||||
*
|
*
|
||||||
* @param session the session info
|
* @param sessionInfo the session info
|
||||||
* @param devicesByUser the devices map
|
* @param devicesByUser the devices map
|
||||||
*/
|
*/
|
||||||
private suspend fun shareUserDevicesKey(
|
private suspend fun shareUserDevicesKey(sessionInfo: MXOutboundSessionInfo,
|
||||||
session: MXOutboundSessionInfo,
|
devicesByUser: Map<String, List<CryptoDeviceInfo>>) {
|
||||||
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 sessionKey = olmDevice.getSessionKey(session.sessionId)
|
}
|
||||||
val chainIndex = olmDevice.getMessageIndex(session.sessionId)
|
val chainIndex = olmDevice.getMessageIndex(sessionInfo.sessionId)
|
||||||
|
|
||||||
val submap = HashMap<String, Any>()
|
val payload = mapOf(
|
||||||
submap["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM
|
"type" to EventType.ROOM_KEY,
|
||||||
submap["room_id"] = roomId
|
"content" to mapOf(
|
||||||
submap["session_id"] = session.sessionId
|
"algorithm" to MXCRYPTO_ALGORITHM_MEGOLM,
|
||||||
submap["session_key"] = sessionKey!!
|
"room_id" to roomId,
|
||||||
submap["chain_index"] = chainIndex
|
"session_id" to sessionInfo.sessionId,
|
||||||
|
"session_key" to sessionKey,
|
||||||
val payload = HashMap<String, Any>()
|
"chain_index" to chainIndex,
|
||||||
payload["type"] = EventType.ROOM_KEY
|
"org.matrix.msc3061.shared_history" to sessionInfo.sharedHistory
|
||||||
payload["content"] = submap
|
)
|
||||||
|
)
|
||||||
|
|
||||||
var t0 = clock.epochMillis()
|
var t0 = clock.epochMillis()
|
||||||
Timber.tag(loggerTag.value).v("shareUserDevicesKey() : starts")
|
Timber.tag(loggerTag.value).v("shareUserDevicesKey() : starts")
|
||||||
|
@ -292,7 +309,7 @@ internal class MXMegolmEncryption(
|
||||||
// for dead devices on every message.
|
// for dead devices on every message.
|
||||||
for ((_, devicesToShareWith) in devicesByUser) {
|
for ((_, devicesToShareWith) in devicesByUser) {
|
||||||
for (deviceInfo in devicesToShareWith) {
|
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?
|
// XXX is it needed to add it to the audit trail?
|
||||||
// For now decided that no, we are more interested by forward trail
|
// For now decided that no, we are more interested by forward trail
|
||||||
}
|
}
|
||||||
|
@ -300,8 +317,8 @@ internal class MXMegolmEncryption(
|
||||||
|
|
||||||
if (haveTargets) {
|
if (haveTargets) {
|
||||||
t0 = clock.epochMillis()
|
t0 = clock.epochMillis()
|
||||||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target")
|
Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${sessionInfo.sessionId} : has target")
|
||||||
Timber.tag(loggerTag.value).d("sending to device room key for ${session.sessionId} to ${contentMap.toDebugString()}")
|
Timber.tag(loggerTag.value).d("sending to device room key for ${sessionInfo.sessionId} to ${contentMap.toDebugString()}")
|
||||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
|
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
|
||||||
try {
|
try {
|
||||||
withContext(coroutineDispatchers.io) {
|
withContext(coroutineDispatchers.io) {
|
||||||
|
@ -310,7 +327,7 @@ internal class MXMegolmEncryption(
|
||||||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${clock.epochMillis() - t0} ms")
|
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${clock.epochMillis() - t0} ms")
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
// What to do here...
|
// 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 {
|
} else {
|
||||||
Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key")
|
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?
|
// XXX offload?, as they won't read the message anyhow?
|
||||||
notifyKeyWithHeld(
|
notifyKeyWithHeld(
|
||||||
noOlmToNotify,
|
noOlmToNotify,
|
||||||
session.sessionId,
|
sessionInfo.sessionId,
|
||||||
olmDevice.deviceCurve25519Key,
|
olmDevice.deviceCurve25519Key,
|
||||||
WithHeldCode.NO_OLM
|
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(
|
data class DeviceInRoomInfo(
|
||||||
val allowedDevices: MXUsersDevicesMap<CryptoDeviceInfo> = MXUsersDevicesMap(),
|
val allowedDevices: MXUsersDevicesMap<CryptoDeviceInfo> = MXUsersDevicesMap(),
|
||||||
val withHeldDevices: MXUsersDevicesMap<WithHeldCode> = MXUsersDevicesMap()
|
val withHeldDevices: MXUsersDevicesMap<WithHeldCode> = MXUsersDevicesMap()
|
||||||
|
|
|
@ -28,6 +28,7 @@ internal class MXOutboundSessionInfo(
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
// When the session was created
|
// When the session was created
|
||||||
private val creationTime: Long = clock.epochMillis(),
|
private val creationTime: Long = clock.epochMillis(),
|
||||||
|
val sharedHistory: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// Number of times this session has been used
|
// Number of times this session has been used
|
||||||
|
|
|
@ -24,8 +24,10 @@ import androidx.annotation.WorkerThread
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
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.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
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.session.crypto.model.ImportRoomKeysResult
|
||||||
import org.matrix.android.sdk.api.util.awaitCallback
|
import org.matrix.android.sdk.api.util.awaitCallback
|
||||||
import org.matrix.android.sdk.api.util.fromBase64
|
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.MXOlmDevice
|
||||||
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
|
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
|
||||||
import org.matrix.android.sdk.internal.crypto.ObjectSigner
|
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.GetSessionsDataTask
|
||||||
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask
|
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.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.IMXCryptoStore
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
|
@ -118,6 +121,8 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
|
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
|
||||||
// Task executor
|
// Task executor
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
|
private val matrixConfiguration: MatrixConfiguration,
|
||||||
|
private val inboundGroupSessionStore: InboundGroupSessionStore,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val cryptoCoroutineScope: CoroutineScope
|
private val cryptoCoroutineScope: CoroutineScope
|
||||||
) : KeysBackupService {
|
) : KeysBackupService {
|
||||||
|
@ -1316,7 +1321,7 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
|
|
||||||
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
|
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
|
||||||
val roomId = olmInboundGroupSessionWrapper.roomId ?: return@forEach
|
val roomId = olmInboundGroupSessionWrapper.roomId ?: return@forEach
|
||||||
val olmInboundGroupSession = olmInboundGroupSessionWrapper.olmInboundGroupSession ?: return@forEach
|
val olmInboundGroupSession = olmInboundGroupSessionWrapper.session
|
||||||
|
|
||||||
try {
|
try {
|
||||||
encryptGroupSession(olmInboundGroupSessionWrapper)
|
encryptGroupSession(olmInboundGroupSessionWrapper)
|
||||||
|
@ -1405,19 +1410,29 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@WorkerThread
|
@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
|
// 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
|
// 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
|
// 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(
|
val sessionBackupData = mapOf(
|
||||||
"algorithm" to sessionData.algorithm,
|
"algorithm" to sessionData.algorithm,
|
||||||
"sender_key" to sessionData.senderKey,
|
"sender_key" to sessionData.senderKey,
|
||||||
"sender_claimed_keys" to sessionData.senderClaimedKeys,
|
"sender_claimed_keys" to sessionData.senderClaimedKeys,
|
||||||
"forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain.orEmpty()),
|
"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()
|
val json = MoshiProvider.providesMoshi()
|
||||||
|
@ -1425,7 +1440,9 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
.toJson(sessionBackupData)
|
.toJson(sessionBackupData)
|
||||||
|
|
||||||
val encryptedSessionBackupData = try {
|
val encryptedSessionBackupData = try {
|
||||||
backupOlmPkEncryption?.encrypt(json)
|
withContext(coroutineDispatchers.computation) {
|
||||||
|
backupOlmPkEncryption?.encrypt(json)
|
||||||
|
}
|
||||||
} catch (e: OlmException) {
|
} catch (e: OlmException) {
|
||||||
Timber.e(e, "OlmException")
|
Timber.e(e, "OlmException")
|
||||||
null
|
null
|
||||||
|
@ -1435,14 +1452,14 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
// Build backup data for that key
|
// Build backup data for that key
|
||||||
return KeyBackupData(
|
return KeyBackupData(
|
||||||
firstMessageIndex = try {
|
firstMessageIndex = try {
|
||||||
olmInboundGroupSessionWrapper.olmInboundGroupSession?.firstKnownIndex ?: 0
|
olmInboundGroupSessionWrapper.session.firstKnownIndex
|
||||||
} catch (e: OlmException) {
|
} catch (e: OlmException) {
|
||||||
Timber.e(e, "OlmException")
|
Timber.e(e, "OlmException")
|
||||||
0L
|
0L
|
||||||
},
|
},
|
||||||
forwardedCount = olmInboundGroupSessionWrapper.forwardingCurve25519KeyChain.orEmpty().size,
|
forwardedCount = olmInboundGroupSessionWrapper.sessionData.forwardingCurve25519KeyChain.orEmpty().size,
|
||||||
isVerified = device?.isVerified == true,
|
isVerified = device?.isVerified == true,
|
||||||
|
sharedHistory = olmInboundGroupSessionWrapper.getSharedKey(),
|
||||||
sessionData = mapOf(
|
sessionData = mapOf(
|
||||||
"ciphertext" to encryptedSessionBackupData.mCipherText,
|
"ciphertext" to encryptedSessionBackupData.mCipherText,
|
||||||
"mac" to encryptedSessionBackupData.mMac,
|
"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
|
@VisibleForTesting
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun decryptKeyBackupData(keyBackupData: KeyBackupData, sessionId: String, roomId: String, decryption: OlmPkDecryption): MegolmSessionData? {
|
fun decryptKeyBackupData(keyBackupData: KeyBackupData, sessionId: String, roomId: String, decryption: OlmPkDecryption): MegolmSessionData? {
|
||||||
|
|
|
@ -50,5 +50,12 @@ internal data class KeyBackupData(
|
||||||
* Algorithm-dependent data.
|
* Algorithm-dependent data.
|
||||||
*/
|
*/
|
||||||
@Json(name = "session_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 class adds more context to a OlmInboundGroupSession object.
|
||||||
* This allows additional checks. The class implements Serializable so that the context can be stored.
|
* 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 {
|
internal class OlmInboundGroupSessionWrapper2 : Serializable {
|
||||||
|
|
||||||
// The associated olm inbound group session.
|
// The associated olm inbound group session.
|
||||||
|
|
|
@ -20,5 +20,9 @@ import org.matrix.olm.OlmOutboundGroupSession
|
||||||
|
|
||||||
internal data class OutboundGroupSessionWrapper(
|
internal data class OutboundGroupSessionWrapper(
|
||||||
val outboundGroupSession: OlmOutboundGroupSession,
|
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.RoomKeyWithHeldContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
|
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.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.OlmSessionWrapper
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
|
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
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.
|
* @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.
|
* @return true to unilaterally blacklist all unverified devices.
|
||||||
|
@ -90,6 +98,20 @@ internal interface IMXCryptoStore {
|
||||||
|
|
||||||
fun isKeyGossipingEnabled(): Boolean
|
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.
|
* 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 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.
|
* 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.
|
* @param sessions the inbound group sessions to store.
|
||||||
*/
|
*/
|
||||||
fun storeInboundGroupSessions(sessions: List<OlmInboundGroupSessionWrapper2>)
|
fun storeInboundGroupSessions(sessions: List<MXInboundMegolmSessionWrapper>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve an inbound group session.
|
* Retrieve an inbound group session.
|
||||||
|
@ -299,7 +332,17 @@ internal interface IMXCryptoStore {
|
||||||
* @param senderKey the base64-encoded curve25519 key of the sender.
|
* @param senderKey the base64-encoded curve25519 key of the sender.
|
||||||
* @return an inbound group session.
|
* @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.
|
* Get the current outbound group session for this encrypted room.
|
||||||
|
@ -333,7 +376,7 @@ internal interface IMXCryptoStore {
|
||||||
*
|
*
|
||||||
* @param olmInboundGroupSessionWrappers the sessions
|
* @param olmInboundGroupSessionWrappers the sessions
|
||||||
*/
|
*/
|
||||||
fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<OlmInboundGroupSessionWrapper2>)
|
fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<MXInboundMegolmSessionWrapper>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve inbound group sessions that are not yet backed up.
|
* 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.
|
* @param limit the maximum number of sessions to return.
|
||||||
* @return an array of non backed up inbound group sessions.
|
* @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.
|
* 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.session.events.model.content.WithHeldCode
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.api.util.toOptional
|
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.OlmSessionWrapper
|
||||||
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
|
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
|
||||||
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
|
@ -657,12 +657,28 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
?: false
|
?: 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) {
|
override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) {
|
||||||
doRealmTransaction(realmConfiguration) {
|
doRealmTransaction(realmConfiguration) {
|
||||||
CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers
|
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) {
|
override fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) {
|
||||||
var sessionIdentifier: String? = null
|
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()) {
|
if (sessions.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
doRealmTransaction(realmConfiguration) { realm ->
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
sessions.forEach { session ->
|
sessions.forEach { wrapper ->
|
||||||
var sessionIdentifier: String? = null
|
|
||||||
|
|
||||||
try {
|
val sessionIdentifier = try {
|
||||||
sessionIdentifier = session.olmInboundGroupSession?.sessionIdentifier()
|
wrapper.session.sessionIdentifier()
|
||||||
} catch (e: OlmException) {
|
} catch (e: OlmException) {
|
||||||
Timber.e(e, "## storeInboundGroupSession() : sessionIdentifier failed")
|
Timber.e(e, "## storeInboundGroupSession() : sessionIdentifier failed")
|
||||||
|
return@forEach
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionIdentifier != null) {
|
// val shouldShareHistory = session.roomId?.let { roomId ->
|
||||||
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey)
|
// CryptoRoomEntity.getById(realm, roomId)?.shouldShareHistory
|
||||||
|
// } ?: false
|
||||||
|
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, wrapper.sessionData.senderKey)
|
||||||
|
|
||||||
val existing = realm.where<OlmInboundGroupSessionEntity>()
|
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
|
||||||
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
|
primaryKey = key
|
||||||
.findFirst()
|
store(wrapper)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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)
|
val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
|
||||||
|
|
||||||
return doWithRealm(realmConfiguration) {
|
return doWithRealm(realmConfiguration) { realm ->
|
||||||
it.where<OlmInboundGroupSessionEntity>()
|
realm.where<OlmInboundGroupSessionEntity>()
|
||||||
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
|
.equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
|
||||||
.findFirst()
|
.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 {
|
entity.getOutboundGroupSession()?.let {
|
||||||
OutboundGroupSessionWrapper(
|
OutboundGroupSessionWrapper(
|
||||||
it,
|
it,
|
||||||
entity.creationTime ?: 0
|
entity.creationTime ?: 0,
|
||||||
|
entity.shouldShareHistory
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -806,6 +824,8 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
if (outboundGroupSession != null) {
|
if (outboundGroupSession != null) {
|
||||||
val info = realm.createObject(OutboundGroupSessionInfoEntity::class.java).apply {
|
val info = realm.createObject(OutboundGroupSessionInfoEntity::class.java).apply {
|
||||||
creationTime = clock.epochMillis()
|
creationTime = clock.epochMillis()
|
||||||
|
// Store the room history visibility on the outbound session creation
|
||||||
|
shouldShareHistory = entity.shouldShareHistory
|
||||||
putOutboundGroupSession(outboundGroupSession)
|
putOutboundGroupSession(outboundGroupSession)
|
||||||
}
|
}
|
||||||
entity.outboundSessionInfo = info
|
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,
|
* 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.
|
* so there is no need to use or update `inboundGroupSessionToRelease` for native memory management.
|
||||||
*/
|
*/
|
||||||
override fun getInboundGroupSessions(): List<OlmInboundGroupSessionWrapper2> {
|
override fun getInboundGroupSessions(): List<MXInboundMegolmSessionWrapper> {
|
||||||
return doWithRealm(realmConfiguration) {
|
return doWithRealm(realmConfiguration) { realm ->
|
||||||
it.where<OlmInboundGroupSessionEntity>()
|
realm.where<OlmInboundGroupSessionEntity>()
|
||||||
.findAll()
|
.findAll()
|
||||||
.mapNotNull { inboundGroupSessionEntity ->
|
.mapNotNull { it.toModel() }
|
||||||
inboundGroupSessionEntity.getInboundGroupSession()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()) {
|
if (olmInboundGroupSessionWrappers.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -893,10 +928,13 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
doRealmTransaction(realmConfiguration) { realm ->
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
|
olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
|
||||||
try {
|
try {
|
||||||
val sessionIdentifier = olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier()
|
val sessionIdentifier =
|
||||||
|
tryOrNull("Failed to get session identifier") {
|
||||||
|
olmInboundGroupSessionWrapper.session.sessionIdentifier()
|
||||||
|
} ?: return@forEach
|
||||||
val key = OlmInboundGroupSessionEntity.createPrimaryKey(
|
val key = OlmInboundGroupSessionEntity.createPrimaryKey(
|
||||||
sessionIdentifier,
|
sessionIdentifier,
|
||||||
olmInboundGroupSessionWrapper.senderKey
|
olmInboundGroupSessionWrapper.sessionData.senderKey
|
||||||
)
|
)
|
||||||
|
|
||||||
val existing = realm.where<OlmInboundGroupSessionEntity>()
|
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
|
// ... might be in cache but not yet persisted, create a record to persist backedup state
|
||||||
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
|
val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
|
||||||
primaryKey = key
|
primaryKey = key
|
||||||
sessionId = sessionIdentifier
|
store(olmInboundGroupSessionWrapper)
|
||||||
senderKey = olmInboundGroupSessionWrapper.senderKey
|
|
||||||
putInboundGroupSession(olmInboundGroupSessionWrapper)
|
|
||||||
backedUp = true
|
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) {
|
return doWithRealm(realmConfiguration) {
|
||||||
it.where<OlmInboundGroupSessionEntity>()
|
it.where<OlmInboundGroupSessionEntity>()
|
||||||
.equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false)
|
.equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false)
|
||||||
.limit(limit.toLong())
|
.limit(limit.toLong())
|
||||||
.findAll()
|
.findAll()
|
||||||
.mapNotNull { inboundGroupSession ->
|
.mapNotNull { it.toModel() }
|
||||||
inboundGroupSession.getInboundGroupSession()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -973,6 +1007,18 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
} ?: false
|
} ?: 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) {
|
override fun setDeviceKeysUploaded(uploaded: Boolean) {
|
||||||
doRealmTransaction(realmConfiguration) {
|
doRealmTransaction(realmConfiguration) {
|
||||||
it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer = uploaded
|
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.MigrateCryptoTo014
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015
|
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016
|
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017
|
||||||
import org.matrix.android.sdk.internal.util.time.Clock
|
import org.matrix.android.sdk.internal.util.time.Clock
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -51,7 +52,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(
|
||||||
// 0, 1, 2: legacy Riot-Android
|
// 0, 1, 2: legacy Riot-Android
|
||||||
// 3: migrate to RiotX schema
|
// 3: migrate to RiotX schema
|
||||||
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
|
// 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) {
|
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||||
Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion")
|
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 < 14) MigrateCryptoTo014(realm).perform()
|
||||||
if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
|
if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
|
||||||
if (oldVersion < 16) MigrateCryptoTo016(realm).perform()
|
if (oldVersion < 16) MigrateCryptoTo016(realm).perform()
|
||||||
|
if (oldVersion < 17) MigrateCryptoTo017(realm).perform()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
var globalBlacklistUnverifiedDevices: Boolean = false,
|
||||||
// setting to enable or disable key gossiping
|
// setting to enable or disable key gossiping
|
||||||
var globalEnableKeyGossiping: Boolean = true,
|
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.
|
// The keys backup version currently used. Null means no backup.
|
||||||
var backupVersion: String? = null,
|
var backupVersion: String? = null,
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,8 @@ internal open class CryptoRoomEntity(
|
||||||
var algorithm: String? = null,
|
var algorithm: String? = null,
|
||||||
var shouldEncryptForInvitedMembers: Boolean? = null,
|
var shouldEncryptForInvitedMembers: Boolean? = null,
|
||||||
var blacklistUnverifiedDevices: Boolean = false,
|
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,
|
// Store the current outbound session for this room,
|
||||||
// to avoid re-create and re-share at each startup (if rotation not needed..)
|
// 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
|
// 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.RealmObject
|
||||||
import io.realm.annotations.PrimaryKey
|
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.deserializeFromRealm
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
|
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
|
import timber.log.Timber
|
||||||
|
|
||||||
internal fun OlmInboundGroupSessionEntity.Companion.createPrimaryKey(sessionId: String?, senderKey: String?) = "$sessionId|$senderKey"
|
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(
|
internal open class OlmInboundGroupSessionEntity(
|
||||||
// Combined value to build a primary key
|
// Combined value to build a primary key
|
||||||
@PrimaryKey var primaryKey: String? = null,
|
@PrimaryKey var primaryKey: String? = null,
|
||||||
|
|
||||||
|
// denormalization for faster querying (these fields are in the inboundGroupSessionDataJson)
|
||||||
var sessionId: String? = null,
|
var sessionId: String? = null,
|
||||||
var senderKey: 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,
|
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
|
// Indicate if the key has been backed up to the homeserver
|
||||||
var backedUp: Boolean = false
|
var backedUp: Boolean = false
|
||||||
) :
|
) :
|
||||||
RealmObject() {
|
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 {
|
return try {
|
||||||
deserializeFromRealm<OlmInboundGroupSessionWrapper2?>(olmInboundGroupSessionData)
|
deserializeFromRealm(serializedOlmInboundGroupSession)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e(failure, "## Deserialization failure")
|
Timber.e(failure, "## Deserialization failure")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2?) {
|
fun getData(): InboundGroupSessionData? {
|
||||||
olmInboundGroupSessionData = serializeForRealm(olmInboundGroupSessionWrapper)
|
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(
|
internal open class OutboundGroupSessionInfoEntity(
|
||||||
var serializedOutboundSessionData: String? = null,
|
var serializedOutboundSessionData: String? = null,
|
||||||
var creationTime: Long? = null
|
var creationTime: Long? = null,
|
||||||
|
var shouldShareHistory: Boolean = false
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
fun getOutboundGroupSession(): OlmOutboundGroupSession? {
|
fun getOutboundGroupSession(): OlmOutboundGroupSession? {
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package org.matrix.android.sdk.internal.crypto.tasks
|
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.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||||
|
@ -48,8 +47,12 @@ internal class DefaultSendEventTask @Inject constructor(
|
||||||
params.event.roomId
|
params.event.roomId
|
||||||
?.takeIf { params.encrypt }
|
?.takeIf { params.encrypt }
|
||||||
?.let { roomId ->
|
?.let { roomId ->
|
||||||
tryOrNull {
|
try {
|
||||||
loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
|
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.Realm
|
||||||
import io.realm.kotlin.createObject
|
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.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.ChunkEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
|
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
|
||||||
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
|
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.TimelineEventEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
|
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.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.getOrCreate
|
||||||
import org.matrix.android.sdk.internal.database.query.where
|
import org.matrix.android.sdk.internal.database.query.where
|
||||||
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
|
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
|
// We don't know, so we assume it's false
|
||||||
return 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.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import okhttp3.ConnectionSpec
|
import okhttp3.ConnectionSpec
|
||||||
|
import okhttp3.Dispatcher
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Protocol
|
import okhttp3.Protocol
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
@ -73,7 +74,9 @@ internal object NetworkModule {
|
||||||
apiInterceptor: ApiInterceptor
|
apiInterceptor: ApiInterceptor
|
||||||
): OkHttpClient {
|
): OkHttpClient {
|
||||||
val spec = ConnectionSpec.Builder(matrixConfiguration.connectionSpec).build()
|
val spec = ConnectionSpec.Builder(matrixConfiguration.connectionSpec).build()
|
||||||
|
val dispatcher = Dispatcher().apply {
|
||||||
|
maxRequestsPerHost = 20
|
||||||
|
}
|
||||||
return OkHttpClient.Builder()
|
return OkHttpClient.Builder()
|
||||||
// workaround for #4669
|
// workaround for #4669
|
||||||
.protocols(listOf(Protocol.HTTP_1_1))
|
.protocols(listOf(Protocol.HTTP_1_1))
|
||||||
|
@ -94,6 +97,7 @@ internal object NetworkModule {
|
||||||
addInterceptor(curlLoggingInterceptor)
|
addInterceptor(curlLoggingInterceptor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.dispatcher(dispatcher)
|
||||||
.connectionSpecs(Collections.singletonList(spec))
|
.connectionSpecs(Collections.singletonList(spec))
|
||||||
.applyMatrixConfiguration(matrixConfiguration)
|
.applyMatrixConfiguration(matrixConfiguration)
|
||||||
.build()
|
.build()
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import kotlinx.coroutines.runBlocking
|
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.Credentials
|
||||||
import org.matrix.android.sdk.api.auth.data.DiscoveryInformation
|
import org.matrix.android.sdk.api.auth.data.DiscoveryInformation
|
||||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||||
|
@ -145,7 +146,8 @@ internal class DefaultLegacySessionImporter @Inject constructor(
|
||||||
forceUsageTlsVersions = legacyConfig.forceUsageOfTlsVersions()
|
forceUsageTlsVersions = legacyConfig.forceUsageOfTlsVersions()
|
||||||
),
|
),
|
||||||
// If token is not valid, this boolean will be updated later
|
// If token is not valid, this boolean will be updated later
|
||||||
isTokenValid = true
|
isTokenValid = true,
|
||||||
|
loginType = LoginType.UNKNOWN,
|
||||||
)
|
)
|
||||||
|
|
||||||
Timber.d("Migration: save session")
|
Timber.d("Migration: save session")
|
||||||
|
|
|
@ -70,7 +70,15 @@ internal class PreferredNetworkCallbackStrategy @Inject constructor(context: Con
|
||||||
|
|
||||||
override fun register(hasChanged: () -> Unit) {
|
override fun register(hasChanged: () -> Unit) {
|
||||||
hasChangedCallback = hasChanged
|
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() {
|
override fun unregister() {
|
||||||
|
|
|
@ -72,7 +72,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
|
||||||
return sendLiveLocationTask.execute(params)
|
return sendLiveLocationTask.execute(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult {
|
override suspend fun startLiveLocationShare(timeoutMillis: Long, description: String): UpdateLiveLocationShareResult {
|
||||||
// Ensure to stop any active live before starting a new one
|
// Ensure to stop any active live before starting a new one
|
||||||
if (checkIfExistingActiveLive()) {
|
if (checkIfExistingActiveLive()) {
|
||||||
val result = stopLiveLocationShare()
|
val result = stopLiveLocationShare()
|
||||||
|
@ -82,7 +82,8 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
val params = StartLiveLocationShareTask.Params(
|
val params = StartLiveLocationShareTask.Params(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
timeoutMillis = timeoutMillis
|
timeoutMillis = timeoutMillis,
|
||||||
|
description = description
|
||||||
)
|
)
|
||||||
return startLiveLocationShareTask.execute(params)
|
return startLiveLocationShareTask.execute(params)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ internal interface StartLiveLocationShareTask : Task<StartLiveLocationShareTask.
|
||||||
data class Params(
|
data class Params(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val timeoutMillis: Long,
|
val timeoutMillis: Long,
|
||||||
|
val description: String,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +42,7 @@ internal class DefaultStartLiveLocationShareTask @Inject constructor(
|
||||||
|
|
||||||
override suspend fun execute(params: StartLiveLocationShareTask.Params): UpdateLiveLocationShareResult {
|
override suspend fun execute(params: StartLiveLocationShareTask.Params): UpdateLiveLocationShareResult {
|
||||||
val beaconContent = MessageBeaconInfoContent(
|
val beaconContent = MessageBeaconInfoContent(
|
||||||
|
body = params.description,
|
||||||
timeout = params.timeoutMillis,
|
timeout = params.timeoutMillis,
|
||||||
isLive = true,
|
isLive = true,
|
||||||
unstableTimestampMillis = clock.epochMillis()
|
unstableTimestampMillis = clock.epochMillis()
|
||||||
|
|
|
@ -17,18 +17,23 @@
|
||||||
package org.matrix.android.sdk.internal.session.room.membership
|
package org.matrix.android.sdk.internal.session.room.membership
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import com.otaliastudios.opengl.core.use
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmQuery
|
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.identity.ThreePid
|
||||||
import org.matrix.android.sdk.api.session.room.members.MembershipService
|
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.members.RoomMemberQueryParams
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
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.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.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.RoomMemberSummaryEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
|
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
|
import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
|
||||||
|
@ -50,8 +55,10 @@ internal class DefaultMembershipService @AssistedInject constructor(
|
||||||
private val inviteThreePidTask: InviteThreePidTask,
|
private val inviteThreePidTask: InviteThreePidTask,
|
||||||
private val membershipAdminTask: MembershipAdminTask,
|
private val membershipAdminTask: MembershipAdminTask,
|
||||||
private val roomDataSource: RoomDataSource,
|
private val roomDataSource: RoomDataSource,
|
||||||
|
private val cryptoService: CryptoService,
|
||||||
@UserId
|
@UserId
|
||||||
private val userId: String,
|
private val userId: String,
|
||||||
|
private val matrixConfiguration: MatrixConfiguration,
|
||||||
private val queryStringValueProcessor: QueryStringValueProcessor
|
private val queryStringValueProcessor: QueryStringValueProcessor
|
||||||
) : MembershipService {
|
) : MembershipService {
|
||||||
|
|
||||||
|
@ -139,10 +146,20 @@ internal class DefaultMembershipService @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun invite(userId: String, reason: String?) {
|
override suspend fun invite(userId: String, reason: String?) {
|
||||||
|
sendShareHistoryKeysIfNeeded(userId)
|
||||||
val params = InviteTask.Params(roomId, userId, reason)
|
val params = InviteTask.Params(roomId, userId, reason)
|
||||||
inviteTask.execute(params)
|
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) {
|
override suspend fun invite3pid(threePid: ThreePid) {
|
||||||
val params = InviteThreePidTask.Params(roomId, threePid)
|
val params = InviteThreePidTask.Params(roomId, threePid)
|
||||||
return inviteThreePidTask.execute(params)
|
return inviteThreePidTask.execute(params)
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
|
import org.matrix.android.sdk.api.extensions.ensureNotEmpty
|
||||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
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.Event
|
||||||
|
@ -700,6 +701,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||||
MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.")
|
MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.")
|
||||||
MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.")
|
MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.")
|
||||||
MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.")
|
MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.")
|
||||||
|
MessageType.MSGTYPE_BEACON_INFO -> return TextContent(content.body.ensureNotEmpty() ?: "shared live location.")
|
||||||
MessageType.MSGTYPE_POLL_START -> {
|
MessageType.MSGTYPE_POLL_START -> {
|
||||||
return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "")
|
return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "")
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.android.asCoroutineDispatcher
|
import kotlinx.coroutines.android.asCoroutineDispatcher
|
||||||
import kotlinx.coroutines.cancelChildren
|
import kotlinx.coroutines.cancelChildren
|
||||||
|
@ -116,6 +117,7 @@ internal class DefaultTimeline(
|
||||||
)
|
)
|
||||||
|
|
||||||
private var strategy: LoadTimelineStrategy = buildStrategy(LoadTimelineStrategy.Mode.Live)
|
private var strategy: LoadTimelineStrategy = buildStrategy(LoadTimelineStrategy.Mode.Live)
|
||||||
|
private var startTimelineJob: Job? = null
|
||||||
|
|
||||||
override val isLive: Boolean
|
override val isLive: Boolean
|
||||||
get() = !getPaginationState(Timeline.Direction.FORWARDS).hasMoreToLoad
|
get() = !getPaginationState(Timeline.Direction.FORWARDS).hasMoreToLoad
|
||||||
|
@ -143,7 +145,7 @@ internal class DefaultTimeline(
|
||||||
timelineScope.launch {
|
timelineScope.launch {
|
||||||
loadRoomMembersIfNeeded()
|
loadRoomMembersIfNeeded()
|
||||||
}
|
}
|
||||||
timelineScope.launch {
|
startTimelineJob = timelineScope.launch {
|
||||||
sequencer.post {
|
sequencer.post {
|
||||||
if (isStarted.compareAndSet(false, true)) {
|
if (isStarted.compareAndSet(false, true)) {
|
||||||
isFromThreadTimeline = rootThreadEventId != null
|
isFromThreadTimeline = rootThreadEventId != null
|
||||||
|
@ -174,8 +176,10 @@ internal class DefaultTimeline(
|
||||||
|
|
||||||
override fun restartWithEventId(eventId: String?) {
|
override fun restartWithEventId(eventId: String?) {
|
||||||
timelineScope.launch {
|
timelineScope.launch {
|
||||||
openAround(eventId, rootThreadEventId)
|
sequencer.post {
|
||||||
postSnapshot()
|
openAround(eventId, rootThreadEventId)
|
||||||
|
postSnapshot()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,6 +189,7 @@ internal class DefaultTimeline(
|
||||||
|
|
||||||
override fun paginate(direction: Timeline.Direction, count: Int) {
|
override fun paginate(direction: Timeline.Direction, count: Int) {
|
||||||
timelineScope.launch {
|
timelineScope.launch {
|
||||||
|
startTimelineJob?.join()
|
||||||
val postSnapshot = loadMore(count, direction, fetchOnServerIfNeeded = true)
|
val postSnapshot = loadMore(count, direction, fetchOnServerIfNeeded = true)
|
||||||
if (postSnapshot) {
|
if (postSnapshot) {
|
||||||
postSnapshot()
|
postSnapshot()
|
||||||
|
@ -193,6 +198,7 @@ internal class DefaultTimeline(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun awaitPaginate(direction: Timeline.Direction, count: Int): List<TimelineEvent> {
|
override suspend fun awaitPaginate(direction: Timeline.Direction, count: Int): List<TimelineEvent> {
|
||||||
|
startTimelineJob?.join()
|
||||||
withContext(timelineDispatcher) {
|
withContext(timelineDispatcher) {
|
||||||
loadMore(count, direction, fetchOnServerIfNeeded = true)
|
loadMore(count, direction, fetchOnServerIfNeeded = true)
|
||||||
}
|
}
|
||||||
|
@ -279,6 +285,7 @@ internal class DefaultTimeline(
|
||||||
direction = Timeline.Direction.BACKWARDS,
|
direction = Timeline.Direction.BACKWARDS,
|
||||||
fetchOnServerIfNeeded = false
|
fetchOnServerIfNeeded = false
|
||||||
)
|
)
|
||||||
|
|
||||||
Timber.v("$baseLogMessage finished")
|
Timber.v("$baseLogMessage finished")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,9 +319,11 @@ internal class DefaultTimeline(
|
||||||
|
|
||||||
private fun onLimitedTimeline() {
|
private fun onLimitedTimeline() {
|
||||||
timelineScope.launch {
|
timelineScope.launch {
|
||||||
initPaginationStates(null)
|
sequencer.post {
|
||||||
loadMore(settings.initialSize, Timeline.Direction.BACKWARDS, false)
|
initPaginationStates(null)
|
||||||
postSnapshot()
|
loadMore(settings.initialSize, Timeline.Direction.BACKWARDS, false)
|
||||||
|
postSnapshot()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import io.realm.RealmResults
|
import io.realm.RealmResults
|
||||||
import io.realm.kotlin.createObject
|
import io.realm.kotlin.createObject
|
||||||
|
import io.realm.kotlin.executeTransactionAwait
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
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) {
|
return when (mode) {
|
||||||
is Mode.Live -> {
|
is Mode.Live -> {
|
||||||
ChunkEntity.where(realm, roomId)
|
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
|
* Clear any existing thread chunk entity and create a new one, with the
|
||||||
* rootThreadEventId included.
|
* rootThreadEventId included.
|
||||||
*/
|
*/
|
||||||
private fun recreateThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
|
private suspend fun recreateThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
|
||||||
realm.executeTransaction {
|
realm.executeTransactionAwait {
|
||||||
// Lets delete the chunk and start a new one
|
// Lets delete the chunk and start a new one
|
||||||
ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
|
ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
|
||||||
Timber.i("###THREADS LoadTimelineStrategy [onStart] thread chunk cleared..")
|
Timber.i("###THREADS LoadTimelineStrategy [onStart] thread chunk cleared..")
|
||||||
|
@ -309,8 +310,8 @@ internal class LoadTimelineStrategy constructor(
|
||||||
/**
|
/**
|
||||||
* Clear any existing thread chunk.
|
* Clear any existing thread chunk.
|
||||||
*/
|
*/
|
||||||
private fun clearThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
|
private suspend fun clearThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
|
||||||
realm.executeTransaction {
|
realm.executeTransactionAwait {
|
||||||
ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
|
ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
|
||||||
Timber.i("###THREADS LoadTimelineStrategy [onStop] thread chunk cleared..")
|
Timber.i("###THREADS LoadTimelineStrategy [onStop] thread chunk cleared..")
|
||||||
}
|
}
|
||||||
|
|
|
@ -490,38 +490,11 @@ internal class TimelineChunk(
|
||||||
private fun handleDatabaseChangeSet(results: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet) {
|
private fun handleDatabaseChangeSet(results: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||||
val insertions = changeSet.insertionRanges
|
val insertions = changeSet.insertionRanges
|
||||||
for (range in insertions) {
|
for (range in insertions) {
|
||||||
// Check if the insertion's displayIndices match our expectations - or skip this insertion.
|
if (!validateInsertion(range, results)) continue
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val newItems = results
|
val newItems = results
|
||||||
.subList(range.startIndex, range.startIndex + range.length)
|
.subList(range.startIndex, range.startIndex + range.length)
|
||||||
.map { it.buildAndDecryptIfNeeded() }
|
.map { it.buildAndDecryptIfNeeded() }
|
||||||
|
|
||||||
builtEventsIndexes.entries.filter { it.value >= range.startIndex }.forEach { it.setValue(it.value + range.length) }
|
builtEventsIndexes.entries.filter { it.value >= range.startIndex }.forEach { it.setValue(it.value + range.length) }
|
||||||
newItems.mapIndexed { index, timelineEvent ->
|
newItems.mapIndexed { index, timelineEvent ->
|
||||||
if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) {
|
if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) {
|
||||||
|
@ -536,12 +509,9 @@ internal class TimelineChunk(
|
||||||
for (range in modifications) {
|
for (range in modifications) {
|
||||||
for (modificationIndex in (range.startIndex until range.startIndex + range.length)) {
|
for (modificationIndex in (range.startIndex until range.startIndex + range.length)) {
|
||||||
val updatedEntity = results[modificationIndex] ?: continue
|
val updatedEntity = results[modificationIndex] ?: continue
|
||||||
val displayIndex = builtEventsIndexes[updatedEntity.eventId]
|
val builtEventIndex = builtEventsIndexes[updatedEntity.eventId] ?: continue
|
||||||
if (displayIndex == null) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
builtEvents[displayIndex] = updatedEntity.buildAndDecryptIfNeeded()
|
builtEvents[builtEventIndex] = updatedEntity.buildAndDecryptIfNeeded()
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.v("Fail to update items at index: $modificationIndex")
|
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? {
|
private fun getNextDisplayIndex(direction: Timeline.Direction): Int? {
|
||||||
if (timelineEventEntities.isEmpty()) {
|
if (timelineEventEntities.isEmpty()) {
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.util
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet
|
||||||
|
|
||||||
internal interface BackgroundDetectionObserver : DefaultLifecycleObserver {
|
internal interface BackgroundDetectionObserver : DefaultLifecycleObserver {
|
||||||
val isInBackground: Boolean
|
val isInBackground: Boolean
|
||||||
|
@ -37,7 +38,7 @@ internal class DefaultBackgroundDetectionObserver : BackgroundDetectionObserver
|
||||||
override var isInBackground: Boolean = true
|
override var isInBackground: Boolean = true
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private val listeners = LinkedHashSet<BackgroundDetectionObserver.Listener>()
|
private val listeners = CopyOnWriteArraySet<BackgroundDetectionObserver.Listener>()
|
||||||
|
|
||||||
override fun register(listener: BackgroundDetectionObserver.Listener) {
|
override fun register(listener: BackgroundDetectionObserver.Listener) {
|
||||||
listeners.add(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,6 +51,7 @@ private const val A_LATITUDE = 1.4
|
||||||
private const val A_LONGITUDE = 40.0
|
private const val A_LONGITUDE = 40.0
|
||||||
private const val AN_UNCERTAINTY = 5.0
|
private const val AN_UNCERTAINTY = 5.0
|
||||||
private const val A_TIMEOUT = 15_000L
|
private const val A_TIMEOUT = 15_000L
|
||||||
|
private const val A_DESCRIPTION = "description"
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
internal class DefaultLocationSharingServiceTest {
|
internal class DefaultLocationSharingServiceTest {
|
||||||
|
@ -137,7 +138,7 @@ internal class DefaultLocationSharingServiceTest {
|
||||||
coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success("stopped-event-id")
|
coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success("stopped-event-id")
|
||||||
coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
|
coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
|
||||||
|
|
||||||
val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
|
val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION)
|
||||||
|
|
||||||
result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
|
result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
|
||||||
val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
|
val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
|
||||||
|
@ -150,7 +151,8 @@ internal class DefaultLocationSharingServiceTest {
|
||||||
coVerify { stopLiveLocationShareTask.execute(expectedStopParams) }
|
coVerify { stopLiveLocationShareTask.execute(expectedStopParams) }
|
||||||
val expectedStartParams = StartLiveLocationShareTask.Params(
|
val expectedStartParams = StartLiveLocationShareTask.Params(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
timeoutMillis = A_TIMEOUT
|
timeoutMillis = A_TIMEOUT,
|
||||||
|
description = A_DESCRIPTION
|
||||||
)
|
)
|
||||||
coVerify { startLiveLocationShareTask.execute(expectedStartParams) }
|
coVerify { startLiveLocationShareTask.execute(expectedStartParams) }
|
||||||
}
|
}
|
||||||
|
@ -161,7 +163,7 @@ internal class DefaultLocationSharingServiceTest {
|
||||||
val error = Throwable()
|
val error = Throwable()
|
||||||
coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Failure(error)
|
coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Failure(error)
|
||||||
|
|
||||||
val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
|
val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION)
|
||||||
|
|
||||||
result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error)
|
result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error)
|
||||||
val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
|
val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
|
||||||
|
@ -179,7 +181,7 @@ internal class DefaultLocationSharingServiceTest {
|
||||||
coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns false
|
coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns false
|
||||||
coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
|
coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
|
||||||
|
|
||||||
val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
|
val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION)
|
||||||
|
|
||||||
result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
|
result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
|
||||||
val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
|
val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
|
||||||
|
@ -188,7 +190,8 @@ internal class DefaultLocationSharingServiceTest {
|
||||||
coVerify { checkIfExistingActiveLiveTask.execute(expectedCheckExistingParams) }
|
coVerify { checkIfExistingActiveLiveTask.execute(expectedCheckExistingParams) }
|
||||||
val expectedStartParams = StartLiveLocationShareTask.Params(
|
val expectedStartParams = StartLiveLocationShareTask.Params(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
timeoutMillis = A_TIMEOUT
|
timeoutMillis = A_TIMEOUT,
|
||||||
|
description = A_DESCRIPTION
|
||||||
)
|
)
|
||||||
coVerify { startLiveLocationShareTask.execute(expectedStartParams) }
|
coVerify { startLiveLocationShareTask.execute(expectedStartParams) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import org.matrix.android.sdk.test.fakes.FakeSendStateTask
|
||||||
private const val A_USER_ID = "user-id"
|
private const val A_USER_ID = "user-id"
|
||||||
private const val A_ROOM_ID = "room-id"
|
private const val A_ROOM_ID = "room-id"
|
||||||
private const val AN_EVENT_ID = "event-id"
|
private const val AN_EVENT_ID = "event-id"
|
||||||
|
private const val A_DESCRIPTION = "description"
|
||||||
private const val A_TIMEOUT = 15_000L
|
private const val A_TIMEOUT = 15_000L
|
||||||
private const val AN_EPOCH = 1655210176L
|
private const val AN_EPOCH = 1655210176L
|
||||||
|
|
||||||
|
@ -58,7 +59,8 @@ internal class DefaultStartLiveLocationShareTaskTest {
|
||||||
fun `given parameters and no error when calling the task then result is success`() = runTest {
|
fun `given parameters and no error when calling the task then result is success`() = runTest {
|
||||||
val params = StartLiveLocationShareTask.Params(
|
val params = StartLiveLocationShareTask.Params(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
timeoutMillis = A_TIMEOUT
|
timeoutMillis = A_TIMEOUT,
|
||||||
|
description = A_DESCRIPTION
|
||||||
)
|
)
|
||||||
fakeClock.givenEpoch(AN_EPOCH)
|
fakeClock.givenEpoch(AN_EPOCH)
|
||||||
fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
|
fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
|
||||||
|
@ -67,6 +69,7 @@ internal class DefaultStartLiveLocationShareTaskTest {
|
||||||
|
|
||||||
result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
|
result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
|
||||||
val expectedBeaconContent = MessageBeaconInfoContent(
|
val expectedBeaconContent = MessageBeaconInfoContent(
|
||||||
|
body = A_DESCRIPTION,
|
||||||
timeout = params.timeoutMillis,
|
timeout = params.timeoutMillis,
|
||||||
isLive = true,
|
isLive = true,
|
||||||
unstableTimestampMillis = AN_EPOCH
|
unstableTimestampMillis = AN_EPOCH
|
||||||
|
@ -87,7 +90,8 @@ internal class DefaultStartLiveLocationShareTaskTest {
|
||||||
fun `given parameters and an empty returned event id when calling the task then result is failure`() = runTest {
|
fun `given parameters and an empty returned event id when calling the task then result is failure`() = runTest {
|
||||||
val params = StartLiveLocationShareTask.Params(
|
val params = StartLiveLocationShareTask.Params(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
timeoutMillis = A_TIMEOUT
|
timeoutMillis = A_TIMEOUT,
|
||||||
|
description = A_DESCRIPTION
|
||||||
)
|
)
|
||||||
fakeClock.givenEpoch(AN_EPOCH)
|
fakeClock.givenEpoch(AN_EPOCH)
|
||||||
fakeSendStateTask.givenExecuteRetryReturns("")
|
fakeSendStateTask.givenExecuteRetryReturns("")
|
||||||
|
@ -101,7 +105,8 @@ internal class DefaultStartLiveLocationShareTaskTest {
|
||||||
fun `given parameters and error during event sending when calling the task then result is failure`() = runTest {
|
fun `given parameters and error during event sending when calling the task then result is failure`() = runTest {
|
||||||
val params = StartLiveLocationShareTask.Params(
|
val params = StartLiveLocationShareTask.Params(
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
timeoutMillis = A_TIMEOUT
|
timeoutMillis = A_TIMEOUT,
|
||||||
|
description = A_DESCRIPTION
|
||||||
)
|
)
|
||||||
fakeClock.givenEpoch(AN_EPOCH)
|
fakeClock.givenEpoch(AN_EPOCH)
|
||||||
val error = Throwable()
|
val error = Throwable()
|
||||||
|
|
|
@ -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()) }
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue