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:
parent
381bd3fd3f
commit
6677f80abe
38 changed files with 600 additions and 199 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
@ -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}")
|
||||
|
|
@ -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()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue