diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index 42c9869523..8a9ea22926 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -141,7 +141,7 @@ class RootFlowNode @AssistedInject constructor( onSuccess(sessionId) } .onFailure { - Timber.v("Failed to restore session $sessionId") + Timber.e(it, "Failed to restore session $sessionId") onFailure() } } diff --git a/changelog.d/2219.misc b/changelog.d/2219.misc new file mode 100644 index 0000000000..c8c11e8105 --- /dev/null +++ b/changelog.d/2219.misc @@ -0,0 +1 @@ +Encrypt databases used by the Rust SDK on Nightly and Debug builds. 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 0182e87cf3..df3549b0f8 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 @@ -50,5 +50,6 @@ fun aSessionData( loginTimestamp = null, isTokenValid = isTokenValid, loginType = LoginType.UNKNOWN, + passphrase = null, ) } 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 a7b13c0b8d..b99811c61f 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 @@ -146,6 +146,7 @@ class RustMatrixClient( val newData = client.session().toSessionData( isTokenValid = false, loginType = existingData.loginType, + passphrase = existingData.passphrase, ) sessionStore.updateData(newData) Timber.d("Removed session data with token: '...$anonymizedToken'.") @@ -172,6 +173,7 @@ class RustMatrixClient( val newData = client.session().toSessionData( isTokenValid = true, loginType = existingData.loginType, + passphrase = existingData.passphrase, ) sessionStore.updateData(newData) Timber.d("Saved new session data with token: '...$anonymizedToken'.") 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 53aa560b97..0f1c47445b 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 @@ -44,6 +44,7 @@ class RustMatrixClientFactory @Inject constructor( .basePath(baseDirectory.absolutePath) .homeserverUrl(sessionData.homeserverUrl) .username(sessionData.userId) + .passphrase(sessionData.passphrase) .userAgent(userAgentProvider.provide()) // FIXME Quick and dirty fix for stopping version requests on startup https://github.com/matrix-org/matrix-rust-sdk/pull/1376 .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/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index bfce2b3ec2..b777988783 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 @@ -19,6 +19,8 @@ package io.element.android.libraries.matrix.impl.auth import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.mapFailure +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.MatrixClient @@ -28,6 +30,7 @@ import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.impl.RustMatrixClientFactory import io.element.android.libraries.matrix.impl.exception.mapClientException +import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator import io.element.android.libraries.matrix.impl.mapper.toSessionData import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.sessionstorage.api.LoggedInState @@ -39,6 +42,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.OidcAuthenticationData import org.matrix.rustcomponents.sdk.use +import timber.log.Timber import java.io.File import javax.inject.Inject import org.matrix.rustcomponents.sdk.AuthenticationService as RustAuthenticationService @@ -51,10 +55,15 @@ class RustMatrixAuthenticationService @Inject constructor( private val sessionStore: SessionStore, userAgentProvider: UserAgentProvider, private val rustMatrixClientFactory: RustMatrixClientFactory, + private val passphraseGenerator: PassphraseGenerator, + private val buildMeta: BuildMeta, ) : 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 authService: RustAuthenticationService = RustAuthenticationService( basePath = baseDirectory.absolutePath, - passphrase = null, + passphrase = pendingPassphrase, userAgent = userAgentProvider.provide(), oidcConfiguration = oidcConfiguration, customSlidingSyncProxy = null, @@ -76,6 +85,12 @@ class RustMatrixAuthenticationService @Inject constructor( val sessionData = sessionStore.getSession(sessionId.value) if (sessionData != null) { if (sessionData.isTokenValid) { + // Use the sessionData.passphrase, which can be null for a previously created session + if (sessionData.passphrase == null) { + Timber.w("Restoring a session without a passphrase") + } else { + Timber.w("Restoring a session with a passphrase") + } rustMatrixClientFactory.create(sessionData) } else { error("Token is not valid") @@ -88,6 +103,21 @@ class RustMatrixAuthenticationService @Inject constructor( } } + private fun getDatabasePassphrase(): String? { + // TODO Remove this if block at some point + // Return a passphrase only for debug and nightly build for now + if (buildMeta.buildType == BuildType.RELEASE) { + Timber.w("New sessions will not be encrypted with a passphrase (release build)") + return null + } + + val passphrase = passphraseGenerator.generatePassphrase() + if (passphrase != null) { + Timber.w("New sessions will be encrypted with a passphrase") + } + return passphrase + } + override fun getHomeserverDetails(): StateFlow = currentHomeserver override suspend fun setHomeserver(homeserver: String): Result = @@ -111,6 +141,7 @@ class RustMatrixAuthenticationService @Inject constructor( it.session().toSessionData( isTokenValid = true, loginType = LoginType.PASSWORD, + passphrase = pendingPassphrase, ) } sessionStore.storeData(sessionData) @@ -158,6 +189,7 @@ class RustMatrixAuthenticationService @Inject constructor( it.session().toSessionData( isTokenValid = true, loginType = LoginType.OIDC, + passphrase = pendingPassphrase ) } pendingOidcAuthenticationData?.close() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultPassphraseGenerator.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultPassphraseGenerator.kt new file mode 100644 index 0000000000..7e72dfeb55 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultPassphraseGenerator.kt @@ -0,0 +1,34 @@ +/* + * 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.libraries.matrix.impl.keys + +import android.util.Base64 +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import java.security.SecureRandom +import javax.inject.Inject + +private const val SECRET_SIZE = 256 + +@ContributesBinding(AppScope::class) +class DefaultPassphraseGenerator @Inject constructor() : PassphraseGenerator { + override fun generatePassphrase(): String? { + val key = ByteArray(size = SECRET_SIZE) + SecureRandom().nextBytes(key) + return Base64.encodeToString(key, Base64.NO_PADDING or Base64.NO_WRAP) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/PassphraseGenerator.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/PassphraseGenerator.kt new file mode 100644 index 0000000000..2b62a36d07 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/PassphraseGenerator.kt @@ -0,0 +1,25 @@ +/* + * 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.libraries.matrix.impl.keys + +interface PassphraseGenerator { + /** + * Generate a passphrase to encrypt the databases of a session. + * Return null to not encrypt the databases. + */ + fun generatePassphrase(): String? +} 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 fe21a460c8..aea838b705 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 @@ -24,6 +24,7 @@ import java.util.Date internal fun Session.toSessionData( isTokenValid: Boolean, loginType: LoginType, + passphrase: String?, ) = SessionData( userId = userId, deviceId = deviceId, @@ -35,4 +36,5 @@ internal fun Session.toSessionData( loginTimestamp = Date(), isTokenValid = isTokenValid, loginType = loginType, + passphrase = passphrase, ) 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 25a48c0efe..7189442716 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 @@ -29,4 +29,5 @@ data class SessionData( val loginTimestamp: Date?, val isTokenValid: Boolean, val loginType: LoginType, + val passphrase: 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 1a81647f5c..3824def48c 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 @@ -33,6 +33,7 @@ internal fun SessionData.toDbModel(): DbSessionData { loginTimestamp = loginTimestamp?.time, isTokenValid = if (isTokenValid) 1L else 0L, loginType = loginType.name, + passphrase = passphrase, ) } @@ -48,5 +49,6 @@ internal fun DbSessionData.toApiModel(): SessionData { loginTimestamp = loginTimestamp?.let { Date(it) }, isTokenValid = isTokenValid == 1L, loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name), + passphrase = passphrase, ) } diff --git a/libraries/session-storage/impl/src/main/sqldelight/databases/5.db b/libraries/session-storage/impl/src/main/sqldelight/databases/5.db new file mode 100644 index 0000000000..c6cf5ebef0 Binary files /dev/null and b/libraries/session-storage/impl/src/main/sqldelight/databases/5.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 d6d16cb6e2..c33b4d7c7e 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 @@ -21,7 +21,9 @@ CREATE TABLE SessionData ( oidcData TEXT, -- added in version 4 isTokenValid INTEGER NOT NULL DEFAULT 1, - loginType TEXT + loginType TEXT, + -- added in version 5 + passphrase TEXT ); diff --git a/libraries/session-storage/impl/src/main/sqldelight/migrations/4.sqm b/libraries/session-storage/impl/src/main/sqldelight/migrations/4.sqm new file mode 100644 index 0000000000..144d56959f --- /dev/null +++ b/libraries/session-storage/impl/src/main/sqldelight/migrations/4.sqm @@ -0,0 +1,3 @@ +-- Migrate DB from version 4 + +ALTER TABLE SessionData ADD COLUMN passphrase TEXT; diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt index a195c46c5c..760eefd20c 100644 --- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt @@ -44,6 +44,7 @@ class DatabaseSessionStoreTests { oidcData = "aOidcData", isTokenValid = 1, loginType = LoginType.UNKNOWN.name, + passphrase = null, ) @OptIn(ExperimentalCoroutinesApi::class) @@ -137,6 +138,7 @@ class DatabaseSessionStoreTests { oidcData = "aOidcData", isTokenValid = 1, loginType = null, + passphrase = "aPassphrase", ) val secondSessionData = SessionData( userId = "userId", @@ -149,6 +151,7 @@ class DatabaseSessionStoreTests { oidcData = "aOidcDataAltered", isTokenValid = 1, loginType = null, + passphrase = "aPassphraseAltered", ) assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId) assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp) @@ -168,5 +171,6 @@ class DatabaseSessionStoreTests { // Check that alteredSession.loginTimestamp is not altered, so equal to firstSessionData.loginTimestamp assertThat(alteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp) assertThat(alteredSession.oidcData).isEqualTo(secondSessionData.oidcData) + assertThat(alteredSession.passphrase).isEqualTo(secondSessionData.passphrase) } } diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt index 4d5c9aa216..30f5d2afe9 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt @@ -55,7 +55,9 @@ class MainActivity : ComponentActivity() { sessionStore = sessionStore, userAgentProvider = userAgentProvider, clock = DefaultSystemClock(), - ) + ), + passphraseGenerator = NullPassphraseGenerator(), + buildMeta = Singleton.buildMeta, ) } diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NullPassphraseGenerator.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NullPassphraseGenerator.kt new file mode 100644 index 0000000000..ab0117fd38 --- /dev/null +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NullPassphraseGenerator.kt @@ -0,0 +1,23 @@ +/* + * 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.samples.minimal + +import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator + +class NullPassphraseGenerator : PassphraseGenerator { + override fun generatePassphrase(): String? = null +} diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/Singleton.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/Singleton.kt index fc8fe81d8a..fc146d1ff3 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/Singleton.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/Singleton.kt @@ -29,7 +29,7 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.plus object Singleton { - private val buildMeta = BuildMeta( + val buildMeta = BuildMeta( isDebuggable = true, buildType = BuildType.DEBUG, applicationName = "EAX-Minimal",