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) <noreply@anthropic.com>
This commit is contained in:
Jenna Vassar
2026-06-09 14:42:38 -07:00
parent 2a66df7b8a
commit a205d03b43
2 changed files with 36 additions and 14 deletions
@@ -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)
}
@@ -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)
)
)
)
}