Store session data in a secure way (#98)

* Replace SessionData DataStore with an encrypted SQLite DB.

---------

Co-authored-by: Benoit Marty <benoit@matrix.org>
This commit is contained in:
Jorge Martin Espinosa 2023-03-02 16:48:54 +01:00 committed by GitHub
parent 381bd3fd3f
commit 6677f80abe
38 changed files with 600 additions and 199 deletions

View file

@ -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)
}

View file

@ -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 {
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>

View file

@ -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
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(
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(
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()

View file

@ -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
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(
}
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(
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(
)
}
}
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,
)

View file

@ -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

View file

@ -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() }
}
}

View file

@ -1,22 +0,0 @@
/*
* 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.matrix.session
import io.element.android.libraries.matrix.core.SessionId
import org.matrix.rustcomponents.sdk.Session
fun Session.sessionId() = SessionId("${userId}_${deviceId}")

View file

@ -1,29 +0,0 @@
/*
* 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.matrix.session
import io.element.android.libraries.matrix.core.SessionId
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()
}