Add a password strength estimator wrapping the SDK zxcvbn API (#7186)

* Add a password strength estimator wrapping the SDK zxcvbn API

* Add tests for the password strength estimator SDK mapping

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jenna Vassar
2026-07-28 03:30:06 -07:00
committed by GitHub
parent 0cd163a680
commit fe24aeb2ce
4 changed files with 184 additions and 0 deletions
@@ -0,0 +1,43 @@
/*
* 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.matrix.impl.encryption
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.matrix.api.encryption.PasswordStrengthEstimate
import io.element.android.libraries.matrix.api.encryption.PasswordStrengthEstimator
import io.element.android.libraries.matrix.api.encryption.PasswordStrengthRanking
import org.matrix.rustcomponents.sdk.PasswordStrengthEstimate as SdkPasswordStrengthEstimate
import org.matrix.rustcomponents.sdk.PasswordStrengthEstimator as SdkPasswordStrengthEstimator
import org.matrix.rustcomponents.sdk.PasswordStrengthRanking as SdkPasswordStrengthRanking
@ContributesBinding(AppScope::class)
class RustPasswordStrengthEstimator : PasswordStrengthEstimator {
// The SDK ranking is derived from caller-supplied thresholds. We use the "modern hardware (2025)"
// preset so Android and iOS rank passphrases identically. The zxcvbn estimator holds no per-password
// state, so a single app-scoped instance is reused for every keystroke rather than reconstructed.
private val estimator by lazy { SdkPasswordStrengthEstimator.withModernDefaults2025() }
override fun estimate(password: String, userInputs: List<String>): PasswordStrengthEstimate {
return estimator.estimate(password, userInputs).toApiModel()
}
}
internal fun SdkPasswordStrengthEstimate.toApiModel() = PasswordStrengthEstimate(
ranking = ranking.toApiModel(),
score = score,
normalScore = normalScore,
)
internal fun SdkPasswordStrengthRanking.toApiModel() = when (this) {
SdkPasswordStrengthRanking.VERY_WEAK -> PasswordStrengthRanking.VeryWeak
SdkPasswordStrengthRanking.WEAK -> PasswordStrengthRanking.Weak
SdkPasswordStrengthRanking.FAIR -> PasswordStrengthRanking.Fair
SdkPasswordStrengthRanking.STRONG -> PasswordStrengthRanking.Strong
SdkPasswordStrengthRanking.VERY_STRONG -> PasswordStrengthRanking.VeryStrong
}
@@ -0,0 +1,52 @@
/*
* 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.matrix.impl.encryption
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.encryption.PasswordStrengthRanking
import io.element.android.libraries.matrix.test.encryption.FakePasswordStrengthEstimator
import io.element.android.libraries.matrix.test.encryption.aPasswordStrengthEstimate
import org.junit.Test
import org.matrix.rustcomponents.sdk.PasswordStrengthEstimate as SdkPasswordStrengthEstimate
import org.matrix.rustcomponents.sdk.PasswordStrengthRanking as SdkPasswordStrengthRanking
class RustPasswordStrengthEstimatorTest {
@Test
fun `each SDK ranking maps to the matching api ranking`() {
assertThat(SdkPasswordStrengthRanking.VERY_WEAK.toApiModel()).isEqualTo(PasswordStrengthRanking.VeryWeak)
assertThat(SdkPasswordStrengthRanking.WEAK.toApiModel()).isEqualTo(PasswordStrengthRanking.Weak)
assertThat(SdkPasswordStrengthRanking.FAIR.toApiModel()).isEqualTo(PasswordStrengthRanking.Fair)
assertThat(SdkPasswordStrengthRanking.STRONG.toApiModel()).isEqualTo(PasswordStrengthRanking.Strong)
assertThat(SdkPasswordStrengthRanking.VERY_STRONG.toApiModel()).isEqualTo(PasswordStrengthRanking.VeryStrong)
}
@Test
fun `an SDK estimate forwards its ranking, score and normalScore to the api model`() {
val sdkEstimate = SdkPasswordStrengthEstimate(
ranking = SdkPasswordStrengthRanking.FAIR,
guesses = 1_000_000uL,
score = 6.0,
normalScore = 0.35,
feedback = null,
)
assertThat(sdkEstimate.toApiModel()).isEqualTo(
aPasswordStrengthEstimate(
ranking = PasswordStrengthRanking.Fair,
score = 6.0,
normalScore = 0.35,
)
)
}
@Test
fun `the fake estimator returns its configured estimate`() {
val estimate = aPasswordStrengthEstimate(ranking = PasswordStrengthRanking.Strong)
val fake = FakePasswordStrengthEstimator { _, _ -> estimate }
assertThat(fake.estimate("hunter2")).isEqualTo(estimate)
}
}