Parse custom_recovery_passphrase_settings from Element .well-known

Add CustomRecoveryPassphraseRequirements to ElementWellKnown and decode
the custom_recovery_passphrase_settings block (currently just
min_character_count) from the homeserver's element.json. Invalid specs
(missing or non-positive min_character_count) are logged and mapped to
null so the setup flow falls back to the generated-key path. Field names
match the ESS well-known schema (element-enterprise#261).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jenna Vassar
2026-06-02 13:33:18 -07:00
parent 748454f34d
commit 2931418018
7 changed files with 181 additions and 1 deletions
@@ -15,7 +15,15 @@ import kotlinx.serialization.Serializable
* Example:
* <pre>
* {
* "registration_helper_url": "https://element.io"
* "registration_helper_url": "https://element.io",
* "enforce_element_pro": true,
* "rageshake_url": "https://example.org/rageshake",
* "brand_color": "#FF0000",
* "notification_sound": "ring.flac",
* "idp_app_scheme": "io.element.app",
* "custom_recovery_passphrase_settings": {
* "min_character_count": 8
* }
* }
* </pre>
* .
@@ -34,4 +42,12 @@ data class InternalElementWellKnown(
val notificationSound: String? = null,
@SerialName("idp_app_scheme")
val identityProviderAppScheme: String? = null,
@SerialName("custom_recovery_passphrase_settings")
val customRecoveryPassphraseRequirements: InternalCustomRecoveryPassphraseRequirements? = null,
)
@Serializable
data class InternalCustomRecoveryPassphraseRequirements(
@SerialName("min_character_count")
val minCharacterCount: Int? = null,
)
@@ -8,7 +8,12 @@
package io.element.android.libraries.wellknown.impl
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.wellknown.api.CustomRecoveryPassphraseRequirements
import io.element.android.libraries.wellknown.api.ElementWellKnown
import timber.log.Timber
private val loggerTag = LoggerTag("Wellknown")
internal fun InternalElementWellKnown.map() = ElementWellKnown(
registrationHelperUrl = registrationHelperUrl,
@@ -17,4 +22,17 @@ internal fun InternalElementWellKnown.map() = ElementWellKnown(
brandColor = brandColor,
notificationSound = notificationSound,
identityProviderAppScheme = identityProviderAppScheme,
customRecoveryPassphraseRequirements = customRecoveryPassphraseRequirements?.toPublic(),
)
private fun InternalCustomRecoveryPassphraseRequirements.toPublic(): CustomRecoveryPassphraseRequirements? {
val min = minCharacterCount ?: run {
Timber.tag(loggerTag.value).w("custom_recovery_passphrase_settings missing min_character_count; ignoring spec")
return null
}
if (min <= 0) {
Timber.tag(loggerTag.value).w("custom_recovery_passphrase_settings.min_character_count must be > 0; ignoring spec")
return null
}
return CustomRecoveryPassphraseRequirements(minCharacterCount = min)
}
@@ -0,0 +1,33 @@
/*
* 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.wellknown.api
import com.google.common.truth.Truth.assertThat
import org.junit.Test
class CustomRecoveryPassphraseRequirementsTest {
@Test
fun `empty input never satisfies a positive minimum`() {
assertThat(CustomRecoveryPassphraseRequirements(minCharacterCount = 1).isSatisfiedBy("")).isFalse()
}
@Test
fun `input shorter than minimum is not satisfied`() {
assertThat(CustomRecoveryPassphraseRequirements(minCharacterCount = 8).isSatisfiedBy("abc")).isFalse()
}
@Test
fun `input exactly at minimum is satisfied`() {
assertThat(CustomRecoveryPassphraseRequirements(minCharacterCount = 8).isSatisfiedBy("abcdefgh")).isTrue()
}
@Test
fun `input longer than minimum is satisfied`() {
assertThat(CustomRecoveryPassphraseRequirements(minCharacterCount = 4).isSatisfiedBy("abcdefgh")).isTrue()
}
}
@@ -19,6 +19,7 @@ import io.element.android.libraries.cachestore.api.CacheStore
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.sessionstorage.test.InMemoryCacheStore
import io.element.android.libraries.wellknown.api.CustomRecoveryPassphraseRequirements
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
import io.element.android.services.toolbox.api.systemclock.SystemClock
@@ -51,6 +52,7 @@ class DefaultSessionWellknownRetrieverTest {
brandColor = null,
notificationSound = null,
identityProviderAppScheme = null,
customRecoveryPassphraseRequirements = null,
)
)
)
@@ -76,6 +78,7 @@ class DefaultSessionWellknownRetrieverTest {
brandColor = "#FF0000",
notificationSound = "a_notification_sound.flac",
identityProviderAppScheme = "an_app_scheme",
customRecoveryPassphraseRequirements = null,
)
)
)
@@ -105,11 +108,86 @@ class DefaultSessionWellknownRetrieverTest {
brandColor = null,
notificationSound = null,
identityProviderAppScheme = null,
customRecoveryPassphraseRequirements = null,
)
)
)
}
@Test
fun `get element wellknown with custom recovery passphrase requirements`() = runTest {
val sut = createDefaultSessionWellknownRetriever(
getUrlLambda = {
Result.success(
"""{
"custom_recovery_passphrase_settings": {
"min_character_count": 8
}
}""".trimIndent().toByteArray()
)
},
)
assertThat(sut.getElementWellKnown()).isEqualTo(
WellknownRetrieverResult.Success(
anElementWellKnown(
customRecoveryPassphraseRequirements = CustomRecoveryPassphraseRequirements(minCharacterCount = 8)
)
)
)
}
@Test
fun `get element wellknown with custom recovery passphrase requirements missing min character count maps to null`() = runTest {
val sut = createDefaultSessionWellknownRetriever(
getUrlLambda = {
Result.success(
"""{
"custom_recovery_passphrase_settings": {}
}""".trimIndent().toByteArray()
)
},
)
assertThat(sut.getElementWellKnown()).isEqualTo(
WellknownRetrieverResult.Success(anElementWellKnown())
)
}
@Test
fun `get element wellknown with non-positive min character count maps to null`() = runTest {
val sut = createDefaultSessionWellknownRetriever(
getUrlLambda = {
Result.success(
"""{
"custom_recovery_passphrase_settings": {
"min_character_count": 0
}
}""".trimIndent().toByteArray()
)
},
)
assertThat(sut.getElementWellKnown()).isEqualTo(
WellknownRetrieverResult.Success(anElementWellKnown())
)
}
@Test
fun `get element wellknown with negative min character count maps to null`() = runTest {
val sut = createDefaultSessionWellknownRetriever(
getUrlLambda = {
Result.success(
"""{
"custom_recovery_passphrase_settings": {
"min_character_count": -5
}
}""".trimIndent().toByteArray()
)
},
)
assertThat(sut.getElementWellKnown()).isEqualTo(
WellknownRetrieverResult.Success(anElementWellKnown())
)
}
@Test
fun `get element wellknown json error`() = runTest {
val sut = createDefaultSessionWellknownRetriever(
@@ -157,6 +235,7 @@ class DefaultSessionWellknownRetrieverTest {
brandColor = "#FF0000",
notificationSound = "a_notification_sound.flac",
identityProviderAppScheme = "an_app_scheme",
customRecoveryPassphraseRequirements = null,
)
)
)
@@ -210,6 +289,7 @@ class DefaultSessionWellknownRetrieverTest {
brandColor = "#FF0000",
notificationSound = "a_notification_sound.flac",
identityProviderAppScheme = "an_app_scheme",
customRecoveryPassphraseRequirements = null,
)
)
)