From a205d03b43e4f4c9114b69b1d91c649da9874c9d Mon Sep 17 00:00:00 2001 From: Jenna Vassar Date: Tue, 9 Jun 2026 14:42:38 -0700 Subject: [PATCH] Floor the custom recovery passphrase minimum at 1 instead of ignoring the spec When the homeserver advertises custom_recovery_passphrase_settings, always run the custom passphrase flow, flooring min_character_count at 1 (covering missing, zero, and negative values) so the derived passphrase can never be empty. Any stronger minimum stays the operator's choice. Replaces the previous behaviour of dropping the spec and falling back to the auto-generated key. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../libraries/wellknown/impl/Mapper.kt | 26 +++++++++++++------ .../DefaultSessionWellknownRetrieverTest.kt | 24 ++++++++++++----- 2 files changed, 36 insertions(+), 14 deletions(-) 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 61899578d7..2353017be8 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 @@ -15,6 +15,13 @@ import timber.log.Timber private val loggerTag = LoggerTag("Wellknown") +/** + * Floor for the recovery passphrase minimum length. A value of 1 guarantees the derived passphrase is + * never empty (an empty passphrase derives a recoverable but effectively unprotected secret-storage + * key). Anything stronger than non-empty is left to the homeserver operator's configuration. + */ +private const val MINIMUM_PASSPHRASE_CHARACTER_COUNT = 1 + internal fun InternalElementWellKnown.map() = ElementWellKnown( registrationHelperUrl = registrationHelperUrl, enforceElementPro = enforceElementPro, @@ -25,14 +32,17 @@ internal fun InternalElementWellKnown.map() = ElementWellKnown( 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 +private fun InternalCustomRecoveryPassphraseRequirements.toPublic(): CustomRecoveryPassphraseRequirements { + // Whenever the homeserver advertises the settings block we run the custom passphrase flow, flooring + // the minimum at 1 so the passphrase can never be empty even if the server omits min_character_count + // or advertises a non-positive value. The operator owns any stronger minimum. + val min = (minCharacterCount ?: MINIMUM_PASSPHRASE_CHARACTER_COUNT).coerceAtLeast(MINIMUM_PASSPHRASE_CHARACTER_COUNT) + if (min != minCharacterCount) { + Timber.tag(loggerTag.value).w( + "custom_recovery_passphrase_settings.min_character_count was %s; flooring to %d", + minCharacterCount, + min, + ) } return CustomRecoveryPassphraseRequirements(minCharacterCount = min) } 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 68d4d818c2..bdd25aa80b 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 @@ -137,7 +137,7 @@ class DefaultSessionWellknownRetrieverTest { } @Test - fun `get element wellknown with custom recovery passphrase requirements missing min character count maps to null`() = runTest { + fun `get element wellknown with custom recovery passphrase requirements missing min character count floors to 1`() = runTest { val sut = createDefaultSessionWellknownRetriever( getUrlLambda = { Result.success( @@ -148,12 +148,16 @@ class DefaultSessionWellknownRetrieverTest { }, ) assertThat(sut.getElementWellKnown()).isEqualTo( - WellknownRetrieverResult.Success(anElementWellKnown()) + WellknownRetrieverResult.Success( + anElementWellKnown( + customRecoveryPassphraseRequirements = CustomRecoveryPassphraseRequirements(minCharacterCount = 1) + ) + ) ) } @Test - fun `get element wellknown with non-positive min character count maps to null`() = runTest { + fun `get element wellknown with zero min character count floors to 1`() = runTest { val sut = createDefaultSessionWellknownRetriever( getUrlLambda = { Result.success( @@ -166,12 +170,16 @@ class DefaultSessionWellknownRetrieverTest { }, ) assertThat(sut.getElementWellKnown()).isEqualTo( - WellknownRetrieverResult.Success(anElementWellKnown()) + WellknownRetrieverResult.Success( + anElementWellKnown( + customRecoveryPassphraseRequirements = CustomRecoveryPassphraseRequirements(minCharacterCount = 1) + ) + ) ) } @Test - fun `get element wellknown with negative min character count maps to null`() = runTest { + fun `get element wellknown with negative min character count floors to 1`() = runTest { val sut = createDefaultSessionWellknownRetriever( getUrlLambda = { Result.success( @@ -184,7 +192,11 @@ class DefaultSessionWellknownRetrieverTest { }, ) assertThat(sut.getElementWellKnown()).isEqualTo( - WellknownRetrieverResult.Success(anElementWellKnown()) + WellknownRetrieverResult.Success( + anElementWellKnown( + customRecoveryPassphraseRequirements = CustomRecoveryPassphraseRequirements(minCharacterCount = 1) + ) + ) ) }