Merge pull request #2244 from element-hq/feature/bma/encryptedDb2
Encrypted db
This commit is contained in:
commit
5e5eddd537
18 changed files with 140 additions and 5 deletions
|
|
@ -141,7 +141,7 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
onSuccess(sessionId)
|
onSuccess(sessionId)
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
Timber.v("Failed to restore session $sessionId")
|
Timber.e(it, "Failed to restore session $sessionId")
|
||||||
onFailure()
|
onFailure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
changelog.d/2219.misc
Normal file
1
changelog.d/2219.misc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Encrypt databases used by the Rust SDK on Nightly and Debug builds.
|
||||||
|
|
@ -50,5 +50,6 @@ fun aSessionData(
|
||||||
loginTimestamp = null,
|
loginTimestamp = null,
|
||||||
isTokenValid = isTokenValid,
|
isTokenValid = isTokenValid,
|
||||||
loginType = LoginType.UNKNOWN,
|
loginType = LoginType.UNKNOWN,
|
||||||
|
passphrase = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,7 @@ class RustMatrixClient(
|
||||||
val newData = client.session().toSessionData(
|
val newData = client.session().toSessionData(
|
||||||
isTokenValid = false,
|
isTokenValid = false,
|
||||||
loginType = existingData.loginType,
|
loginType = existingData.loginType,
|
||||||
|
passphrase = existingData.passphrase,
|
||||||
)
|
)
|
||||||
sessionStore.updateData(newData)
|
sessionStore.updateData(newData)
|
||||||
Timber.d("Removed session data with token: '...$anonymizedToken'.")
|
Timber.d("Removed session data with token: '...$anonymizedToken'.")
|
||||||
|
|
@ -172,6 +173,7 @@ class RustMatrixClient(
|
||||||
val newData = client.session().toSessionData(
|
val newData = client.session().toSessionData(
|
||||||
isTokenValid = true,
|
isTokenValid = true,
|
||||||
loginType = existingData.loginType,
|
loginType = existingData.loginType,
|
||||||
|
passphrase = existingData.passphrase,
|
||||||
)
|
)
|
||||||
sessionStore.updateData(newData)
|
sessionStore.updateData(newData)
|
||||||
Timber.d("Saved new session data with token: '...$anonymizedToken'.")
|
Timber.d("Saved new session data with token: '...$anonymizedToken'.")
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ class RustMatrixClientFactory @Inject constructor(
|
||||||
.basePath(baseDirectory.absolutePath)
|
.basePath(baseDirectory.absolutePath)
|
||||||
.homeserverUrl(sessionData.homeserverUrl)
|
.homeserverUrl(sessionData.homeserverUrl)
|
||||||
.username(sessionData.userId)
|
.username(sessionData.userId)
|
||||||
|
.passphrase(sessionData.passphrase)
|
||||||
.userAgent(userAgentProvider.provide())
|
.userAgent(userAgentProvider.provide())
|
||||||
// FIXME Quick and dirty fix for stopping version requests on startup https://github.com/matrix-org/matrix-rust-sdk/pull/1376
|
// 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"))
|
.serverVersions(listOf("v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5"))
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ package io.element.android.libraries.matrix.impl.auth
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.extensions.mapFailure
|
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.AppScope
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
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.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
|
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
|
||||||
import io.element.android.libraries.matrix.impl.exception.mapClientException
|
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.matrix.impl.mapper.toSessionData
|
||||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||||
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
import io.element.android.libraries.sessionstorage.api.LoggedInState
|
||||||
|
|
@ -39,6 +42,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.rustcomponents.sdk.OidcAuthenticationData
|
import org.matrix.rustcomponents.sdk.OidcAuthenticationData
|
||||||
import org.matrix.rustcomponents.sdk.use
|
import org.matrix.rustcomponents.sdk.use
|
||||||
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.matrix.rustcomponents.sdk.AuthenticationService as RustAuthenticationService
|
import org.matrix.rustcomponents.sdk.AuthenticationService as RustAuthenticationService
|
||||||
|
|
@ -51,10 +55,15 @@ class RustMatrixAuthenticationService @Inject constructor(
|
||||||
private val sessionStore: SessionStore,
|
private val sessionStore: SessionStore,
|
||||||
userAgentProvider: UserAgentProvider,
|
userAgentProvider: UserAgentProvider,
|
||||||
private val rustMatrixClientFactory: RustMatrixClientFactory,
|
private val rustMatrixClientFactory: RustMatrixClientFactory,
|
||||||
|
private val passphraseGenerator: PassphraseGenerator,
|
||||||
|
private val buildMeta: BuildMeta,
|
||||||
) : MatrixAuthenticationService {
|
) : 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(
|
private val authService: RustAuthenticationService = RustAuthenticationService(
|
||||||
basePath = baseDirectory.absolutePath,
|
basePath = baseDirectory.absolutePath,
|
||||||
passphrase = null,
|
passphrase = pendingPassphrase,
|
||||||
userAgent = userAgentProvider.provide(),
|
userAgent = userAgentProvider.provide(),
|
||||||
oidcConfiguration = oidcConfiguration,
|
oidcConfiguration = oidcConfiguration,
|
||||||
customSlidingSyncProxy = null,
|
customSlidingSyncProxy = null,
|
||||||
|
|
@ -76,6 +85,12 @@ class RustMatrixAuthenticationService @Inject constructor(
|
||||||
val sessionData = sessionStore.getSession(sessionId.value)
|
val sessionData = sessionStore.getSession(sessionId.value)
|
||||||
if (sessionData != null) {
|
if (sessionData != null) {
|
||||||
if (sessionData.isTokenValid) {
|
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)
|
rustMatrixClientFactory.create(sessionData)
|
||||||
} else {
|
} else {
|
||||||
error("Token is not valid")
|
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<MatrixHomeServerDetails?> = currentHomeserver
|
override fun getHomeserverDetails(): StateFlow<MatrixHomeServerDetails?> = currentHomeserver
|
||||||
|
|
||||||
override suspend fun setHomeserver(homeserver: String): Result<Unit> =
|
override suspend fun setHomeserver(homeserver: String): Result<Unit> =
|
||||||
|
|
@ -111,6 +141,7 @@ class RustMatrixAuthenticationService @Inject constructor(
|
||||||
it.session().toSessionData(
|
it.session().toSessionData(
|
||||||
isTokenValid = true,
|
isTokenValid = true,
|
||||||
loginType = LoginType.PASSWORD,
|
loginType = LoginType.PASSWORD,
|
||||||
|
passphrase = pendingPassphrase,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
sessionStore.storeData(sessionData)
|
sessionStore.storeData(sessionData)
|
||||||
|
|
@ -158,6 +189,7 @@ class RustMatrixAuthenticationService @Inject constructor(
|
||||||
it.session().toSessionData(
|
it.session().toSessionData(
|
||||||
isTokenValid = true,
|
isTokenValid = true,
|
||||||
loginType = LoginType.OIDC,
|
loginType = LoginType.OIDC,
|
||||||
|
passphrase = pendingPassphrase
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
pendingOidcAuthenticationData?.close()
|
pendingOidcAuthenticationData?.close()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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?
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,7 @@ import java.util.Date
|
||||||
internal fun Session.toSessionData(
|
internal fun Session.toSessionData(
|
||||||
isTokenValid: Boolean,
|
isTokenValid: Boolean,
|
||||||
loginType: LoginType,
|
loginType: LoginType,
|
||||||
|
passphrase: String?,
|
||||||
) = SessionData(
|
) = SessionData(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
deviceId = deviceId,
|
deviceId = deviceId,
|
||||||
|
|
@ -35,4 +36,5 @@ internal fun Session.toSessionData(
|
||||||
loginTimestamp = Date(),
|
loginTimestamp = Date(),
|
||||||
isTokenValid = isTokenValid,
|
isTokenValid = isTokenValid,
|
||||||
loginType = loginType,
|
loginType = loginType,
|
||||||
|
passphrase = passphrase,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -29,4 +29,5 @@ data class SessionData(
|
||||||
val loginTimestamp: Date?,
|
val loginTimestamp: Date?,
|
||||||
val isTokenValid: Boolean,
|
val isTokenValid: Boolean,
|
||||||
val loginType: LoginType,
|
val loginType: LoginType,
|
||||||
|
val passphrase: String?,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ internal fun SessionData.toDbModel(): DbSessionData {
|
||||||
loginTimestamp = loginTimestamp?.time,
|
loginTimestamp = loginTimestamp?.time,
|
||||||
isTokenValid = if (isTokenValid) 1L else 0L,
|
isTokenValid = if (isTokenValid) 1L else 0L,
|
||||||
loginType = loginType.name,
|
loginType = loginType.name,
|
||||||
|
passphrase = passphrase,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,5 +49,6 @@ internal fun DbSessionData.toApiModel(): SessionData {
|
||||||
loginTimestamp = loginTimestamp?.let { Date(it) },
|
loginTimestamp = loginTimestamp?.let { Date(it) },
|
||||||
isTokenValid = isTokenValid == 1L,
|
isTokenValid = isTokenValid == 1L,
|
||||||
loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name),
|
loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name),
|
||||||
|
passphrase = passphrase,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -21,7 +21,9 @@ CREATE TABLE SessionData (
|
||||||
oidcData TEXT,
|
oidcData TEXT,
|
||||||
-- added in version 4
|
-- added in version 4
|
||||||
isTokenValid INTEGER NOT NULL DEFAULT 1,
|
isTokenValid INTEGER NOT NULL DEFAULT 1,
|
||||||
loginType TEXT
|
loginType TEXT,
|
||||||
|
-- added in version 5
|
||||||
|
passphrase TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- Migrate DB from version 4
|
||||||
|
|
||||||
|
ALTER TABLE SessionData ADD COLUMN passphrase TEXT;
|
||||||
|
|
@ -44,6 +44,7 @@ class DatabaseSessionStoreTests {
|
||||||
oidcData = "aOidcData",
|
oidcData = "aOidcData",
|
||||||
isTokenValid = 1,
|
isTokenValid = 1,
|
||||||
loginType = LoginType.UNKNOWN.name,
|
loginType = LoginType.UNKNOWN.name,
|
||||||
|
passphrase = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
@ -137,6 +138,7 @@ class DatabaseSessionStoreTests {
|
||||||
oidcData = "aOidcData",
|
oidcData = "aOidcData",
|
||||||
isTokenValid = 1,
|
isTokenValid = 1,
|
||||||
loginType = null,
|
loginType = null,
|
||||||
|
passphrase = "aPassphrase",
|
||||||
)
|
)
|
||||||
val secondSessionData = SessionData(
|
val secondSessionData = SessionData(
|
||||||
userId = "userId",
|
userId = "userId",
|
||||||
|
|
@ -149,6 +151,7 @@ class DatabaseSessionStoreTests {
|
||||||
oidcData = "aOidcDataAltered",
|
oidcData = "aOidcDataAltered",
|
||||||
isTokenValid = 1,
|
isTokenValid = 1,
|
||||||
loginType = null,
|
loginType = null,
|
||||||
|
passphrase = "aPassphraseAltered",
|
||||||
)
|
)
|
||||||
assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId)
|
assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId)
|
||||||
assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp)
|
assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp)
|
||||||
|
|
@ -168,5 +171,6 @@ class DatabaseSessionStoreTests {
|
||||||
// Check that alteredSession.loginTimestamp is not altered, so equal to firstSessionData.loginTimestamp
|
// Check that alteredSession.loginTimestamp is not altered, so equal to firstSessionData.loginTimestamp
|
||||||
assertThat(alteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp)
|
assertThat(alteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp)
|
||||||
assertThat(alteredSession.oidcData).isEqualTo(secondSessionData.oidcData)
|
assertThat(alteredSession.oidcData).isEqualTo(secondSessionData.oidcData)
|
||||||
|
assertThat(alteredSession.passphrase).isEqualTo(secondSessionData.passphrase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,9 @@ class MainActivity : ComponentActivity() {
|
||||||
sessionStore = sessionStore,
|
sessionStore = sessionStore,
|
||||||
userAgentProvider = userAgentProvider,
|
userAgentProvider = userAgentProvider,
|
||||||
clock = DefaultSystemClock(),
|
clock = DefaultSystemClock(),
|
||||||
)
|
),
|
||||||
|
passphraseGenerator = NullPassphraseGenerator(),
|
||||||
|
buildMeta = Singleton.buildMeta,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -29,7 +29,7 @@ import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
|
|
||||||
object Singleton {
|
object Singleton {
|
||||||
private val buildMeta = BuildMeta(
|
val buildMeta = BuildMeta(
|
||||||
isDebuggable = true,
|
isDebuggable = true,
|
||||||
buildType = BuildType.DEBUG,
|
buildType = BuildType.DEBUG,
|
||||||
applicationName = "EAX-Minimal",
|
applicationName = "EAX-Minimal",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue