diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/CustomRecoveryPassphraseStrength.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/CustomRecoveryPassphraseStrength.kt new file mode 100644 index 0000000000..ea8b02d93f --- /dev/null +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/CustomRecoveryPassphraseStrength.kt @@ -0,0 +1,35 @@ +/* + * 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.features.enterprise.api + +/** + * Strength tier surfaced to the user under a custom recovery passphrase field. + * The eight tiers mirror iOS `PasswordStrengthThreshold` 1:1 so both clients label a given + * passphrase identically; each maps to its own label and time-to-crack description in the UI. + */ +enum class CustomRecoveryPassphraseStrength { + Garbage, + Weak, + Moderate, + Okay, + Strong, + VeryStrong, + UltraStrong, + Mega, +} + +/** + * Result of estimating a custom recovery passphrase's strength. The estimation algorithm itself + * is an enterprise capability ([EnterpriseService.estimateCustomRecoveryPassphraseStrength]); FOSS + * builds never produce one. + */ +data class CustomRecoveryPassphraseStrengthResult( + val strength: CustomRecoveryPassphraseStrength, + /** Fill amount for the progress bar. Always in `0f..1f`. */ + val score: Float, +) diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt index 92d8b9b646..ce56a5dc80 100644 --- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt @@ -41,6 +41,22 @@ interface EnterpriseService { */ fun getNoisyNotificationChannelId(sessionId: SessionId): String? + /** + * Whether the user is allowed to set a custom recovery passphrase. + * Enterprise builds gate this in addition to the homeserver's .well-known `customRecoveryPassphraseRequirements`; + * when false the setup flow falls back to the auto-generated key path even if the homeserver + * advertises a spec. + */ + fun isCustomRecoveryPassphraseEnabled(): Boolean + + /** + * Estimate the strength of a user-typed custom recovery [passphrase]. + * The estimation algorithm is an enterprise capability: FOSS builds return null, so callers + * should treat null as "no indicator to show". Callers are responsible for not calling this with + * an empty passphrase if they want the indicator hidden while the field is empty. + */ + fun estimateCustomRecoveryPassphraseStrength(passphrase: String): CustomRecoveryPassphraseStrengthResult? + companion object { const val ANY_ACCOUNT_PROVIDER = "*" } diff --git a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt index 6e3ed5d3cc..caa2eb291f 100644 --- a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt +++ b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt @@ -13,6 +13,7 @@ import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import io.element.android.compound.colors.SemanticColorsLightDark import io.element.android.features.enterprise.api.BugReportUrl +import io.element.android.features.enterprise.api.CustomRecoveryPassphraseStrengthResult import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.coroutines.flow.Flow @@ -45,4 +46,8 @@ class DefaultEnterpriseService : EnterpriseService { } override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null + + override fun isCustomRecoveryPassphraseEnabled(): Boolean = false + + override fun estimateCustomRecoveryPassphraseStrength(passphrase: String): CustomRecoveryPassphraseStrengthResult? = null } diff --git a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt index 805c75be6a..fb474a96e5 100644 --- a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt +++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt @@ -11,6 +11,7 @@ package io.element.android.features.enterprise.test import androidx.compose.ui.graphics.Color import io.element.android.compound.colors.SemanticColorsLightDark import io.element.android.features.enterprise.api.BugReportUrl +import io.element.android.features.enterprise.api.CustomRecoveryPassphraseStrengthResult import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.tests.testutils.lambda.lambdaError @@ -31,6 +32,8 @@ class FakeEnterpriseService( private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() }, private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() }, private val tweakMasUrlResult: (String, String) -> String = { _, _ -> lambdaError() }, + private val isCustomRecoveryPassphraseEnabledResult: Boolean = false, + private val estimateCustomRecoveryPassphraseStrengthResult: (String) -> CustomRecoveryPassphraseStrengthResult? = { null }, ) : EnterpriseService { private val brandColorState = MutableStateFlow(initialBrandColor) private val semanticColorsState = MutableStateFlow(initialSemanticColors) @@ -79,4 +82,9 @@ class FakeEnterpriseService( override fun getNoisyNotificationChannelId(sessionId: SessionId): String? { return getNoisyNotificationChannelIdResult(sessionId) } + + override fun isCustomRecoveryPassphraseEnabled(): Boolean = isCustomRecoveryPassphraseEnabledResult + + override fun estimateCustomRecoveryPassphraseStrength(passphrase: String): CustomRecoveryPassphraseStrengthResult? = + estimateCustomRecoveryPassphraseStrengthResult(passphrase) }