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) + ) + ) ) }