Browse Source

Store session data in a secure way (#98)

* Replace SessionData DataStore with an encrypted SQLite DB.

---------

Co-authored-by: Benoit Marty <benoit@matrix.org>
misc/jme/add-logging-to-state-machine
Jorge Martin Espinosa 2 years ago committed by GitHub
parent
commit
6677f80abe
  1. 2
      README.md
  2. 11
      app/src/main/kotlin/io/element/android/x/di/MatrixClientsHolder.kt
  3. 15
      app/src/main/kotlin/io/element/android/x/node/RootFlowNode.kt
  4. 1
      changelog.d/84.feature
  5. 4
      features/login/src/main/kotlin/io/element/android/features/login/root/LoginRootScreen.kt
  6. 3
      features/login/src/test/kotlin/io/element/android/features/login/root/LoginRootPresenterTest.kt
  7. 9
      features/logout/src/test/kotlin/io/element/android/features/logout/LogoutPreferencePresenterTest.kt
  8. 7
      features/roomlist/src/main/kotlin/io/element/android/features/roomlist/RoomListPresenter.kt
  9. 18
      features/roomlist/src/test/kotlin/io/element/android/features/roomlist/RoomListPresenterTests.kt
  10. 8
      gradle/libs.versions.toml
  11. 6
      libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt
  12. 30
      libraries/encrypted-db/build.gradle.kts
  13. 43
      libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/SqlCipherDriverFactory.kt
  14. 15
      libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/PassphraseProvider.kt
  15. 59
      libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/RandomSecretPassphraseProvider.kt
  16. 3
      libraries/matrix/build.gradle.kts
  17. 2
      libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/MatrixClient.kt
  18. 10
      libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/RustMatrixClient.kt
  19. 40
      libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/auth/RustMatrixAuthenticationService.kt
  20. 5
      libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/core/SessionId.kt
  21. 109
      libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/session/PreferencesSessionStore.kt
  22. 5
      libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/FakeMatrixClient.kt
  23. 3
      libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/TestData.kt
  24. 12
      libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/auth/FakeAuthenticationService.kt
  25. 4
      libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/MatrixItemHelper.kt
  26. 51
      libraries/session-storage/build.gradle.kts
  27. 56
      libraries/session-storage/src/main/kotlin/io/element/android/libraries/sessionstorage/DatabaseSessionStore.kt
  28. 13
      libraries/session-storage/src/main/kotlin/io/element/android/libraries/sessionstorage/SessionStore.kt
  29. 43
      libraries/session-storage/src/main/kotlin/io/element/android/libraries/sessionstorage/di/SessionStorageModule.kt
  30. 21
      libraries/session-storage/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq
  31. 112
      libraries/session-storage/src/test/kotlin/io/element/android/libraries/sessionstorage/DatabaseSessionStoreTests.kt
  32. 2
      plugins/src/main/kotlin/Versions.kt
  33. 1
      samples/minimal/build.gradle.kts
  34. 50
      samples/minimal/src/main/kotlin/io/element/android/samples/minimal/InMemorySessionStore.kt
  35. 3
      samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt
  36. 20
      samples/minimal/src/main/res/values-night/themes.xml
  37. 1
      samples/minimal/src/main/res/values/themes.xml
  38. 2
      settings.gradle.kts

2
README.md

@ -10,7 +10,7 @@ @@ -10,7 +10,7 @@
ElementX Android is a [Matrix](https://matrix.org/) Android Client provided by [Element](https://element.io/).
The application is a total rewrite of [Element-Android](https://github.com/vector-im/element-android) using the [Matrix Rust SDK](https://github.com/matrix-org/matrix-rust-sdk) underneath and targeting devices running Android 5+. The UI layer is written using Jetpack compose.
The application is a total rewrite of [Element-Android](https://github.com/vector-im/element-android) using the [Matrix Rust SDK](https://github.com/matrix-org/matrix-rust-sdk) underneath and targeting devices running Android 6+. The UI layer is written using Jetpack compose.
<!--- TOC -->

11
app/src/main/kotlin/io/element/android/x/di/MatrixClientsHolder.kt

@ -22,6 +22,7 @@ import io.element.android.libraries.di.SingleIn @@ -22,6 +22,7 @@ import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.libraries.matrix.core.UserId
import kotlinx.coroutines.runBlocking
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
@ -57,13 +58,13 @@ class MatrixClientsHolder @Inject constructor(private val authenticationService: @@ -57,13 +58,13 @@ class MatrixClientsHolder @Inject constructor(private val authenticationService:
@Suppress("DEPRECATION")
fun restore(savedInstanceState: Bundle?) {
if (savedInstanceState == null || sessionIdsToMatrixClient.isNotEmpty()) return
val sessionIds = savedInstanceState.getSerializable(SAVE_INSTANCE_KEY) as? Array<SessionId>
if (sessionIds.isNullOrEmpty()) return
val userIds = savedInstanceState.getSerializable(SAVE_INSTANCE_KEY) as? Array<UserId>
if (userIds.isNullOrEmpty()) return
// Not ideal but should only happens in case of process recreation. This ensure we restore all the active sessions before restoring the node graphs.
runBlocking {
sessionIds.forEach { sessionId ->
Timber.v("Restore matrix session: $sessionId")
val matrixClient = authenticationService.restoreSession(sessionId)
userIds.forEach { userId ->
Timber.v("Restore matrix session: $userId")
val matrixClient = authenticationService.restoreSession(userId)
if (matrixClient != null) {
add(matrixClient)
}

15
app/src/main/kotlin/io/element/android/x/node/RootFlowNode.kt

@ -39,6 +39,7 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre @@ -39,6 +39,7 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
import io.element.android.libraries.di.DaggerComponentOwner
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.libraries.matrix.core.UserId
import io.element.android.x.di.MatrixClientsHolder
import io.element.android.x.root.RootPresenter
import io.element.android.x.root.RootView
@ -89,7 +90,7 @@ class RootFlowNode( @@ -89,7 +90,7 @@ class RootFlowNode(
}
private fun switchToLoggedInFlow(sessionId: SessionId) {
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId = sessionId))
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId))
}
private fun switchToLogoutFlow() {
@ -98,19 +99,19 @@ class RootFlowNode( @@ -98,19 +99,19 @@ class RootFlowNode(
}
private suspend fun tryToRestoreLatestSession(
onSuccess: (SessionId) -> Unit = {},
onSuccess: (UserId) -> Unit = {},
onFailure: () -> Unit = {}
) {
val latestKnownSessionId = authenticationService.getLatestSessionId()
if (latestKnownSessionId == null) {
val latestKnownUserId = authenticationService.getLatestSessionId()
if (latestKnownUserId == null) {
onFailure()
return
}
if (matrixClientsHolder.knowSession(latestKnownSessionId)) {
onSuccess(latestKnownSessionId)
if (matrixClientsHolder.knowSession(latestKnownUserId)) {
onSuccess(latestKnownUserId)
return
}
val matrixClient = authenticationService.restoreSession(latestKnownSessionId)
val matrixClient = authenticationService.restoreSession(UserId(latestKnownUserId.value))
if (matrixClient == null) {
Timber.v("Failed to restore session...")
onFailure()

1
changelog.d/84.feature

@ -0,0 +1 @@ @@ -0,0 +1 @@
Store session data in a secure storage.

4
features/login/src/main/kotlin/io/element/android/features/login/root/LoginRootScreen.kt

@ -58,7 +58,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon @@ -58,7 +58,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.libraries.matrix.core.UserId
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.R as StringR
@ -68,7 +68,7 @@ fun LoginRootScreen( @@ -68,7 +68,7 @@ fun LoginRootScreen(
state: LoginRootState,
modifier: Modifier = Modifier,
onChangeServer: () -> Unit = {},
onLoginWithSuccess: (SessionId) -> Unit = {},
onLoginWithSuccess: (UserId) -> Unit = {},
) {
val eventSink = state.eventSink
Box(

3
features/login/src/test/kotlin/io/element/android/features/login/root/LoginRootPresenterTest.kt

@ -22,7 +22,6 @@ import app.cash.molecule.RecompositionClock @@ -22,7 +22,6 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.libraries.matrixtest.A_HOMESERVER
import io.element.android.libraries.matrixtest.A_HOMESERVER_2
import io.element.android.libraries.matrixtest.A_PASSWORD
@ -88,7 +87,7 @@ class LoginRootPresenterTest { @@ -88,7 +87,7 @@ class LoginRootPresenterTest {
val submitState = awaitItem()
assertThat(submitState.loggedInState).isEqualTo(LoggedInState.LoggingIn)
val loggedInState = awaitItem()
assertThat(loggedInState.loggedInState).isEqualTo(LoggedInState.LoggedIn(SessionId(A_SESSION_ID)))
assertThat(loggedInState.loggedInState).isEqualTo(LoggedInState.LoggedIn(A_SESSION_ID))
}
}

9
features/logout/src/test/kotlin/io/element/android/features/logout/LogoutPreferencePresenterTest.kt

@ -23,8 +23,9 @@ import app.cash.molecule.moleculeFlow @@ -23,8 +23,9 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.libraries.matrixtest.A_SESSION_ID
import io.element.android.libraries.matrixtest.A_THROWABLE
import io.element.android.libraries.matrixtest.A_USER_ID
import io.element.android.libraries.matrixtest.FakeMatrixClient
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
@ -34,7 +35,7 @@ class LogoutPreferencePresenterTest { @@ -34,7 +35,7 @@ class LogoutPreferencePresenterTest {
@Test
fun `present - initial state`() = runTest {
val presenter = LogoutPreferencePresenter(
FakeMatrixClient(SessionId("sessionId")),
FakeMatrixClient(A_SESSION_ID),
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -47,7 +48,7 @@ class LogoutPreferencePresenterTest { @@ -47,7 +48,7 @@ class LogoutPreferencePresenterTest {
@Test
fun `present - logout`() = runTest {
val presenter = LogoutPreferencePresenter(
FakeMatrixClient(SessionId("sessionId")),
FakeMatrixClient(A_SESSION_ID),
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
@ -63,7 +64,7 @@ class LogoutPreferencePresenterTest { @@ -63,7 +64,7 @@ class LogoutPreferencePresenterTest {
@Test
fun `present - logout with error`() = runTest {
val matrixClient = FakeMatrixClient(SessionId("sessionId"))
val matrixClient = FakeMatrixClient(A_SESSION_ID)
val presenter = LogoutPreferencePresenter(
matrixClient,
)

7
features/roomlist/src/main/kotlin/io/element/android/features/roomlist/RoomListPresenter.kt

@ -35,6 +35,7 @@ import io.element.android.libraries.dateformatter.LastMessageFormatter @@ -35,6 +35,7 @@ import io.element.android.libraries.dateformatter.LastMessageFormatter
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.core.UserId
import io.element.android.libraries.matrix.room.RoomSummary
import io.element.android.libraries.matrix.ui.model.MatrixUser
import kotlinx.collections.immutable.ImmutableList
@ -107,14 +108,14 @@ class RoomListPresenter @Inject constructor( @@ -107,14 +108,14 @@ class RoomListPresenter @Inject constructor(
val userDisplayName = client.loadUserDisplayName().getOrNull()
val avatarData =
AvatarData(
id = client.userId().value,
id = client.sessionId.value,
name = userDisplayName,
url = userAvatarUrl,
size = AvatarSize.SMALL
)
matrixUser.value = MatrixUser(
id = client.userId(),
username = userDisplayName ?: client.userId().value,
id = UserId(client.sessionId.value),
username = userDisplayName ?: client.sessionId.value,
avatarData = avatarData,
)
}

18
features/roomlist/src/test/kotlin/io/element/android/features/roomlist/RoomListPresenterTests.kt

@ -26,12 +26,12 @@ import io.element.android.features.roomlist.model.RoomListEvents @@ -26,12 +26,12 @@ import io.element.android.features.roomlist.model.RoomListEvents
import io.element.android.features.roomlist.model.RoomListRoomSummary
import io.element.android.libraries.dateformatter.LastMessageFormatter
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.libraries.matrixtest.AN_AVATAR_URL
import io.element.android.libraries.matrixtest.AN_EXCEPTION
import io.element.android.libraries.matrixtest.A_MESSAGE
import io.element.android.libraries.matrixtest.A_ROOM_ID
import io.element.android.libraries.matrixtest.A_ROOM_NAME
import io.element.android.libraries.matrixtest.A_SESSION_ID
import io.element.android.libraries.matrixtest.A_USER_ID
import io.element.android.libraries.matrixtest.A_USER_NAME
import io.element.android.libraries.matrixtest.FakeMatrixClient
@ -46,9 +46,7 @@ class RoomListPresenterTests { @@ -46,9 +46,7 @@ class RoomListPresenterTests {
@Test
fun `present - should start with no user and then load user with success`() = runTest {
val presenter = RoomListPresenter(
FakeMatrixClient(
SessionId("sessionId")
),
FakeMatrixClient(A_SESSION_ID),
createDateFormatter()
)
moleculeFlow(RecompositionClock.Immediate) {
@ -69,7 +67,7 @@ class RoomListPresenterTests { @@ -69,7 +67,7 @@ class RoomListPresenterTests {
fun `present - should start with no user and then load user with error`() = runTest {
val presenter = RoomListPresenter(
FakeMatrixClient(
SessionId("sessionId"),
A_SESSION_ID,
userDisplayName = Result.failure(AN_EXCEPTION),
userAvatarURLString = Result.failure(AN_EXCEPTION),
),
@ -90,9 +88,7 @@ class RoomListPresenterTests { @@ -90,9 +88,7 @@ class RoomListPresenterTests {
@Test
fun `present - should filter room with success`() = runTest {
val presenter = RoomListPresenter(
FakeMatrixClient(
SessionId("sessionId")
),
FakeMatrixClient(A_SESSION_ID),
createDateFormatter()
)
moleculeFlow(RecompositionClock.Immediate) {
@ -112,7 +108,7 @@ class RoomListPresenterTests { @@ -112,7 +108,7 @@ class RoomListPresenterTests {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val presenter = RoomListPresenter(
FakeMatrixClient(
sessionId = SessionId("sessionId"),
sessionId = A_SESSION_ID,
roomSummaryDataSource = roomSummaryDataSource
),
createDateFormatter()
@ -138,7 +134,7 @@ class RoomListPresenterTests { @@ -138,7 +134,7 @@ class RoomListPresenterTests {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val presenter = RoomListPresenter(
FakeMatrixClient(
sessionId = SessionId("sessionId"),
sessionId = A_SESSION_ID,
roomSummaryDataSource = roomSummaryDataSource
),
createDateFormatter()
@ -169,7 +165,7 @@ class RoomListPresenterTests { @@ -169,7 +165,7 @@ class RoomListPresenterTests {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val presenter = RoomListPresenter(
FakeMatrixClient(
sessionId = SessionId("sessionId"),
sessionId = A_SESSION_ID,
roomSummaryDataSource = roomSummaryDataSource
),
createDateFormatter()

8
gradle/libs.versions.toml

@ -42,6 +42,7 @@ jsoup = "1.15.3" @@ -42,6 +42,7 @@ jsoup = "1.15.3"
appyx = "1.0.3"
dependencycheck = "7.4.4"
stem = "2.2.3"
sqldelight = "1.5.5"
# DI
dagger = "2.44.2"
@ -69,6 +70,7 @@ androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-kt @@ -69,6 +70,7 @@ androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-kt
androidx_lifecycle_compose = { module = "androidx.lifecycle:compose", version.ref = "lifecycle" }
androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
androidx_splash = "androidx.core:core-splashscreen:1.0.0"
androidx_security_crypto = "androidx.security:security-crypto:1.0.0"
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity_compose" }
androidx_startup = { module = "androidx.startup:startup-runtime", version.ref = "startup" }
@ -119,6 +121,11 @@ appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } @@ -119,6 +121,11 @@ appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.2"
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" }
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.3"
sqlite = "androidx.sqlite:sqlite:2.3.0"
# Di
inject = "javax.inject:javax.inject:1"
@ -149,3 +156,4 @@ stemlibrary = { id = "com.likethesalad.stem-library", version.ref = "stem" } @@ -149,3 +156,4 @@ stemlibrary = { id = "com.likethesalad.stem-library", version.ref = "stem" }
paparazzi = "app.cash.paparazzi:1.2.0"
sonarqube = "org.sonarqube:3.5.0.2730"
kover = "org.jetbrains.kotlinx.kover:0.6.1"
sqldelight = { id = "com.squareup.sqldelight", version.ref = "sqldelight" }

6
libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt

@ -39,17 +39,17 @@ class DateFormatters @Inject constructor( @@ -39,17 +39,17 @@ class DateFormatters @Inject constructor(
private val onlyTimeFormatter: DateTimeFormatter by lazy {
val pattern = DateFormat.getBestDateTimePattern(locale, "HH:mm") ?: "HH:mm"
DateTimeFormatter.ofPattern(pattern)
DateTimeFormatter.ofPattern(pattern, locale)
}
private val dateWithMonthFormatter: DateTimeFormatter by lazy {
val pattern = DateFormat.getBestDateTimePattern(locale, "d MMM") ?: "d MMM"
DateTimeFormatter.ofPattern(pattern)
DateTimeFormatter.ofPattern(pattern, locale)
}
private val dateWithYearFormatter: DateTimeFormatter by lazy {
val pattern = DateFormat.getBestDateTimePattern(locale, "dd.MM.yyyy") ?: "dd.MM.yyyy"
DateTimeFormatter.ofPattern(pattern)
DateTimeFormatter.ofPattern(pattern, locale)
}
internal fun formatTime(localDateTime: LocalDateTime): String {

30
libraries/encrypted-db/build.gradle.kts

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.encrypteddb"
}
dependencies {
implementation(libs.sqldelight.driver.android)
implementation(libs.sqlcipher)
implementation(libs.sqlite)
implementation(libs.androidx.security.crypto)
}

43
libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/SqlCipherDriverFactory.kt

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.encrypteddb
import android.content.Context
import com.squareup.sqldelight.android.AndroidSqliteDriver
import com.squareup.sqldelight.db.SqlDriver
import io.element.encrypteddb.passphrase.PassphraseProvider
import net.sqlcipher.database.SupportFactory
/**
* Creates an encrypted version of the [SqlDriver] using SQLCipher's [SupportFactory].
* @param passphraseProvider Provides the passphrase needed to use the SQLite database with SQLCipher.
*/
class SqlCipherDriverFactory(
private val passphraseProvider: PassphraseProvider,
) {
/**
* Returns a valid [SqlDriver] with SQLCipher support.
* @param schema The SQLite DB schema.
* @param name The name of the database to create.
* @param context Android [Context], used to instantiate the driver.
*/
fun create(schema: SqlDriver.Schema, name: String, context: Context): SqlDriver {
val passphrase = passphraseProvider.getPassphrase()
val factory = SupportFactory(passphrase)
return AndroidSqliteDriver(schema = schema, context = context, name = name, factory = factory)
}
}

15
libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/session/Session.kt → libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/PassphraseProvider.kt

@ -14,9 +14,14 @@ @@ -14,9 +14,14 @@
* limitations under the License.
*/
package io.element.android.libraries.matrix.session
package io.element.encrypteddb.passphrase
import io.element.android.libraries.matrix.core.SessionId
import org.matrix.rustcomponents.sdk.Session
fun Session.sessionId() = SessionId("${userId}_${deviceId}")
/**
* An abstraction to implement secure providers for SQLCipher passphrases.
*/
interface PassphraseProvider {
/**
* Returns a passphrase for SQLCipher in [ByteArray] format.
*/
fun getPassphrase(): ByteArray
}

59
libraries/encrypted-db/src/main/kotlin/io/element/encrypteddb/passphrase/RandomSecretPassphraseProvider.kt

@ -0,0 +1,59 @@ @@ -0,0 +1,59 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.encrypteddb.passphrase
import android.content.Context
import androidx.security.crypto.EncryptedFile
import java.io.File
import java.security.SecureRandom
/**
* Provides a secure passphrase for SQLCipher by generating a random secret and storing it into an [EncryptedFile].
* @param context Android [Context], used by [EncryptedFile] for cryptographic operations.
* @param file Destination file where the key will be stored.
* @param alias Alias of the key used to encrypt & decrypt the [EncryptedFile]'s contents.
* @param secretSize Length of the generated secret.
*/
class RandomSecretPassphraseProvider(
private val context: Context,
private val file: File,
private val alias: String,
private val secretSize: Int = 256,
) : PassphraseProvider {
override fun getPassphrase(): ByteArray {
val encryptedFile = EncryptedFile.Builder(
file,
context,
alias,
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
return if (!file.exists()) {
val secret = generateSecret()
encryptedFile.openFileOutput().use { it.write(secret) }
secret
} else {
encryptedFile.openFileInput().use { it.readBytes() }
}
}
private fun generateSecret(): ByteArray {
val buffer = ByteArray(size = secretSize)
SecureRandom().nextBytes(buffer)
return buffer
}
}

3
libraries/matrix/build.gradle.kts

@ -37,6 +37,7 @@ dependencies { @@ -37,6 +37,7 @@ dependencies {
implementation(libs.dagger)
implementation(projects.libraries.core)
implementation("net.java.dev.jna:jna:5.13.0@aar")
implementation(libs.androidx.datastore.preferences)
implementation(libs.serialization.json)
api(projects.libraries.sessionStorage)
implementation(libs.coroutines.core)
}

2
libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/MatrixClient.kt

@ -18,7 +18,6 @@ package io.element.android.libraries.matrix @@ -18,7 +18,6 @@ package io.element.android.libraries.matrix
import io.element.android.libraries.matrix.core.RoomId
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.libraries.matrix.core.UserId
import io.element.android.libraries.matrix.media.MediaResolver
import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrix.room.RoomSummaryDataSource
@ -33,7 +32,6 @@ interface MatrixClient : Closeable { @@ -33,7 +32,6 @@ interface MatrixClient : Closeable {
fun roomSummaryDataSource(): RoomSummaryDataSource
fun mediaResolver(): MediaResolver
suspend fun logout()
fun userId(): UserId
suspend fun loadUserDisplayName(): Result<String>
suspend fun loadUserAvatarURLString(): Result<String>
suspend fun loadMediaContentForSource(source: MediaSource): Result<ByteArray>

10
libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/RustMatrixClient.kt

@ -18,7 +18,6 @@ package io.element.android.libraries.matrix @@ -18,7 +18,6 @@ package io.element.android.libraries.matrix
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.core.RoomId
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.libraries.matrix.core.UserId
import io.element.android.libraries.matrix.media.MediaResolver
import io.element.android.libraries.matrix.media.RustMediaResolver
@ -26,9 +25,8 @@ import io.element.android.libraries.matrix.room.MatrixRoom @@ -26,9 +25,8 @@ import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrix.room.RoomSummaryDataSource
import io.element.android.libraries.matrix.room.RustMatrixRoom
import io.element.android.libraries.matrix.room.RustRoomSummaryDataSource
import io.element.android.libraries.matrix.session.SessionStore
import io.element.android.libraries.matrix.session.sessionId
import io.element.android.libraries.matrix.sync.SlidingSyncObserverProxy
import io.element.android.libraries.sessionstorage.SessionStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client
@ -51,7 +49,7 @@ class RustMatrixClient constructor( @@ -51,7 +49,7 @@ class RustMatrixClient constructor(
private val baseDirectory: File,
) : MatrixClient {
override val sessionId: SessionId = client.session().sessionId()
override val sessionId: UserId = UserId(client.userId())
private val clientDelegate = object : ClientDelegate {
override fun didReceiveAuthError(isSoftLogout: Boolean) {
@ -174,11 +172,9 @@ class RustMatrixClient constructor( @@ -174,11 +172,9 @@ class RustMatrixClient constructor(
Timber.e(failure, "Fail to call logout on HS. Still delete local files.")
}
baseDirectory.deleteSessionDirectory(userID = client.userId())
sessionStore.reset()
sessionStore.removeSession(client.userId())
}
override fun userId(): UserId = UserId(client.userId())
override suspend fun loadUserDisplayName(): Result<String> = withContext(dispatchers.io) {
runCatching {
client.displayName()

40
libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/auth/RustMatrixAuthenticationService.kt

@ -22,8 +22,9 @@ import io.element.android.libraries.di.AppScope @@ -22,8 +22,9 @@ import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.RustMatrixClient
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.libraries.matrix.session.SessionStore
import io.element.android.libraries.matrix.session.sessionId
import io.element.android.libraries.matrix.core.UserId
import io.element.android.libraries.matrix.session.SessionData
import io.element.android.libraries.sessionstorage.SessionStore
import io.element.android.libraries.matrix.util.logError
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@ -31,6 +32,7 @@ import kotlinx.coroutines.withContext @@ -31,6 +32,7 @@ import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.AuthenticationService
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientBuilder
import org.matrix.rustcomponents.sdk.Session
import timber.log.Timber
import java.io.File
import javax.inject.Inject
@ -49,18 +51,18 @@ class RustMatrixAuthenticationService @Inject constructor( @@ -49,18 +51,18 @@ class RustMatrixAuthenticationService @Inject constructor(
}
override suspend fun getLatestSessionId(): SessionId? = withContext(coroutineDispatchers.io) {
sessionStore.getLatestSession()?.sessionId()
sessionStore.getLatestSession()?.userId?.let { UserId(it) }
}
override suspend fun restoreSession(sessionId: SessionId) = withContext(coroutineDispatchers.io) {
sessionStore.getSession(sessionId)
?.let { session ->
sessionStore.getSession(sessionId.value)
?.let { sessionData ->
try {
ClientBuilder()
.basePath(baseDirectory.absolutePath)
.username(session.userId)
.username(sessionData.userId)
.build().apply {
restoreSession(session)
restoreSession(sessionData.toSession())
}
} catch (throwable: Throwable) {
logError(throwable)
@ -90,8 +92,8 @@ class RustMatrixAuthenticationService @Inject constructor( @@ -90,8 +92,8 @@ class RustMatrixAuthenticationService @Inject constructor(
throw failure
}
val session = client.session()
sessionStore.storeData(session)
session.sessionId()
sessionStore.storeData(session.toSessionData())
SessionId(session.userId)
}
private fun createMatrixClient(client: Client): MatrixClient {
@ -104,3 +106,23 @@ class RustMatrixAuthenticationService @Inject constructor( @@ -104,3 +106,23 @@ class RustMatrixAuthenticationService @Inject constructor(
)
}
}
private fun SessionData.toSession() = Session(
accessToken = accessToken,
refreshToken = refreshToken,
userId = userId,
deviceId = deviceId,
homeserverUrl = homeserverUrl,
isSoftLogout = isSoftLogout,
slidingSyncProxy = slidingSyncProxy,
)
private fun Session.toSessionData() = SessionData(
userId = userId,
deviceId = deviceId,
accessToken = accessToken,
refreshToken = refreshToken,
homeserverUrl = homeserverUrl,
isSoftLogout = isSoftLogout,
slidingSyncProxy = slidingSyncProxy,
)

5
libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/core/SessionId.kt

@ -16,7 +16,4 @@ @@ -16,7 +16,4 @@
package io.element.android.libraries.matrix.core
import java.io.Serializable
@JvmInline
value class SessionId(val value: String) : Serializable
typealias SessionId = UserId

109
libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/session/PreferencesSessionStore.kt

@ -1,109 +0,0 @@ @@ -1,109 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.session
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.core.SessionId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.matrix.rustcomponents.sdk.Session
import javax.inject.Inject
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_sessions")
// TODO It contains the access token, so it has to be stored in a more secured storage.
private val sessionKey = stringPreferencesKey("session")
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class PreferencesSessionStore @Inject constructor(
@ApplicationContext context: Context
) : SessionStore {
@Serializable
data class SessionData(
val accessToken: String,
val deviceId: String,
val homeserverUrl: String,
val isSoftLogout: Boolean,
val refreshToken: String?,
val userId: String,
val slidingSyncProxy: String?
)
private val store = context.dataStore
override fun isLoggedIn(): Flow<Boolean> {
return store.data.map { prefs ->
prefs[sessionKey] != null
}
}
override suspend fun storeData(session: Session) {
store.edit { prefs ->
val sessionData = SessionData(
accessToken = session.accessToken,
deviceId = session.deviceId,
homeserverUrl = session.homeserverUrl,
isSoftLogout = session.isSoftLogout,
refreshToken = session.refreshToken,
userId = session.userId,
slidingSyncProxy = session.slidingSyncProxy
)
val encodedSession = Json.encodeToString(sessionData)
prefs[sessionKey] = encodedSession
}
}
override suspend fun getLatestSession(): Session? {
return store.data.firstOrNull()?.let { prefs ->
val encodedSession = prefs[sessionKey] ?: return@let null
val sessionData = Json.decodeFromString<SessionData>(encodedSession)
Session(
accessToken = sessionData.accessToken,
deviceId = sessionData.deviceId,
homeserverUrl = sessionData.homeserverUrl,
isSoftLogout = sessionData.isSoftLogout,
refreshToken = sessionData.refreshToken,
userId = sessionData.userId,
slidingSyncProxy = sessionData.slidingSyncProxy
)
}
}
override suspend fun getSession(sessionId: SessionId): Session? {
//TODO we should have a proper session management
return getLatestSession()
}
override suspend fun reset() {
store.edit { it.clear() }
}
}

5
libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/FakeMatrixClient.kt

@ -19,7 +19,6 @@ package io.element.android.libraries.matrixtest @@ -19,7 +19,6 @@ package io.element.android.libraries.matrixtest
import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.core.RoomId
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.libraries.matrix.core.UserId
import io.element.android.libraries.matrix.media.MediaResolver
import io.element.android.libraries.matrix.room.MatrixRoom
import io.element.android.libraries.matrix.room.RoomSummaryDataSource
@ -30,7 +29,7 @@ import kotlinx.coroutines.delay @@ -30,7 +29,7 @@ import kotlinx.coroutines.delay
import org.matrix.rustcomponents.sdk.MediaSource
class FakeMatrixClient(
override val sessionId: SessionId = SessionId(A_SESSION_ID),
override val sessionId: SessionId = A_SESSION_ID,
private val userDisplayName: Result<String> = Result.success(A_USER_NAME),
private val userAvatarURLString: Result<String> = Result.success(AN_AVATAR_URL),
val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource()
@ -63,8 +62,6 @@ class FakeMatrixClient( @@ -63,8 +62,6 @@ class FakeMatrixClient(
logoutFailure?.let { throw it }
}
override fun userId(): UserId = A_USER_ID
override suspend fun loadUserDisplayName(): Result<String> {
return userDisplayName
}

3
libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/TestData.kt

@ -18,12 +18,14 @@ package io.element.android.libraries.matrixtest @@ -18,12 +18,14 @@ package io.element.android.libraries.matrixtest
import io.element.android.libraries.matrix.core.EventId
import io.element.android.libraries.matrix.core.RoomId
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.libraries.matrix.core.UserId
const val A_USER_NAME = "alice"
const val A_PASSWORD = "password"
val A_USER_ID = UserId("@alice:server.org")
val A_SESSION_ID = SessionId(A_USER_ID.value)
val A_ROOM_ID = RoomId("!aRoomId")
val AN_EVENT_ID = EventId("\$anEventId")
@ -34,7 +36,6 @@ const val ANOTHER_MESSAGE = "Hello universe!" @@ -34,7 +36,6 @@ const val ANOTHER_MESSAGE = "Hello universe!"
const val A_HOMESERVER = "matrix.org"
const val A_HOMESERVER_2 = "matrix-client.org"
const val A_SESSION_ID = "sessionId"
const val AN_AVATAR_URL = "mxc://data"

12
libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/auth/FakeAuthenticationService.kt

@ -18,9 +18,9 @@ package io.element.android.libraries.matrixtest.auth @@ -18,9 +18,9 @@ package io.element.android.libraries.matrixtest.auth
import io.element.android.libraries.matrix.MatrixClient
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.libraries.matrix.core.UserId
import io.element.android.libraries.matrixtest.A_HOMESERVER
import io.element.android.libraries.matrixtest.A_SESSION_ID
import io.element.android.libraries.matrixtest.A_USER_ID
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
@ -33,11 +33,11 @@ class FakeAuthenticationService : MatrixAuthenticationService { @@ -33,11 +33,11 @@ class FakeAuthenticationService : MatrixAuthenticationService {
return flowOf(false)
}
override suspend fun getLatestSessionId(): SessionId? {
override suspend fun getLatestSessionId(): UserId? {
return null
}
override suspend fun restoreSession(sessionId: SessionId): MatrixClient? {
override suspend fun restoreSession(userId: UserId): MatrixClient? {
return null
}
@ -57,10 +57,10 @@ class FakeAuthenticationService : MatrixAuthenticationService { @@ -57,10 +57,10 @@ class FakeAuthenticationService : MatrixAuthenticationService {
delay(100)
}
override suspend fun login(username: String, password: String): SessionId {
override suspend fun login(username: String, password: String): UserId {
delay(100)
loginError?.let { throw it }
return SessionId(A_SESSION_ID)
return A_USER_ID
}
fun givenLoginError(throwable: Throwable?) {

4
libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/MatrixItemHelper.kt

@ -38,13 +38,13 @@ class MatrixItemHelper @Inject constructor( @@ -38,13 +38,13 @@ class MatrixItemHelper @Inject constructor(
val userDisplayName = client.loadUserDisplayName().getOrNull()
val avatarData =
AvatarData(
client.userId().value,
client.sessionId.value,
userDisplayName,
userAvatarUrl,
avatarSize
)
MatrixUser(
id = client.userId(),
id = client.sessionId,
username = userDisplayName,
avatarData = avatarData,
)

51
libraries/session-storage/build.gradle.kts

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("io.element.android-library")
alias(libs.plugins.anvil)
alias(libs.plugins.sqldelight)
}
android {
namespace = "io.element.android.libraries.sessionstorage"
}
anvil {
generateDaggerFactories.set(true)
}
dependencies {
implementation(libs.dagger)
implementation(projects.libraries.core)
implementation(projects.libraries.encryptedDb)
implementation(libs.sqldelight.driver.android)
implementation(libs.sqlcipher)
implementation(libs.sqlite)
implementation(libs.androidx.security.crypto)
implementation(projects.libraries.di)
implementation(libs.sqldelight.coroutines)
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(libs.coroutines.test)
testImplementation(libs.sqldelight.driver.jvm)
}
sqldelight {
database("SessionDatabase") {}
}

56
libraries/session-storage/src/main/kotlin/io/element/android/libraries/sessionstorage/DatabaseSessionStore.kt

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.sessionstorage
import com.squareup.anvil.annotations.ContributesBinding
import com.squareup.sqldelight.runtime.coroutines.asFlow
import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.session.SessionData
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DatabaseSessionStore @Inject constructor(
private val database: SessionDatabase,
) : SessionStore {
override fun isLoggedIn(): Flow<Boolean> {
return database.sessionDataQueries.selectFirst().asFlow().mapToOneOrNull().map { it != null }
}
override suspend fun storeData(sessionData: SessionData) {
database.sessionDataQueries.insertSessionData(sessionData)
}
override suspend fun getLatestSession(): SessionData? {
return database.sessionDataQueries.selectFirst()
.executeAsOneOrNull()
}
override suspend fun getSession(sessionId: String): SessionData? {
return database.sessionDataQueries.selectByUserId(sessionId)
.executeAsOneOrNull()
}
override suspend fun removeSession(sessionId: String) {
database.sessionDataQueries.removeSession(sessionId)
}
}

13
libraries/matrix/src/main/kotlin/io/element/android/libraries/matrix/session/SessionStore.kt → libraries/session-storage/src/main/kotlin/io/element/android/libraries/sessionstorage/SessionStore.kt

@ -14,16 +14,15 @@ @@ -14,16 +14,15 @@
* limitations under the License.
*/
package io.element.android.libraries.matrix.session
package io.element.android.libraries.sessionstorage
import io.element.android.libraries.matrix.core.SessionId
import io.element.android.libraries.matrix.session.SessionData
import kotlinx.coroutines.flow.Flow
import org.matrix.rustcomponents.sdk.Session
interface SessionStore {
fun isLoggedIn(): Flow<Boolean>
suspend fun storeData(session: Session)
suspend fun getSession(sessionId: SessionId): Session?
suspend fun getLatestSession(): Session?
suspend fun reset()
suspend fun storeData(session: SessionData)
suspend fun getSession(sessionId: String): SessionData?
suspend fun getLatestSession(): SessionData?
suspend fun removeSession(sessionId: String)
}

43
libraries/session-storage/src/main/kotlin/io/element/android/libraries/sessionstorage/di/SessionStorageModule.kt

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.sessionstorage.di
import android.content.Context
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.sessionstorage.SessionDatabase
import io.element.encrypteddb.SqlCipherDriverFactory
import io.element.encrypteddb.passphrase.RandomSecretPassphraseProvider
@Module
@ContributesTo(AppScope::class)
object SessionStorageModule {
@Provides
@SingleIn(AppScope::class)
fun provideMatrixDatabase(@ApplicationContext context: Context): SessionDatabase {
val name = "session_database"
val secretFile = context.getDatabasePath("$name.key")
val passphraseProvider = RandomSecretPassphraseProvider(context, secretFile, name)
val driver = SqlCipherDriverFactory(passphraseProvider)
.create(SessionDatabase.Schema, "$name.db", context)
return SessionDatabase(driver)
}
}

21
libraries/session-storage/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
CREATE TABLE SessionData (
userId TEXT NOT NULL PRIMARY KEY,
deviceId TEXT NOT NULL,
accessToken TEXT NOT NULL,
refreshToken TEXT,
homeserverUrl TEXT NOT NULL,
isSoftLogout INTEGER AS Boolean NOT NULL DEFAULT 0,
slidingSyncProxy TEXT
);
selectFirst:
SELECT * FROM SessionData LIMIT 1;
selectByUserId:
SELECT * FROM SessionData WHERE userId = ?;
insertSessionData:
INSERT INTO SessionData(userId, deviceId, accessToken, refreshToken, homeserverUrl, isSoftLogout, slidingSyncProxy) VALUES ?;
removeSession:
DELETE FROM SessionData WHERE userId = ?;

112
libraries/session-storage/src/test/kotlin/io/element/android/libraries/sessionstorage/DatabaseSessionStoreTests.kt

@ -0,0 +1,112 @@ @@ -0,0 +1,112 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.sessionstorage
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
import io.element.android.libraries.matrix.session.SessionData
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class DatabaseSessionStoreTests {
private lateinit var database: SessionDatabase
private lateinit var databaseSessionStore: DatabaseSessionStore
private val aSessionData = SessionData(
userId = "userId",
deviceId = "deviceId",
accessToken = "accessToken",
refreshToken = "refreshToken",
homeserverUrl = "homeserverUrl",
isSoftLogout = false,
slidingSyncProxy = null
)
@Before
fun setup() {
// Initialise in memory SQLite driver
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
SessionDatabase.Schema.create(driver)
database = SessionDatabase(driver)
databaseSessionStore = DatabaseSessionStore(database)
}
@Test
fun `storeData persists the SessionData into the DB`() = runTest {
assertThat(database.sessionDataQueries.selectFirst().executeAsOneOrNull()).isNull()
databaseSessionStore.storeData(aSessionData)
assertThat(database.sessionDataQueries.selectFirst().executeAsOneOrNull()).isEqualTo(aSessionData)
}
@Test
fun `isLoggedIn emits true while there are sessions in the DB`() = runTest {
databaseSessionStore.isLoggedIn().test {
assertThat(awaitItem()).isFalse()
database.sessionDataQueries.insertSessionData(aSessionData)
assertThat(awaitItem()).isTrue()
database.sessionDataQueries.removeSession(aSessionData.userId)
assertThat(awaitItem()).isFalse()
}
}
@Test
fun `getLatestSession gets the first session in the DB`() = runTest {
database.sessionDataQueries.insertSessionData(aSessionData)
database.sessionDataQueries.insertSessionData(aSessionData.copy(userId = "otherUserId"))
val latestSession = databaseSessionStore.getLatestSession()
assertThat(latestSession).isEqualTo(aSessionData)
}
@Test
fun `getSession returns a matching session in DB if exists`() = runTest {
database.sessionDataQueries.insertSessionData(aSessionData)
database.sessionDataQueries.insertSessionData(aSessionData.copy(userId = "otherUserId"))
val foundSession = databaseSessionStore.getSession(aSessionData.userId)
assertThat(foundSession).isEqualTo(aSessionData)
}
@Test
fun `getSession returns null if a no matching session exists in DB`() = runTest {
database.sessionDataQueries.insertSessionData(aSessionData.copy(userId = "otherUserId"))
val foundSession = databaseSessionStore.getSession(aSessionData.userId)
assertThat(foundSession).isNull()
}
@Test
fun `removeSession removes the associated session in DB`() = runTest {
database.sessionDataQueries.insertSessionData(aSessionData)
databaseSessionStore.removeSession(aSessionData.userId)
assertThat(database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOneOrNull()).isNull()
}
}

2
plugins/src/main/kotlin/Versions.kt

@ -23,7 +23,7 @@ object Versions { @@ -23,7 +23,7 @@ object Versions {
const val compileSdk = 33
const val targetSdk = 33
const val minSdk = 21
const val minSdk = 23
val javaCompileVersion = JavaVersion.VERSION_11
val javaLanguageVersion: JavaLanguageVersion = JavaLanguageVersion.of(11)
}

1
samples/minimal/build.gradle.kts

@ -54,5 +54,6 @@ dependencies { @@ -54,5 +54,6 @@ dependencies {
implementation(projects.libraries.dateformatter)
implementation(projects.features.roomlist)
implementation(projects.features.login)
implementation(libs.coroutines.core)
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.2")
}

50
samples/minimal/src/main/kotlin/io/element/android/samples/minimal/InMemorySessionStore.kt

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.samples.minimal
import io.element.android.libraries.matrix.session.SessionData
import io.element.android.libraries.sessionstorage.SessionStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
class InMemorySessionStore : SessionStore {
private var sessionData = MutableStateFlow<SessionData?>(null)
override fun isLoggedIn(): Flow<Boolean> {
return sessionData.map { it != null }
}
override suspend fun storeData(session: SessionData) {
sessionData.value = session
}
override suspend fun getSession(sessionId: String): SessionData? {
return sessionData.value.takeIf { it?.userId == sessionId }
}
override suspend fun getLatestSession(): SessionData? {
return sessionData.value
}
override suspend fun removeSession(sessionId: String) {
if (sessionData.value?.userId == sessionId) {
sessionData.value = null
}
}
}

3
samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt

@ -31,7 +31,6 @@ import androidx.core.view.WindowCompat @@ -31,7 +31,6 @@ import androidx.core.view.WindowCompat
import io.element.android.libraries.designsystem.theme.ElementTheme
import io.element.android.libraries.matrix.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.auth.RustMatrixAuthenticationService
import io.element.android.libraries.matrix.session.PreferencesSessionStore
import kotlinx.coroutines.runBlocking
import org.matrix.rustcomponents.sdk.AuthenticationService
import java.io.File
@ -45,7 +44,7 @@ class MainActivity : ComponentActivity() { @@ -45,7 +44,7 @@ class MainActivity : ComponentActivity() {
baseDirectory = baseDirectory,
coroutineScope = Singleton.appScope,
coroutineDispatchers = Singleton.coroutineDispatchers,
sessionStore = PreferencesSessionStore(applicationContext),
sessionStore = InMemorySessionStore(),
authService = AuthenticationService(baseDirectory.absolutePath, null, null),
)
}

20
samples/minimal/src/main/res/values-night/themes.xml

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2023 New Vector Ltd
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<resources>
<style name="Theme.ElementX" parent="android:Theme.Material.NoActionBar" />
</resources>

1
samples/minimal/src/main/res/values/themes.xml

@ -16,6 +16,5 @@ @@ -16,6 +16,5 @@
-->
<resources>
<style name="Theme.ElementX" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

2
settings.gradle.kts

@ -63,3 +63,5 @@ include(":libraries:matrixtest") @@ -63,3 +63,5 @@ include(":libraries:matrixtest")
include(":features:template")
include(":libraries:androidutils")
include(":samples:minimal")
include(":libraries:encrypted-db")
include(":libraries:session-storage")

Loading…
Cancel
Save