Consider numberless unsolvable reglock to be invalid.

This commit is contained in:
Greyson Parrelli
2026-09-09 16:44:25 -04:00
committed by Cody Henthorne
parent 2f98a16094
commit 782c0a7c86
3 changed files with 11 additions and 52 deletions
@@ -225,19 +225,12 @@ class SignalLoginCredentialEntryViewModel(
stateEmitter(inputState.copy(isLoggingIn = false, areCredentialsIncorrect = true))
}
is RegisterAccountError.RegistrationLock -> {
if (provideRegistrationLock) {
Log.w(TAG, "[Next] Still registration locked after providing the reglock token derived from the recovery key. Falling back to PIN entry.")
stateEmitter(inputState.copy(isLoggingIn = false))
parentEventEmitter.navigateTo(
RegistrationRoute.PinEntryForRegistrationLock(
timeRemaining = error.data.timeRemaining,
svrCredentials = error.data.svr2Credentials
)
)
} else {
Log.w(TAG, "[Next] Registration locked. Retrying with the reglock token derived from the recovery key.")
attemptToLogIn(inputState, aci, aep, totp, provideRegistrationLock = true, parentEventEmitter, stateEmitter)
}
// An account with no phone number can't have a registration lock enabled in the first place, and the only
// PIN that could clear one is behind a phone number we don't have. There is nothing to fall back to.
check(!provideRegistrationLock) { "[Next] Still registration locked after providing the reglock derived from the recovery key. A phone-numberless account cannot be registration locked!" }
Log.w(TAG, "[Next] Registration locked. Retrying with the reglock token derived from the recovery key.")
attemptToLogIn(inputState, aci, aep, totp, provideRegistrationLock = true, parentEventEmitter, stateEmitter)
}
is RegisterAccountError.RateLimited -> {
Log.w(TAG, "[Next] Rate limited (retryAfter: ${error.retryAfter}).")
@@ -1895,36 +1895,6 @@ class RegistrationEndToEndTest {
}
}
@Test
fun `a reglock the entered recovery key cannot derive falls back to asking for the pin`() {
enableSignalLoginRegistration()
val login = SignalLogin(ACI.from(UUID.randomUUID()), AccountEntropyPool.generate())
networkController.onRegisterAccount = {
RequestResult.NonSuccess(
RegisterAccountError.RegistrationLock(
RegistrationLockResponse(
timeRemaining = 14.days.inWholeMilliseconds,
svr2Credentials = SvrCredentials(username = "svr-user", password = "svr-pass")
)
)
)
}
launchRegistrationFlow()
startSignalLoginRegistration()
useExistingSignalLogin()
enterSignalLogin(login)
// The account's reglock isn't the one the recovery key derives, so the PIN behind it is the only way in
waitForTag(TestTags.PIN_ENTRY_SCREEN)
assert(networkController.lastRegisterAccountRequest?.registrationLock == login.aep.deriveMasterKey().deriveRegistrationLock()) {
"Expected the derived reglock to have been tried before falling back but was ${networkController.lastRegisterAccountRequest}"
}
assert(storageController.committedData == null) { "Expected no registration data to be committed while still locked out" }
}
@Test
fun `restoring a remote backup after logging in with a signal login completes registration without a pin`() {
enableSignalLoginRegistration()
@@ -5,6 +5,7 @@
package org.signal.registration.screens.signallogincredentials
import assertk.assertFailure
import assertk.assertThat
import assertk.assertions.containsExactly
import assertk.assertions.hasSize
@@ -411,18 +412,13 @@ class SignalLoginCredentialEntryViewModelTest {
}
@Test
fun `NextClicked still registration locked after providing the reglock token falls back to PIN entry`() = runTest(testDispatcher) {
fun `NextClicked still registration locked after providing the reglock token throws, since there is nothing to fall back to`() = runTest(testDispatcher) {
coEvery { mockRepository.reRegisterAccountWithoutPhoneNumber(any(), any(), any(), any(), any()) } returns
RequestResult.NonSuccess(RegisterAccountError.RegistrationLock(registrationLockResponse()))
applyEvent(completeState(), SignalLoginCredentialEntryScreenEvents.NextClicked)
assertThat(emittedStates.last().isLoggingIn).isEqualTo(false)
assertThat(emittedParentEvents).hasSize(2)
assertThat(emittedParentEvents[1])
.isInstanceOf<RegistrationFlowEvent.NavigateToScreen>()
.prop(RegistrationFlowEvent.NavigateToScreen::route)
.isInstanceOf<RegistrationRoute.PinEntryForRegistrationLock>()
assertFailure {
applyEvent(completeState(), SignalLoginCredentialEntryScreenEvents.NextClicked)
}.isInstanceOf<IllegalStateException>()
}
@Test