diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/CustomRecoveryPassphraseRequirements.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/CustomRecoveryPassphraseRequirements.kt new file mode 100644 index 0000000000..dc6241b0a9 --- /dev/null +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/CustomRecoveryPassphraseRequirements.kt @@ -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 +} diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt index 4c1c476c7a..ac050ba94a 100644 --- a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt @@ -15,4 +15,5 @@ data class ElementWellKnown( val brandColor: String?, val notificationSound: String?, val identityProviderAppScheme: String?, + val customRecoveryPassphraseRequirements: CustomRecoveryPassphraseRequirements?, ) diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt index 2e2b5de16f..803e498eda 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt @@ -15,7 +15,15 @@ import kotlinx.serialization.Serializable * Example: *
* {
- * "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
+ * }
* }
*
* .
@@ -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,
)
diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt
index 41ed54d7db..61899578d7 100644
--- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt
+++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt
@@ -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)
+}
diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/api/CustomRecoveryPassphraseRequirementsTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/api/CustomRecoveryPassphraseRequirementsTest.kt
new file mode 100644
index 0000000000..c8f10fe2ca
--- /dev/null
+++ b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/api/CustomRecoveryPassphraseRequirementsTest.kt
@@ -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()
+ }
+}
diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt
index cca40e283f..68d4d818c2 100644
--- a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt
+++ b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt
@@ -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,
)
)
)
diff --git a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt
index ae7d1c629c..1ed69207ba 100644
--- a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt
+++ b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt
@@ -8,6 +8,7 @@
package io.element.android.features.wellknown.test
+import io.element.android.libraries.wellknown.api.CustomRecoveryPassphraseRequirements
import io.element.android.libraries.wellknown.api.ElementWellKnown
fun anElementWellKnown(
@@ -17,6 +18,7 @@ fun anElementWellKnown(
brandColor: String? = null,
notificationSound: String? = null,
identityProviderAppScheme: String? = null,
+ customRecoveryPassphraseRequirements: CustomRecoveryPassphraseRequirements? = null,
) = ElementWellKnown(
registrationHelperUrl = registrationHelperUrl,
enforceElementPro = enforceElementPro,
@@ -24,4 +26,11 @@ fun anElementWellKnown(
brandColor = brandColor,
notificationSound = notificationSound,
identityProviderAppScheme = identityProviderAppScheme,
+ customRecoveryPassphraseRequirements = customRecoveryPassphraseRequirements,
+)
+
+fun aCustomRecoveryPassphraseRequirements(
+ minCharacterCount: Int = 8,
+) = CustomRecoveryPassphraseRequirements(
+ minCharacterCount = minCharacterCount,
)