mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 16:10:00 +00:00
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:
+23
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server-driven requirements for a user-chosen recovery passphrase. Today the only rule
|
||||||
|
* is a minimum character count; additional rules can be added here as the well-known
|
||||||
|
* schema (`custom_recovery_passphrase_settings`) grows.
|
||||||
|
*/
|
||||||
|
data class CustomRecoveryPassphraseRequirements(
|
||||||
|
val minCharacterCount: Int,
|
||||||
|
) {
|
||||||
|
/** True when [input] meets every active rule. */
|
||||||
|
fun isSatisfiedBy(input: String): Boolean = isSatisfiedBy(input.length)
|
||||||
|
|
||||||
|
/** True when an input of [length] characters meets every active rule. */
|
||||||
|
fun isSatisfiedBy(length: Int): Boolean = length >= minCharacterCount
|
||||||
|
}
|
||||||
+1
@@ -15,4 +15,5 @@ data class ElementWellKnown(
|
|||||||
val brandColor: String?,
|
val brandColor: String?,
|
||||||
val notificationSound: String?,
|
val notificationSound: String?,
|
||||||
val identityProviderAppScheme: String?,
|
val identityProviderAppScheme: String?,
|
||||||
|
val customRecoveryPassphraseRequirements: CustomRecoveryPassphraseRequirements?,
|
||||||
)
|
)
|
||||||
|
|||||||
+17
-1
@@ -15,7 +15,15 @@ import kotlinx.serialization.Serializable
|
|||||||
* Example:
|
* Example:
|
||||||
* <pre>
|
* <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>
|
* </pre>
|
||||||
* .
|
* .
|
||||||
@@ -34,4 +42,12 @@ data class InternalElementWellKnown(
|
|||||||
val notificationSound: String? = null,
|
val notificationSound: String? = null,
|
||||||
@SerialName("idp_app_scheme")
|
@SerialName("idp_app_scheme")
|
||||||
val identityProviderAppScheme: String? = null,
|
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,
|
||||||
)
|
)
|
||||||
|
|||||||
+18
@@ -8,7 +8,12 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.wellknown.impl
|
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 io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("Wellknown")
|
||||||
|
|
||||||
internal fun InternalElementWellKnown.map() = ElementWellKnown(
|
internal fun InternalElementWellKnown.map() = ElementWellKnown(
|
||||||
registrationHelperUrl = registrationHelperUrl,
|
registrationHelperUrl = registrationHelperUrl,
|
||||||
@@ -17,4 +22,17 @@ internal fun InternalElementWellKnown.map() = ElementWellKnown(
|
|||||||
brandColor = brandColor,
|
brandColor = brandColor,
|
||||||
notificationSound = notificationSound,
|
notificationSound = notificationSound,
|
||||||
identityProviderAppScheme = identityProviderAppScheme,
|
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)
|
||||||
|
}
|
||||||
|
|||||||
+33
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+80
@@ -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.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.sessionstorage.test.InMemoryCacheStore
|
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.ElementWellKnown
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
@@ -51,6 +52,7 @@ class DefaultSessionWellknownRetrieverTest {
|
|||||||
brandColor = null,
|
brandColor = null,
|
||||||
notificationSound = null,
|
notificationSound = null,
|
||||||
identityProviderAppScheme = null,
|
identityProviderAppScheme = null,
|
||||||
|
customRecoveryPassphraseRequirements = null,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -76,6 +78,7 @@ class DefaultSessionWellknownRetrieverTest {
|
|||||||
brandColor = "#FF0000",
|
brandColor = "#FF0000",
|
||||||
notificationSound = "a_notification_sound.flac",
|
notificationSound = "a_notification_sound.flac",
|
||||||
identityProviderAppScheme = "an_app_scheme",
|
identityProviderAppScheme = "an_app_scheme",
|
||||||
|
customRecoveryPassphraseRequirements = null,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -105,11 +108,86 @@ class DefaultSessionWellknownRetrieverTest {
|
|||||||
brandColor = null,
|
brandColor = null,
|
||||||
notificationSound = null,
|
notificationSound = null,
|
||||||
identityProviderAppScheme = 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
|
@Test
|
||||||
fun `get element wellknown json error`() = runTest {
|
fun `get element wellknown json error`() = runTest {
|
||||||
val sut = createDefaultSessionWellknownRetriever(
|
val sut = createDefaultSessionWellknownRetriever(
|
||||||
@@ -157,6 +235,7 @@ class DefaultSessionWellknownRetrieverTest {
|
|||||||
brandColor = "#FF0000",
|
brandColor = "#FF0000",
|
||||||
notificationSound = "a_notification_sound.flac",
|
notificationSound = "a_notification_sound.flac",
|
||||||
identityProviderAppScheme = "an_app_scheme",
|
identityProviderAppScheme = "an_app_scheme",
|
||||||
|
customRecoveryPassphraseRequirements = null,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -210,6 +289,7 @@ class DefaultSessionWellknownRetrieverTest {
|
|||||||
brandColor = "#FF0000",
|
brandColor = "#FF0000",
|
||||||
notificationSound = "a_notification_sound.flac",
|
notificationSound = "a_notification_sound.flac",
|
||||||
identityProviderAppScheme = "an_app_scheme",
|
identityProviderAppScheme = "an_app_scheme",
|
||||||
|
customRecoveryPassphraseRequirements = null,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
+9
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
package io.element.android.features.wellknown.test
|
package io.element.android.features.wellknown.test
|
||||||
|
|
||||||
|
import io.element.android.libraries.wellknown.api.CustomRecoveryPassphraseRequirements
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
|
|
||||||
fun anElementWellKnown(
|
fun anElementWellKnown(
|
||||||
@@ -17,6 +18,7 @@ fun anElementWellKnown(
|
|||||||
brandColor: String? = null,
|
brandColor: String? = null,
|
||||||
notificationSound: String? = null,
|
notificationSound: String? = null,
|
||||||
identityProviderAppScheme: String? = null,
|
identityProviderAppScheme: String? = null,
|
||||||
|
customRecoveryPassphraseRequirements: CustomRecoveryPassphraseRequirements? = null,
|
||||||
) = ElementWellKnown(
|
) = ElementWellKnown(
|
||||||
registrationHelperUrl = registrationHelperUrl,
|
registrationHelperUrl = registrationHelperUrl,
|
||||||
enforceElementPro = enforceElementPro,
|
enforceElementPro = enforceElementPro,
|
||||||
@@ -24,4 +26,11 @@ fun anElementWellKnown(
|
|||||||
brandColor = brandColor,
|
brandColor = brandColor,
|
||||||
notificationSound = notificationSound,
|
notificationSound = notificationSound,
|
||||||
identityProviderAppScheme = identityProviderAppScheme,
|
identityProviderAppScheme = identityProviderAppScheme,
|
||||||
|
customRecoveryPassphraseRequirements = customRecoveryPassphraseRequirements,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aCustomRecoveryPassphraseRequirements(
|
||||||
|
minCharacterCount: Int = 8,
|
||||||
|
) = CustomRecoveryPassphraseRequirements(
|
||||||
|
minCharacterCount = minCharacterCount,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user