mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-04 01:19:33 +00:00
Merge branch 'develop' into feature/bma/customMapStyle
This commit is contained in:
@@ -22,7 +22,7 @@ camera = "1.6.1"
|
|||||||
work = "2.11.2"
|
work = "2.11.2"
|
||||||
|
|
||||||
# Compose
|
# Compose
|
||||||
compose_bom = "2026.05.00"
|
compose_bom = "2026.05.01"
|
||||||
|
|
||||||
# Coroutines
|
# Coroutines
|
||||||
coroutines = "1.11.0"
|
coroutines = "1.11.0"
|
||||||
@@ -32,7 +32,7 @@ accompanist = "0.37.3"
|
|||||||
|
|
||||||
# Test
|
# Test
|
||||||
test_core = "1.7.0"
|
test_core = "1.7.0"
|
||||||
roborazzi = "1.60.0"
|
roborazzi = "1.63.0"
|
||||||
|
|
||||||
# Jetbrain
|
# Jetbrain
|
||||||
datetime = "0.8.0"
|
datetime = "0.8.0"
|
||||||
@@ -235,7 +235,7 @@ sigpwned_emoji4j = "com.sigpwned:emoji4j-core:16.0.0"
|
|||||||
metro_runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" }
|
metro_runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" }
|
||||||
|
|
||||||
# Element Call
|
# Element Call
|
||||||
element_call_embedded = "io.element.android:element-call-embedded:0.19.4"
|
element_call_embedded = "io.element.android:element-call-embedded:0.20.0"
|
||||||
|
|
||||||
# Auto services
|
# Auto services
|
||||||
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" }
|
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" }
|
||||||
|
|||||||
+1
@@ -17,4 +17,5 @@ data class SetHttpPusherData(
|
|||||||
val profileTag: String?,
|
val profileTag: String?,
|
||||||
val lang: String,
|
val lang: String,
|
||||||
val defaultPayload: String,
|
val defaultPayload: String,
|
||||||
|
val append: Boolean,
|
||||||
)
|
)
|
||||||
|
|||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.impl
|
||||||
|
|
||||||
|
import okio.ByteString.Companion.decodeHex
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a client secret used to encrypt/decrypt data from databases, which can be either a passphrase or a raw key.
|
||||||
|
*/
|
||||||
|
sealed interface ClientSecret {
|
||||||
|
/**
|
||||||
|
* A passphrase that can be used to derive a key for encryption/decryption.
|
||||||
|
*/
|
||||||
|
data class Passphrase(val value: String) : ClientSecret {
|
||||||
|
override fun formattedAsString(): String = toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A raw key that can be directly used for encryption/decryption. The key is represented as a byte array, and is formatted as a string in the form of
|
||||||
|
* `x'...'` where the bytes are encoded as hex characters.
|
||||||
|
*/
|
||||||
|
data class RawKey(val bytes: ByteArray) : ClientSecret {
|
||||||
|
override fun formattedAsString() = "x'${bytes.toHexString()}'"
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
return bytes.contentEquals((other as RawKey).bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return bytes.contentHashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return formattedAsString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the client secret as a string that can be parsed back with [fromString].
|
||||||
|
* For a passphrase, this is just the passphrase value. For a raw key, this is the hex-encoded representation of the key formatted as `x'...'`.
|
||||||
|
*/
|
||||||
|
fun formattedAsString(): String
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Parse a string representation of a client secret, which can be either a passphrase or a raw key formatted as `x'...'`.
|
||||||
|
*/
|
||||||
|
fun fromString(secret: String): ClientSecret {
|
||||||
|
val regex = Regex("^x'([0-9a-fA-F]+)'$")
|
||||||
|
val rawKeyMatch = regex.matchEntire(secret)
|
||||||
|
return if (rawKeyMatch != null) {
|
||||||
|
RawKey(rawKeyMatch.groupValues[1].decodeHex().toByteArray())
|
||||||
|
} else {
|
||||||
|
Passphrase(secret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-2
@@ -65,10 +65,22 @@ class RustClientSessionDelegate(
|
|||||||
|
|
||||||
// This always runs on a background thread, so we *can* do blocking calls here, although we should avoid doing heavy work
|
// This always runs on a background thread, so we *can* do blocking calls here, although we should avoid doing heavy work
|
||||||
override fun saveSessionInKeychain(session: Session) {
|
override fun saveSessionInKeychain(session: Session) {
|
||||||
|
Timber.tag(loggerTag.value).i("Saving new session info for user ${session.userId} after a token refresh")
|
||||||
runCatchingExceptions {
|
runCatchingExceptions {
|
||||||
val existingData = runBlocking { sessionStore.getSession(session.userId) } ?: return
|
val existingData = runBlocking { sessionStore.getSession(session.userId) } ?: return
|
||||||
|
|
||||||
|
if (existingData.accessToken == session.accessToken) {
|
||||||
|
Timber.tag(loggerTag.value).e("Access token is the same as the one already stored, this should not happen after a token refresh!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingData.refreshToken == session.refreshToken) {
|
||||||
|
Timber.tag(loggerTag.value).e("Refresh token is the same as the one already stored, this should not happen after a token refresh!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val (anonymizedAccessToken, anonymizedRefreshToken) = session.anonymizedTokens()
|
val (anonymizedAccessToken, anonymizedRefreshToken) = session.anonymizedTokens()
|
||||||
Timber.tag(loggerTag.value).d(
|
Timber.tag(loggerTag.value).i(
|
||||||
"Saving new session data with token: access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'. " +
|
"Saving new session data with token: access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'. " +
|
||||||
"Was token valid: ${existingData.isTokenValid}"
|
"Was token valid: ${existingData.isTokenValid}"
|
||||||
)
|
)
|
||||||
@@ -79,7 +91,7 @@ class RustClientSessionDelegate(
|
|||||||
sessionPaths = existingData.getSessionPaths(),
|
sessionPaths = existingData.getSessionPaths(),
|
||||||
)
|
)
|
||||||
runBlocking { sessionStore.updateData(newData) }
|
runBlocking { sessionStore.updateData(newData) }
|
||||||
Timber.tag(loggerTag.value).d("Saved new session data with access token: '$anonymizedAccessToken'.")
|
Timber.tag(loggerTag.value).i("Saved new session data.")
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.tag(loggerTag.value).e(it, "Failed to save new session data.")
|
Timber.tag(loggerTag.value).e(it, "Failed to save new session data.")
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -75,9 +75,11 @@ class RustMatrixClientFactory(
|
|||||||
)
|
)
|
||||||
|
|
||||||
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
|
suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) {
|
||||||
|
// This secret is called 'passphrase' for historical reasons, but it can be a raw key or an actual passphrase
|
||||||
|
val clientSecret = sessionData.passphrase?.let(ClientSecret::fromString)
|
||||||
val client = getBaseClientBuilder(
|
val client = getBaseClientBuilder(
|
||||||
sessionPaths = sessionData.getSessionPaths(),
|
sessionPaths = sessionData.getSessionPaths(),
|
||||||
passphrase = sessionData.passphrase,
|
clientSecret = clientSecret,
|
||||||
slidingSyncType = ClientBuilderSlidingSync.Restored,
|
slidingSyncType = ClientBuilderSlidingSync.Restored,
|
||||||
)
|
)
|
||||||
.homeserverUrl(sessionData.homeserverUrl)
|
.homeserverUrl(sessionData.homeserverUrl)
|
||||||
@@ -131,19 +133,19 @@ class RustMatrixClientFactory(
|
|||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
workManagerScheduler = workManagerScheduler,
|
workManagerScheduler = workManagerScheduler,
|
||||||
).also {
|
).also {
|
||||||
Timber.tag(it.toString()).d("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'")
|
Timber.tag("RustMatrixClient").i("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun getBaseClientBuilder(
|
internal suspend fun getBaseClientBuilder(
|
||||||
sessionPaths: SessionPaths,
|
sessionPaths: SessionPaths,
|
||||||
passphrase: String?,
|
clientSecret: ClientSecret?,
|
||||||
slidingSyncType: ClientBuilderSlidingSync,
|
slidingSyncType: ClientBuilderSlidingSync,
|
||||||
): ClientBuilder {
|
): ClientBuilder {
|
||||||
return clientBuilderProvider.provide()
|
return clientBuilderProvider.provide()
|
||||||
.run {
|
.run {
|
||||||
sqliteStoreBuilderProvider.provide(sessionPaths)
|
sqliteStoreBuilderProvider.provide(sessionPaths)
|
||||||
.passphrase(passphrase)
|
.secret(clientSecret)
|
||||||
.setupClientBuilder(this)
|
.setupClientBuilder(this)
|
||||||
}
|
}
|
||||||
.setSessionDelegate(sessionDelegate)
|
.setSessionDelegate(sessionDelegate)
|
||||||
|
|||||||
+13
-15
@@ -30,12 +30,13 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
|||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.matrix.impl.ClientBuilderSlidingSync
|
import io.element.android.libraries.matrix.impl.ClientBuilderSlidingSync
|
||||||
|
import io.element.android.libraries.matrix.impl.ClientSecret
|
||||||
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
|
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
|
||||||
import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper
|
import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper
|
||||||
import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData
|
import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData
|
||||||
import io.element.android.libraries.matrix.impl.auth.qrlogin.toStep
|
import io.element.android.libraries.matrix.impl.auth.qrlogin.toStep
|
||||||
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.keys.SecretGenerator
|
||||||
import io.element.android.libraries.matrix.impl.mapper.toSessionData
|
import io.element.android.libraries.matrix.impl.mapper.toSessionData
|
||||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||||
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
|
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
|
||||||
@@ -65,7 +66,7 @@ class RustMatrixAuthenticationService(
|
|||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
private val sessionStore: SessionStore,
|
private val sessionStore: SessionStore,
|
||||||
private val rustMatrixClientFactory: RustMatrixClientFactory,
|
private val rustMatrixClientFactory: RustMatrixClientFactory,
|
||||||
private val passphraseGenerator: PassphraseGenerator,
|
private val secretGenerator: SecretGenerator,
|
||||||
private val oAuthConfigurationProvider: OAuthConfigurationProvider,
|
private val oAuthConfigurationProvider: OAuthConfigurationProvider,
|
||||||
private val enterpriseService: EnterpriseService,
|
private val enterpriseService: EnterpriseService,
|
||||||
) : MatrixAuthenticationService {
|
) : MatrixAuthenticationService {
|
||||||
@@ -74,7 +75,7 @@ class RustMatrixAuthenticationService(
|
|||||||
|
|
||||||
// Passphrase which will be used for new sessions. Existing sessions will use the passphrase
|
// Passphrase which will be used for new sessions. Existing sessions will use the passphrase
|
||||||
// stored in the SessionData.
|
// stored in the SessionData.
|
||||||
private val pendingPassphrase = getDatabasePassphrase()
|
private val pendingKey by lazy { getDatabaseKey() }
|
||||||
|
|
||||||
// Need to keep a copy of the current session path to eventually delete it.
|
// Need to keep a copy of the current session path to eventually delete it.
|
||||||
// Ideally it would be possible to get the sessionPath from the Client to avoid doing this.
|
// Ideally it would be possible to get the sessionPath from the Client to avoid doing this.
|
||||||
@@ -115,12 +116,9 @@ class RustMatrixAuthenticationService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDatabasePassphrase(): String? {
|
private fun getDatabaseKey(): ClientSecret {
|
||||||
val passphrase = passphraseGenerator.generatePassphrase()
|
Timber.d("New sessions will be encrypted with a raw key")
|
||||||
if (passphrase != null) {
|
return secretGenerator.generateKey()
|
||||||
Timber.w("New sessions will be encrypted with a passphrase")
|
|
||||||
}
|
|
||||||
return passphrase
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setHomeserver(homeserver: String): Result<MatrixHomeServerDetails> =
|
override suspend fun setHomeserver(homeserver: String): Result<MatrixHomeServerDetails> =
|
||||||
@@ -159,7 +157,7 @@ class RustMatrixAuthenticationService(
|
|||||||
.toSessionData(
|
.toSessionData(
|
||||||
isTokenValid = true,
|
isTokenValid = true,
|
||||||
loginType = LoginType.PASSWORD,
|
loginType = LoginType.PASSWORD,
|
||||||
passphrase = pendingPassphrase,
|
passphrase = pendingKey.formattedAsString(),
|
||||||
sessionPaths = currentSessionPaths,
|
sessionPaths = currentSessionPaths,
|
||||||
)
|
)
|
||||||
val matrixClient = rustMatrixClientFactory.create(client)
|
val matrixClient = rustMatrixClientFactory.create(client)
|
||||||
@@ -231,7 +229,7 @@ class RustMatrixAuthenticationService(
|
|||||||
val sessionData = externalSession.toSessionData(
|
val sessionData = externalSession.toSessionData(
|
||||||
isTokenValid = true,
|
isTokenValid = true,
|
||||||
loginType = LoginType.PASSWORD,
|
loginType = LoginType.PASSWORD,
|
||||||
passphrase = pendingPassphrase,
|
passphrase = pendingKey.formattedAsString(),
|
||||||
sessionPaths = currentSessionPaths,
|
sessionPaths = currentSessionPaths,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -324,7 +322,7 @@ class RustMatrixAuthenticationService(
|
|||||||
val sessionData = client.session().toSessionData(
|
val sessionData = client.session().toSessionData(
|
||||||
isTokenValid = true,
|
isTokenValid = true,
|
||||||
loginType = LoginType.OIDC,
|
loginType = LoginType.OIDC,
|
||||||
passphrase = pendingPassphrase,
|
passphrase = pendingKey.formattedAsString(),
|
||||||
sessionPaths = currentSessionPaths,
|
sessionPaths = currentSessionPaths,
|
||||||
)
|
)
|
||||||
val matrixClient = rustMatrixClientFactory.create(client)
|
val matrixClient = rustMatrixClientFactory.create(client)
|
||||||
@@ -389,7 +387,7 @@ class RustMatrixAuthenticationService(
|
|||||||
.toSessionData(
|
.toSessionData(
|
||||||
isTokenValid = true,
|
isTokenValid = true,
|
||||||
loginType = LoginType.QR,
|
loginType = LoginType.QR,
|
||||||
passphrase = pendingPassphrase,
|
passphrase = pendingKey.formattedAsString(),
|
||||||
sessionPaths = emptySessionPaths,
|
sessionPaths = emptySessionPaths,
|
||||||
)
|
)
|
||||||
val matrixClient = rustMatrixClientFactory.create(client)
|
val matrixClient = rustMatrixClientFactory.create(client)
|
||||||
@@ -422,7 +420,7 @@ class RustMatrixAuthenticationService(
|
|||||||
return rustMatrixClientFactory
|
return rustMatrixClientFactory
|
||||||
.getBaseClientBuilder(
|
.getBaseClientBuilder(
|
||||||
sessionPaths = sessionPaths,
|
sessionPaths = sessionPaths,
|
||||||
passphrase = pendingPassphrase,
|
clientSecret = pendingKey,
|
||||||
slidingSyncType = ClientBuilderSlidingSync.Discovered,
|
slidingSyncType = ClientBuilderSlidingSync.Discovered,
|
||||||
)
|
)
|
||||||
.config()
|
.config()
|
||||||
@@ -454,7 +452,7 @@ class RustMatrixAuthenticationService(
|
|||||||
return rustMatrixClientFactory
|
return rustMatrixClientFactory
|
||||||
.getBaseClientBuilder(
|
.getBaseClientBuilder(
|
||||||
sessionPaths = sessionPaths,
|
sessionPaths = sessionPaths,
|
||||||
passphrase = pendingPassphrase,
|
clientSecret = pendingKey,
|
||||||
slidingSyncType = ClientBuilderSlidingSync.Discovered,
|
slidingSyncType = ClientBuilderSlidingSync.Discovered,
|
||||||
)
|
)
|
||||||
.serverNameOrHomeserverUrl(baseUrlOrServerName)
|
.serverNameOrHomeserverUrl(baseUrlOrServerName)
|
||||||
|
|||||||
+11
-6
@@ -11,15 +11,20 @@ package io.element.android.libraries.matrix.impl.keys
|
|||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import io.element.android.libraries.matrix.impl.ClientSecret
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
|
||||||
private const val SECRET_SIZE = 256
|
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultPassphraseGenerator : PassphraseGenerator {
|
class DefaultSecretGenerator : SecretGenerator {
|
||||||
override fun generatePassphrase(): String? {
|
override fun generatePassphrase(size: Int): ClientSecret.Passphrase? {
|
||||||
val key = ByteArray(size = SECRET_SIZE)
|
val key = ByteArray(size = size)
|
||||||
SecureRandom().nextBytes(key)
|
SecureRandom().nextBytes(key)
|
||||||
return Base64.encodeToString(key, Base64.NO_PADDING or Base64.NO_WRAP)
|
return ClientSecret.Passphrase(Base64.encodeToString(key, Base64.NO_PADDING or Base64.NO_WRAP))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun generateKey(size: Int): ClientSecret.RawKey {
|
||||||
|
val key = ByteArray(size = size)
|
||||||
|
SecureRandom().nextBytes(key)
|
||||||
|
return ClientSecret.RawKey(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2024, 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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?
|
|
||||||
}
|
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
* Copyright 2024, 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.impl.keys
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.impl.ClientSecret
|
||||||
|
|
||||||
|
private const val PASSPHRASE_SIZE = 256
|
||||||
|
private const val KEY_SIZE = 32
|
||||||
|
|
||||||
|
interface SecretGenerator {
|
||||||
|
/**
|
||||||
|
* Generate a passphrase to encrypt the databases.
|
||||||
|
* @param size the size of the passphrase in bytes, before encoding. The default value is 256 bytes.
|
||||||
|
* @return either a random passphrase or `null` to not encrypt the databases.
|
||||||
|
*/
|
||||||
|
fun generatePassphrase(size: Int = PASSPHRASE_SIZE): ClientSecret.Passphrase?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a key to encrypt the databases.
|
||||||
|
* @param size the size of the key in bytes. The default value is 32 bytes.
|
||||||
|
* @return a random key.
|
||||||
|
*/
|
||||||
|
fun generateKey(size: Int = KEY_SIZE): ClientSecret.RawKey
|
||||||
|
}
|
||||||
+1
-1
@@ -45,7 +45,7 @@ class RustPushersService(
|
|||||||
deviceDisplayName = setHttpPusherData.deviceDisplayName,
|
deviceDisplayName = setHttpPusherData.deviceDisplayName,
|
||||||
profileTag = setHttpPusherData.profileTag,
|
profileTag = setHttpPusherData.profileTag,
|
||||||
lang = setHttpPusherData.lang,
|
lang = setHttpPusherData.lang,
|
||||||
append = false,
|
append = setHttpPusherData.append,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.mapFailure { it.mapClientException() }
|
.mapFailure { it.mapClientException() }
|
||||||
|
|||||||
+33
-3
@@ -9,12 +9,23 @@ package io.element.android.libraries.matrix.impl.storage
|
|||||||
|
|
||||||
import io.element.android.libraries.core.data.ByteUnit
|
import io.element.android.libraries.core.data.ByteUnit
|
||||||
import io.element.android.libraries.core.data.megaBytes
|
import io.element.android.libraries.core.data.megaBytes
|
||||||
|
import io.element.android.libraries.matrix.impl.ClientSecret
|
||||||
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
import io.element.android.libraries.matrix.impl.paths.SessionPaths
|
||||||
import org.matrix.rustcomponents.sdk.ClientBuilder
|
import org.matrix.rustcomponents.sdk.ClientBuilder
|
||||||
import org.matrix.rustcomponents.sdk.SqliteStoreBuilder as SdkSqliteStoreBuilder
|
import org.matrix.rustcomponents.sdk.SqliteStoreBuilder as SdkSqliteStoreBuilder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstraction over the SDK's [SdkSqliteStoreBuilder] to allow configuring it with a ClientSecret and to hide the SDK from the rest of the codebase.
|
||||||
|
*/
|
||||||
interface SqliteStoreBuilder {
|
interface SqliteStoreBuilder {
|
||||||
fun passphrase(passphrase: String?): SqliteStoreBuilder
|
/**
|
||||||
|
* Configure the builder with a [ClientSecret], if provided. If the [clientSecret] is null, the databases will not be encrypted.
|
||||||
|
*/
|
||||||
|
fun secret(clientSecret: ClientSecret?): SqliteStoreBuilder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the provided [clientBuilder] with the configured [SdkSqliteStoreBuilder] and return it.
|
||||||
|
*/
|
||||||
fun setupClientBuilder(clientBuilder: ClientBuilder): ClientBuilder
|
fun setupClientBuilder(clientBuilder: ClientBuilder): ClientBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,8 +37,15 @@ class RustSqliteStoreBuilder(
|
|||||||
cachePath = sessionPaths.cacheDirectory.absolutePath,
|
cachePath = sessionPaths.cacheDirectory.absolutePath,
|
||||||
).journalSizeLimit(25.megaBytes.into(ByteUnit.BYTES).toUInt())
|
).journalSizeLimit(25.megaBytes.into(ByteUnit.BYTES).toUInt())
|
||||||
|
|
||||||
override fun passphrase(passphrase: String?): SqliteStoreBuilder {
|
override fun secret(clientSecret: ClientSecret?): SqliteStoreBuilder {
|
||||||
inner = inner.passphrase(passphrase)
|
when (clientSecret) {
|
||||||
|
null -> Unit
|
||||||
|
is ClientSecret.Passphrase -> inner = inner.passphrase(clientSecret.value)
|
||||||
|
is ClientSecret.RawKey -> {
|
||||||
|
// Ensure the key is 32 bytes long, as required by the SDK
|
||||||
|
inner = inner.key(clientSecret.keyOfSize(32))
|
||||||
|
}
|
||||||
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,3 +53,15 @@ class RustSqliteStoreBuilder(
|
|||||||
return clientBuilder.sqliteStore(this.inner)
|
return clientBuilder.sqliteStore(this.inner)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ClientSecret.RawKey.keyOfSize(size: Int): ByteArray {
|
||||||
|
return if (bytes.size == 32) {
|
||||||
|
bytes
|
||||||
|
} else if (bytes.size < 32) {
|
||||||
|
// If the key is shorter than 32 bytes, pad it with zeros
|
||||||
|
bytes + ByteArray(32 - bytes.size)
|
||||||
|
} else {
|
||||||
|
// Otherwise, take the first 32 bytes of the key
|
||||||
|
bytes.copyOfRange(0, 32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+2
-1
@@ -16,7 +16,8 @@ private val sha256 by lazy { MessageDigest.getInstance("SHA-256") }
|
|||||||
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
private fun anonymizeToken(token: String): String {
|
private fun anonymizeToken(token: String): String {
|
||||||
return sha256.digest(token.toByteArray()).toHexString()
|
// Only keep the first 32 chars (16 bytes) of the hashed token to avoid displaying too much information.
|
||||||
|
return sha256.digest(token.toByteArray()).toHexString().take(32)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun SessionData?.anonymizedTokens(): Pair<String?, String?> {
|
fun SessionData?.anonymizedTokens(): Pair<String?, String?> {
|
||||||
|
|||||||
-18
@@ -1,18 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2024, 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.auth
|
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator
|
|
||||||
import io.element.android.libraries.matrix.test.A_PASSPHRASE
|
|
||||||
|
|
||||||
class FakePassphraseGenerator(
|
|
||||||
private val passphrase: () -> String? = { A_PASSPHRASE }
|
|
||||||
) : PassphraseGenerator {
|
|
||||||
override fun generatePassphrase(): String? = passphrase()
|
|
||||||
}
|
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
* Copyright 2024, 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.impl.auth
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.impl.ClientSecret
|
||||||
|
import io.element.android.libraries.matrix.impl.keys.SecretGenerator
|
||||||
|
import io.element.android.libraries.matrix.test.A_PASSPHRASE
|
||||||
|
|
||||||
|
class FakeSecretGenerator(
|
||||||
|
private val passphrase: (Int) -> String? = { A_PASSPHRASE },
|
||||||
|
private val key: (Int) -> ByteArray = { ByteArray(it) { 0 } },
|
||||||
|
) : SecretGenerator {
|
||||||
|
override fun generatePassphrase(size: Int): ClientSecret.Passphrase? = passphrase(size)?.let { ClientSecret.Passphrase(it) }
|
||||||
|
override fun generateKey(size: Int): ClientSecret.RawKey = ClientSecret.RawKey(key(size))
|
||||||
|
}
|
||||||
+1
-1
@@ -66,7 +66,7 @@ class RustMatrixAuthenticationServiceTest {
|
|||||||
coroutineDispatchers = testCoroutineDispatchers(),
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
sessionStore = sessionStore,
|
sessionStore = sessionStore,
|
||||||
rustMatrixClientFactory = rustMatrixClientFactory,
|
rustMatrixClientFactory = rustMatrixClientFactory,
|
||||||
passphraseGenerator = FakePassphraseGenerator(),
|
secretGenerator = FakeSecretGenerator(),
|
||||||
oAuthConfigurationProvider = OAuthConfigurationProvider(
|
oAuthConfigurationProvider = OAuthConfigurationProvider(
|
||||||
buildMeta = aBuildMeta(),
|
buildMeta = aBuildMeta(),
|
||||||
oAuthRedirectUrlProvider = FakeOAuthRedirectUrlProvider(),
|
oAuthRedirectUrlProvider = FakeOAuthRedirectUrlProvider(),
|
||||||
|
|||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2024, 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.keys
|
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.robolectric.RobolectricTestRunner
|
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
|
||||||
class DefaultPassphraseGeneratorTest {
|
|
||||||
@Test
|
|
||||||
fun `check that generated passphrase has the expected length`() {
|
|
||||||
val passphraseGenerator = DefaultPassphraseGenerator()
|
|
||||||
val passphrase = passphraseGenerator.generatePassphrase()
|
|
||||||
assertThat(passphrase!!.length).isEqualTo(342)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
* Copyright 2024, 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.matrix.impl.keys
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.impl.ClientSecret
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
class DefaultSecretGeneratorTest {
|
||||||
|
@Test
|
||||||
|
fun `check that generated passphrase has the expected length`() {
|
||||||
|
val secretGenerator = DefaultSecretGenerator()
|
||||||
|
val passphrase = secretGenerator.generatePassphrase(256)
|
||||||
|
assertThat(passphrase).isInstanceOf(ClientSecret.Passphrase::class.java)
|
||||||
|
// Size after Base64 encoding should be 4/3 of the original size, without padding
|
||||||
|
assertThat(passphrase!!.value).hasLength(342)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `check that generated key has the expected length`() {
|
||||||
|
val secretGenerator = DefaultSecretGenerator()
|
||||||
|
val key = secretGenerator.generateKey(123)
|
||||||
|
assertThat(key).isInstanceOf(ClientSecret.RawKey::class.java)
|
||||||
|
assertThat(key.bytes).hasLength(123)
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -48,6 +48,7 @@ private fun aSetHttpPusherData(
|
|||||||
deviceDisplayName: String = "deviceDisplayName",
|
deviceDisplayName: String = "deviceDisplayName",
|
||||||
profileTag: String = "profileTag",
|
profileTag: String = "profileTag",
|
||||||
lang: String = "lang",
|
lang: String = "lang",
|
||||||
|
append: Boolean = false,
|
||||||
) = SetHttpPusherData(
|
) = SetHttpPusherData(
|
||||||
pushKey = pushKey,
|
pushKey = pushKey,
|
||||||
appId = appId,
|
appId = appId,
|
||||||
@@ -56,7 +57,8 @@ private fun aSetHttpPusherData(
|
|||||||
appDisplayName = appDisplayName,
|
appDisplayName = appDisplayName,
|
||||||
deviceDisplayName = deviceDisplayName,
|
deviceDisplayName = deviceDisplayName,
|
||||||
profileTag = profileTag,
|
profileTag = profileTag,
|
||||||
lang = lang
|
lang = lang,
|
||||||
|
append = append,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun aUnsetHttpPusherData(
|
private fun aUnsetHttpPusherData(
|
||||||
|
|||||||
+2
-1
@@ -7,10 +7,11 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.storage
|
package io.element.android.libraries.matrix.impl.storage
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.impl.ClientSecret
|
||||||
import org.matrix.rustcomponents.sdk.ClientBuilder
|
import org.matrix.rustcomponents.sdk.ClientBuilder
|
||||||
|
|
||||||
class FakeSqliteStoreBuilder : SqliteStoreBuilder {
|
class FakeSqliteStoreBuilder : SqliteStoreBuilder {
|
||||||
override fun passphrase(passphrase: String?): SqliteStoreBuilder = this
|
override fun secret(clientSecret: ClientSecret?): SqliteStoreBuilder = this
|
||||||
|
|
||||||
override fun setupClientBuilder(clientBuilder: ClientBuilder): ClientBuilder {
|
override fun setupClientBuilder(clientBuilder: ClientBuilder): ClientBuilder {
|
||||||
return clientBuilder
|
return clientBuilder
|
||||||
|
|||||||
+2
-1
@@ -83,7 +83,8 @@ class DefaultPusherSubscriber(
|
|||||||
// TODO getDeviceInfoUseCase.execute().displayName().orEmpty()
|
// TODO getDeviceInfoUseCase.execute().displayName().orEmpty()
|
||||||
deviceDisplayName = "MyDevice",
|
deviceDisplayName = "MyDevice",
|
||||||
url = gateway,
|
url = gateway,
|
||||||
defaultPayload = createDefaultPayload(pushClientSecret.getSecretForUser(userId))
|
defaultPayload = createDefaultPayload(pushClientSecret.getSecretForUser(userId)),
|
||||||
|
append = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
@@ -103,6 +103,7 @@ class DefaultPusherSubscriberTest {
|
|||||||
profileTag = DEFAULT_PUSHER_FILE_TAG + "_",
|
profileTag = DEFAULT_PUSHER_FILE_TAG + "_",
|
||||||
lang = "en",
|
lang = "en",
|
||||||
defaultPayload = "{\"cs\":\"$A_SECRET\"}",
|
defaultPayload = "{\"cs\":\"$A_SECRET\"}",
|
||||||
|
append = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -126,6 +126,14 @@ fun Project.setupKover() {
|
|||||||
"io.element.android.tests.konsist.failures",
|
"io.element.android.tests.konsist.failures",
|
||||||
// Copied from Appyx
|
// Copied from Appyx
|
||||||
"io.element.android.libraries.architecture.appyx.SafeChildrenTransitionScope",
|
"io.element.android.libraries.architecture.appyx.SafeChildrenTransitionScope",
|
||||||
|
// DI-generated classes
|
||||||
|
"io.element.android.x.di.*Impl",
|
||||||
|
"io.element.android.x.di.*Impls",
|
||||||
|
"io.element.android.x.di.*Mirror",
|
||||||
|
"io.element.android.x.di.*Factory",
|
||||||
|
$$"io.element.android.*$Metro*",
|
||||||
|
$$"io.element.android.*$Factory*",
|
||||||
|
$$"io.element.android.*$Impl*",
|
||||||
)
|
)
|
||||||
annotatedBy(
|
annotatedBy(
|
||||||
"androidx.compose.ui.tooling.preview.Preview",
|
"androidx.compose.ui.tooling.preview.Preview",
|
||||||
|
|||||||
+3
-2
@@ -56,8 +56,9 @@ class DefaultAnalyticsColdStartWatcher(
|
|||||||
|
|
||||||
override fun onRoomListVisible() {
|
override fun onRoomListVisible() {
|
||||||
if (isColdStart.getAndSet(false)) {
|
if (isColdStart.getAndSet(false)) {
|
||||||
Timber.d("Room list is visible, finishing cold start check")
|
analyticsService.finishLongRunningTransaction(AnalyticsLongRunningTransaction.ColdStart) {
|
||||||
analyticsService.finishLongRunningTransaction(AnalyticsLongRunningTransaction.ColdStart)
|
Timber.d("Room list is visible, finishing cold start check. Elapsed: ${it.duration}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user