Merge branch 'develop' into feature/bma/customMapStyle

This commit is contained in:
Benoit Marty
2026-05-27 12:16:52 +02:00
committed by GitHub
23 changed files with 256 additions and 100 deletions
+3 -3
View File
@@ -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" }
@@ -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,
) )
@@ -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)
}
}
}
}
@@ -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.")
} }
@@ -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)
@@ -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,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)
} }
} }
@@ -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?
}
@@ -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
}
@@ -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() }
@@ -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)
}
}
@@ -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?> {
@@ -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()
}
@@ -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))
}
@@ -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(),
@@ -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)
}
}
@@ -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)
}
}
@@ -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(
@@ -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
@@ -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,
) )
/** /**
@@ -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",
@@ -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}")
}
} }
} }
} }