diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration05.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration05.kt new file mode 100644 index 0000000000..3a2f15e5c9 --- /dev/null +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/migrations/AppMigration05.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 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.features.migration.impl.migrations + +import com.squareup.anvil.annotations.ContributesMultibinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.sessionstorage.api.SessionStore +import java.io.File +import javax.inject.Inject + +@ContributesMultibinding(AppScope::class) +class AppMigration05 @Inject constructor( + private val sessionStore: SessionStore, + private val baseDirectory: File, +) : AppMigration { + override val order: Int = 5 + + override suspend fun migrate() { + val allSessions = sessionStore.getAllSessions() + for (session in allSessions) { + if (session.sessionPath.isEmpty()) { + val sessionPath = File(baseDirectory, session.userId.replace(':', '_')).absolutePath + sessionStore.updateData(session.copy(sessionPath = sessionPath)) + } + } + } +} diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt index df3549b0f8..bd303f7945 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt @@ -51,5 +51,6 @@ fun aSessionData( isTokenValid = isTokenValid, loginType = LoginType.UNKNOWN, passphrase = null, + sessionPath = "/a/path/to/a/session", ) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 2ffbe5ab94..a8186de574 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -63,7 +63,7 @@ import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService import io.element.android.libraries.matrix.impl.sync.RustSyncService import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper -import io.element.android.libraries.matrix.impl.util.SessionDirectoryNameProvider +import io.element.android.libraries.matrix.impl.util.SessionDirectoryProvider import io.element.android.libraries.matrix.impl.util.cancelAndDestroy import io.element.android.libraries.matrix.impl.util.mxCallbackFlow import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService @@ -152,7 +152,7 @@ class RustMatrixClient( sessionDispatcher = sessionDispatcher, ) - private val sessionDirectoryNameProvider = SessionDirectoryNameProvider() + private val sessionDirectoryProvider = SessionDirectoryProvider(sessionStore) private val isLoggingOut = AtomicBoolean(false) @@ -172,6 +172,7 @@ class RustMatrixClient( isTokenValid = false, loginType = existingData.loginType, passphrase = existingData.passphrase, + sessionPath = existingData.sessionPath, ) sessionStore.updateData(newData) Timber.d("Removed session data with token: '...$anonymizedToken'.") @@ -199,6 +200,7 @@ class RustMatrixClient( isTokenValid = true, loginType = existingData.loginType, passphrase = existingData.passphrase, + sessionPath = existingData.sessionPath, ) sessionStore.updateData(newData) Timber.d("Saved new session data with token: '...$anonymizedToken'.") @@ -483,7 +485,7 @@ class RustMatrixClient( override suspend fun clearCache() { close() - baseDirectory.deleteSessionDirectory(deleteCryptoDb = false) + deleteSessionDirectory(deleteCryptoDb = false) } override suspend fun logout(ignoreSdkError: Boolean): String? = doLogout( @@ -513,7 +515,7 @@ class RustMatrixClient( } } close() - baseDirectory.deleteSessionDirectory(deleteCryptoDb = true) + deleteSessionDirectory(deleteCryptoDb = true) if (removeSession) { sessionStore.removeSession(sessionId.value) } @@ -570,8 +572,7 @@ class RustMatrixClient( private suspend fun File.getCacheSize( includeCryptoDb: Boolean = false, ): Long = withContext(sessionDispatcher) { - val sessionDirectoryName = sessionDirectoryNameProvider.provides(sessionId) - val sessionDirectory = File(this@getCacheSize, sessionDirectoryName) + val sessionDirectory = sessionDirectoryProvider.provides(sessionId) ?: return@withContext 0L if (includeCryptoDb) { sessionDirectory.getSizeOfFiles() } else { @@ -587,11 +588,10 @@ class RustMatrixClient( } } - private suspend fun File.deleteSessionDirectory( + private suspend fun deleteSessionDirectory( deleteCryptoDb: Boolean = false, ): Boolean = withContext(sessionDispatcher) { - val sessionDirectoryName = sessionDirectoryNameProvider.provides(sessionId) - val sessionDirectory = File(this@deleteSessionDirectory, sessionDirectoryName) + val sessionDirectory = sessionDirectoryProvider.provides(sessionId) ?: return@withContext false if (deleteCryptoDb) { // Delete the folder and all its content sessionDirectory.deleteRecursively() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt index cb0fbbefa4..4b3d89baac 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt @@ -46,7 +46,7 @@ class RustMatrixClientFactory @Inject constructor( private val utdTracker: UtdTracker, ) { suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) { - val client = getBaseClientBuilder() + val client = getBaseClientBuilder(sessionData.sessionPath) .homeserverUrl(sessionData.homeserverUrl) .username(sessionData.userId) .passphrase(sessionData.passphrase) @@ -71,9 +71,9 @@ class RustMatrixClientFactory @Inject constructor( ) } - internal fun getBaseClientBuilder(): ClientBuilder { + internal fun getBaseClientBuilder(sessionPath: String): ClientBuilder { return ClientBuilder() - .basePath(baseDirectory.absolutePath) + .sessionPath(sessionPath) .userAgent(userAgentProvider.provide()) .addRootCertificates(userCertificatesProvider.provides()) .serverVersions(listOf("v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5")) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfiguration.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfiguration.kt index 2f9a6e3bb8..df9168d954 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfiguration.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfiguration.kt @@ -18,19 +18,26 @@ package io.element.android.libraries.matrix.impl.auth import io.element.android.libraries.matrix.api.auth.OidcConfig import org.matrix.rustcomponents.sdk.OidcConfiguration +import java.io.File +import javax.inject.Inject -val oidcConfiguration: OidcConfiguration = OidcConfiguration( - clientName = "Element", - redirectUri = OidcConfig.REDIRECT_URI, - clientUri = "https://element.io", - logoUri = "https://element.io/mobile-icon.png", - tosUri = "https://element.io/acceptable-use-policy-terms", - policyUri = "https://element.io/privacy", - contacts = listOf( - "support@element.io", - ), - // Some homeservers/auth issuers don't support dynamic client registration, and have to be registered manually - staticRegistrations = mapOf( - "https://id.thirdroom.io/realms/thirdroom" to "elementx", - ), -) +class OidConfigurationProvider @Inject constructor( + private val baseDirectory: File, +) { + fun get(): OidcConfiguration = OidcConfiguration( + clientName = "Element", + redirectUri = OidcConfig.REDIRECT_URI, + clientUri = "https://element.io", + logoUri = "https://element.io/mobile-icon.png", + tosUri = "https://element.io/acceptable-use-policy-terms", + policyUri = "https://element.io/privacy", + contacts = listOf( + "support@element.io", + ), + // Some homeservers/auth issuers don't support dynamic client registration, and have to be registered manually + staticRegistrations = mapOf( + "https://id.thirdroom.io/realms/thirdroom" to "elementx", + ), + dynamicRegistrationsFile = File(baseDirectory, "oidc/registrations.json").absolutePath, + ) +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index d992c7fc1c..feb98ea9a9 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -54,6 +54,7 @@ import org.matrix.rustcomponents.sdk.QrLoginProgressListener import org.matrix.rustcomponents.sdk.use import timber.log.Timber import java.io.File +import java.util.UUID import javax.inject.Inject import org.matrix.rustcomponents.sdk.AuthenticationService as RustAuthenticationService @@ -68,17 +69,19 @@ class RustMatrixAuthenticationService @Inject constructor( private val passphraseGenerator: PassphraseGenerator, userCertificatesProvider: UserCertificatesProvider, proxyProvider: ProxyProvider, + private val oidConfigurationProvider: OidConfigurationProvider, ) : MatrixAuthenticationService { // Passphrase which will be used for new sessions. Existing sessions will use the passphrase // stored in the SessionData. private val pendingPassphrase = getDatabasePassphrase() + private val sessionPath = File(baseDirectory, UUID.randomUUID().toString()).absolutePath private val authService: RustAuthenticationService = RustAuthenticationService( - basePath = baseDirectory.absolutePath, + sessionPath = sessionPath, passphrase = pendingPassphrase, proxy = proxyProvider.provides(), userAgent = userAgentProvider.provide(), additionalRootCertificates = userCertificatesProvider.provides(), - oidcConfiguration = oidcConfiguration, + oidcConfiguration = oidConfigurationProvider.get(), customSlidingSyncProxy = null, sessionDelegate = null, crossProcessRefreshLockId = null, @@ -148,6 +151,7 @@ class RustMatrixAuthenticationService @Inject constructor( isTokenValid = true, loginType = LoginType.PASSWORD, passphrase = pendingPassphrase, + sessionPath = sessionPath, ) } sessionStore.storeData(sessionData) @@ -196,6 +200,7 @@ class RustMatrixAuthenticationService @Inject constructor( isTokenValid = true, loginType = LoginType.OIDC, passphrase = pendingPassphrase, + sessionPath = sessionPath, ) } pendingOidcAuthenticationData?.close() @@ -211,11 +216,11 @@ class RustMatrixAuthenticationService @Inject constructor( override suspend fun loginWithQrCode(qrCodeData: MatrixQrCodeLoginData, progress: (QrCodeLoginStep) -> Unit) = withContext(coroutineDispatchers.io) { runCatching { - val client = rustMatrixClientFactory.getBaseClientBuilder() + val client = rustMatrixClientFactory.getBaseClientBuilder(sessionPath) .passphrase(pendingPassphrase) .buildWithQrCode( qrCodeData = (qrCodeData as SdkQrCodeLoginData).rustQrCodeData, - oidcConfiguration = oidcConfiguration, + oidcConfiguration = oidConfigurationProvider.get(), progressListener = object : QrLoginProgressListener { override fun onUpdate(state: QrLoginProgress) { Timber.d("QR Code login progress: $state") @@ -229,6 +234,7 @@ class RustMatrixAuthenticationService @Inject constructor( isTokenValid = true, loginType = LoginType.QR, passphrase = pendingPassphrase, + sessionPath = sessionPath, ) sessionStore.storeData(sessionData) SessionId(sessionData.userId) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt index aea838b705..e7f9199a34 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt @@ -25,6 +25,7 @@ internal fun Session.toSessionData( isTokenValid: Boolean, loginType: LoginType, passphrase: String?, + sessionPath: String, ) = SessionData( userId = userId, deviceId = deviceId, @@ -37,4 +38,5 @@ internal fun Session.toSessionData( isTokenValid = isTokenValid, loginType = loginType, passphrase = passphrase, + sessionPath = sessionPath, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 593b41deea..b4fd51a03e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -281,6 +281,7 @@ class RustTimeline( messageEventContentFromParts(body, htmlBody).withMentions(mentions.map()).use { content -> runCatching { inner.send(content) + Unit } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/SessionDirectoryNameProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/SessionDirectoryProvider.kt similarity index 65% rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/SessionDirectoryNameProvider.kt rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/SessionDirectoryProvider.kt index de9d6c1520..f9b8edbd8e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/SessionDirectoryNameProvider.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/SessionDirectoryProvider.kt @@ -17,10 +17,15 @@ package io.element.android.libraries.matrix.impl.util import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.sessionstorage.api.SessionStore +import java.io.File +import javax.inject.Inject -class SessionDirectoryNameProvider { - // Rust sanitises the user ID replacing invalid characters with an _ - fun provides(sessionId: SessionId): String { - return sessionId.value.replace(":", "_") +class SessionDirectoryProvider @Inject constructor( + private val sessionStore: SessionStore, +) { + suspend fun provides(sessionId: SessionId): File? { + val path = sessionStore.getSession(sessionId.value)?.sessionPath ?: return null + return File(path) } } diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt index 76bd7f743a..30f721d68a 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt @@ -44,4 +44,6 @@ data class SessionData( val loginType: LoginType, /** The optional passphrase used to encrypt data in the SDK local store. */ val passphrase: String?, + /** The path to the session data stored in the filesystem. */ + val sessionPath: String, ) diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt index 3824def48c..026c13eadc 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt @@ -34,6 +34,7 @@ internal fun SessionData.toDbModel(): DbSessionData { isTokenValid = if (isTokenValid) 1L else 0L, loginType = loginType.name, passphrase = passphrase, + sessionPath = sessionPath, ) } @@ -50,5 +51,6 @@ internal fun DbSessionData.toApiModel(): SessionData { isTokenValid = isTokenValid == 1L, loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name), passphrase = passphrase, + sessionPath = sessionPath, ) } diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt index 8d22dcfa61..3df49bfb1f 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt @@ -32,7 +32,9 @@ import io.element.encrypteddb.passphrase.RandomSecretPassphraseProvider object SessionStorageModule { @Provides @SingleIn(AppScope::class) - fun provideMatrixDatabase(@ApplicationContext context: Context): SessionDatabase { + fun provideMatrixDatabase( + @ApplicationContext context: Context, + ): SessionDatabase { val name = "session_database" val secretFile = context.getDatabasePath("$name.key") diff --git a/libraries/session-storage/impl/src/main/sqldelight/databases/8.db b/libraries/session-storage/impl/src/main/sqldelight/databases/8.db new file mode 100644 index 0000000000..f2870ba857 Binary files /dev/null and b/libraries/session-storage/impl/src/main/sqldelight/databases/8.db differ diff --git a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq index c33b4d7c7e..74f268606a 100644 --- a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq +++ b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq @@ -23,7 +23,9 @@ CREATE TABLE SessionData ( isTokenValid INTEGER NOT NULL DEFAULT 1, loginType TEXT, -- added in version 5 - passphrase TEXT + passphrase TEXT, + -- added in version 6 + sessionPath TEXT NOT NULL DEFAULT "" ); diff --git a/libraries/session-storage/impl/src/main/sqldelight/migrations/7.sqm b/libraries/session-storage/impl/src/main/sqldelight/migrations/7.sqm new file mode 100644 index 0000000000..5814d23914 --- /dev/null +++ b/libraries/session-storage/impl/src/main/sqldelight/migrations/7.sqm @@ -0,0 +1,4 @@ +-- Migrate DB from version 7 +-- Add sessionPath so we can track the anonymized path for the session files dir + +ALTER TABLE SessionData ADD COLUMN sessionPath TEXT NOT NULL DEFAULT ""; diff --git a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt index 84c1142193..9ae6100ca1 100644 --- a/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt +++ b/libraries/session-storage/test/src/main/kotlin/io/element/android/libraries/sessionstorage/test/SessionData.kt @@ -36,5 +36,6 @@ fun aSessionData( isTokenValid = isTokenValid, loginType = LoginType.UNKNOWN, passphrase = null, + sessionPath = "/a/path/to/a/session", ) }