From 5a3e0b8c73bec953b25a8689ff0c2f8fa88ebff3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 17 Jan 2024 11:31:04 +0100 Subject: [PATCH 1/4] Better log. --- .../src/main/kotlin/io/element/android/appnav/RootFlowNode.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() } } From d04f76e8cf41b3d44126b288d53758428f87e487 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 18 Jan 2024 10:57:53 +0100 Subject: [PATCH 2/4] SessionData: add the passphrase. --- .../signedout/impl/SignedOutStateProvider.kt | 1 + .../libraries/matrix/impl/mapper/Session.kt | 2 ++ .../libraries/sessionstorage/api/SessionData.kt | 1 + .../sessionstorage/impl/SessionDataMapper.kt | 2 ++ .../impl/src/main/sqldelight/databases/5.db | Bin 0 -> 12288 bytes .../libraries/matrix/session/SessionData.sq | 4 +++- .../impl/src/main/sqldelight/migrations/4.sqm | 3 +++ 7 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 libraries/session-storage/impl/src/main/sqldelight/databases/5.db create mode 100644 libraries/session-storage/impl/src/main/sqldelight/migrations/4.sqm 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/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 0000000000000000000000000000000000000000..c6cf5ebef0910c8dcd0aab58d56993bb5f0f4517 GIT binary patch literal 12288 zcmeI#O;5rw7zgkUUqeFl=JiRqXd)(v-bI~;I8k93Igz?mX&f82)5zI_pTm#hC-G}| zwB15PqapD~{!Kb4&-3)AhwzVEp-5uo)Vnf9QCYJ}vwS3Wh*Oz(IeowpjN^;VZo-0o(U z6^ch0h@W|Rj0b^P*-!37{C%1{ti+8Hy8ji=f9@v(VU~Urs-vRc>U1PS8Q-=?ad57Z zr%{%Z*H$XT!%&1IV=@!iQ`2^~&nUT-u`kWkDIW}}(e&M8xAk{bEvrgo7+AI`OYCIH zGC%4iH~Ske@FjQX9@RUZPt|3{-a9vv1%V^5P$##AOHafKmY;|fB*y_009WhszA{m g4EO(8eZ4pr1Rwwb2tWV=5P$##AOHafKp+$N1d1T5+5i9m literal 0 HcmV?d00001 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; From 4e232f6e416a1ff6c5595228064df689a37525fe Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 18 Jan 2024 11:48:17 +0100 Subject: [PATCH 3/4] Encrypt databases used by the Rust SDK. The passphrase is stored in the SessionData, so that a Session created by Element Android can be restored. Existing sessions will have a null passphrase and will continue to work. New session will use a passphrase, only on Nightly and Debug build for now. --- changelog.d/2219.misc | 1 + .../libraries/matrix/impl/RustMatrixClient.kt | 2 ++ .../matrix/impl/RustMatrixClientFactory.kt | 1 + .../auth/RustMatrixAuthenticationService.kt | 34 ++++++++++++++++++- .../impl/keys/DefaultPassphraseGenerator.kt | 34 +++++++++++++++++++ .../matrix/impl/keys/PassphraseGenerator.kt | 25 ++++++++++++++ .../android/samples/minimal/MainActivity.kt | 4 ++- .../minimal/NullPassphraseGenerator.kt | 23 +++++++++++++ .../android/samples/minimal/Singleton.kt | 2 +- 9 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 changelog.d/2219.misc create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/DefaultPassphraseGenerator.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/keys/PassphraseGenerator.kt create mode 100644 samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NullPassphraseGenerator.kt 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/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 94e0eb56e4..8971fd78d8 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 @@ -144,6 +144,7 @@ class RustMatrixClient( val newData = client.session().toSessionData( isTokenValid = false, loginType = existingData.loginType, + passphrase = existingData.passphrase, ) sessionStore.updateData(newData) } @@ -161,6 +162,7 @@ class RustMatrixClient( val newData = client.session().toSessionData( isTokenValid = existingData.isTokenValid, loginType = existingData.loginType, + passphrase = existingData.passphrase, ) sessionStore.updateData(newData) } 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/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", From 95e228a9934f00321c2ea59d3913874d8dde415d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 18 Jan 2024 12:44:20 +0100 Subject: [PATCH 4/4] Fix test. --- .../sessionstorage/impl/DatabaseSessionStoreTests.kt | 4 ++++ 1 file changed, 4 insertions(+) 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) } }