From f1782d06a4dc631a1abac88b658ef54d2d5ceef8 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Wed, 22 Jan 2025 13:25:43 -0500 Subject: [PATCH] Fix multiple bugs and erroneous sad path handling in registration flows. --- .../registration/ActionCountDownButton.kt | 14 +- .../ChangeNumberEnterCodeFragment.kt | 1 - .../ChangeNumberRegistrationLockFragment.kt | 4 +- .../app/changenumber/ChangeNumberViewModel.kt | 20 +-- .../data/RegistrationRepository.kt | 8 +- .../data/network/RegistrationSessionResult.kt | 13 +- .../network/VerificationCodeRequestResult.kt | 73 +++++---- .../registration/ui/RegistrationViewModel.kt | 83 +++++++---- .../ui/entercode/EnterCodeFragment.kt | 102 ++++++++++++- .../phonenumber/EnterPhoneNumberFragment.kt | 42 +++++- .../RegistrationLockFragment.kt | 3 - .../data/RegistrationRepository.kt | 8 +- .../ui/RegistrationViewModel.kt | 91 +++++++---- .../ui/entercode/EnterCodeFragment.kt | 102 ++++++++++++- .../phonenumber/EnterPhoneNumberFragment.kt | 42 +++++- .../RegistrationLockFragment.kt | 3 - .../exceptions/ChallengeRequiredException.kt | 4 +- .../exceptions/RegistrationRetryException.kt | 8 - ...questVerificationCodeRateLimitException.kt | 15 ++ ...ubmitVerificationCodeRateLimitException.kt | 15 ++ .../internal/push/PushServiceSocket.java | 141 ++++++++++-------- .../RegistrationSessionMetadataResponse.kt | 5 +- .../exceptions/PaymentsRegionException.java | 4 +- 23 files changed, 586 insertions(+), 215 deletions(-) delete mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/RegistrationRetryException.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/RequestVerificationCodeRateLimitException.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/SubmitVerificationCodeRateLimitException.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/registration/ActionCountDownButton.kt b/app/src/main/java/org/thoughtcrime/securesms/components/registration/ActionCountDownButton.kt index 703f359948..93b7656989 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/registration/ActionCountDownButton.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/registration/ActionCountDownButton.kt @@ -20,13 +20,22 @@ class ActionCountDownButton @JvmOverloads constructor( private var countDownToTime: Long = 0 private var listener: Listener? = null + private var updateRunnable = Runnable { + updateCountDown() + } + /** * Starts a count down to the specified {@param time}. */ fun startCountDownTo(time: Long) { if (time > 0) { countDownToTime = time + removeCallbacks(updateRunnable) updateCountDown() + } else { + setText(enabledText) + isEnabled = false + alpha = 0.5f } } @@ -38,15 +47,16 @@ class ActionCountDownButton @JvmOverloads constructor( private fun updateCountDown() { val remainingMillis = countDownToTime - System.currentTimeMillis() - if (remainingMillis > 0) { + if (remainingMillis > 1000) { isEnabled = false alpha = 0.5f val totalRemainingSeconds = TimeUnit.MILLISECONDS.toSeconds(remainingMillis).toInt() val minutesRemaining = totalRemainingSeconds / 60 val secondsRemaining = totalRemainingSeconds % 60 + text = resources.getString(disabledText, minutesRemaining, secondsRemaining) listener?.onRemaining(this, totalRemainingSeconds) - postDelayed({ updateCountDown() }, 250) + postDelayed(updateRunnable, 250) } else { setActionEnabled() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt index e23c5ea8a9..5df537abc9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt @@ -165,7 +165,6 @@ class ChangeNumberEnterCodeFragment : LoggingFragment(R.layout.fragment_change_n when (result) { is VerificationCodeRequestResult.Success -> binding.codeEntryLayout.keyboard.displaySuccess() is VerificationCodeRequestResult.RateLimited -> presentRateLimitedDialog() - is VerificationCodeRequestResult.AttemptsExhausted -> presentAccountLocked() is VerificationCodeRequestResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining) else -> presentGenericError(result) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRegistrationLockFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRegistrationLockFragment.kt index 4f0db0cbbd..b956ad3582 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRegistrationLockFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRegistrationLockFragment.kt @@ -155,7 +155,6 @@ class ChangeNumberRegistrationLockFragment : LoggingFragment(R.layout.fragment_c when (requestResult) { is VerificationCodeRequestResult.Success -> Unit is VerificationCodeRequestResult.RateLimited -> onRateLimited() - is VerificationCodeRequestResult.AttemptsExhausted, is VerificationCodeRequestResult.RegistrationLocked -> { navigateToAccountLocked() } @@ -166,7 +165,8 @@ class ChangeNumberRegistrationLockFragment : LoggingFragment(R.layout.fragment_c is VerificationCodeRequestResult.ImpossibleNumber, is VerificationCodeRequestResult.InvalidTransportModeFailure, is VerificationCodeRequestResult.MalformedRequest, - is VerificationCodeRequestResult.MustRetry, + is VerificationCodeRequestResult.RequestVerificationCodeRateLimited, + is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited, is VerificationCodeRequestResult.NoSuchSession, is VerificationCodeRequestResult.NonNormalizedNumber, is VerificationCodeRequestResult.TokenNotAccepted, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt index 6539cf9d8d..c71a1ca695 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt @@ -240,7 +240,7 @@ class ChangeNumberViewModel : ViewModel() { } private suspend fun verifyCodeInternal(context: Context, pin: String?, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) { - val sessionId = getOrCreateValidSession(context)?.body?.id ?: return bail { Log.i(TAG, "Bailing from code verification due to invalid session.") } + val sessionId = getOrCreateValidSession(context)?.metadata?.id ?: return bail { Log.i(TAG, "Bailing from code verification due to invalid session.") } val registrationData = getRegistrationData(context) val verificationResponse = RegistrationRepository.submitVerificationCode(context, sessionId, registrationData) @@ -286,7 +286,7 @@ class ChangeNumberViewModel : ViewModel() { viewModelScope.launch { Log.d(TAG, "Getting session in order to submit captcha token…") val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Bailing captcha token submission due to invalid session.") } - if (!Challenge.parse(session.body.requestedInformation).contains(Challenge.CAPTCHA)) { + if (!Challenge.parse(session.metadata.requestedInformation).contains(Challenge.CAPTCHA)) { Log.d(TAG, "Captcha submission no longer necessary, bailing.") store.update { it.copy( @@ -297,7 +297,7 @@ class ChangeNumberViewModel : ViewModel() { return@launch } Log.d(TAG, "Submitting captcha token…") - val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.body.id, captchaToken) + val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.metadata.id, captchaToken) Log.d(TAG, "Captcha token submitted.") store.update { it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(captchaSubmissionResult)) @@ -316,7 +316,7 @@ class ChangeNumberViewModel : ViewModel() { Log.d(TAG, "Getting session in order to perform push token verification…") val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Bailing from push token verification due to invalid session.") } - if (!Challenge.parse(session.body.requestedInformation).contains(Challenge.PUSH)) { + if (!Challenge.parse(session.metadata.requestedInformation).contains(Challenge.PUSH)) { Log.d(TAG, "Push submission no longer necessary, bailing.") store.update { it.copy( @@ -328,7 +328,7 @@ class ChangeNumberViewModel : ViewModel() { } Log.d(TAG, "Requesting push challenge token…") - val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.body.id, e164, password) + val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.metadata.id, e164, password) Log.d(TAG, "Push challenge token submitted.") store.update { it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(pushSubmissionResult)) @@ -364,7 +364,7 @@ class ChangeNumberViewModel : ViewModel() { private fun updateLocalStateFromSession(response: RegistrationSessionMetadataResponse) { Log.v(TAG, "updateLocalStateFromSession()") store.update { - it.copy(sessionId = response.body.id, challengesRequested = Challenge.parse(response.body.requestedInformation), allowedToRequestCode = response.body.allowedToRequestCode) + it.copy(sessionId = response.metadata.id, challengesRequested = Challenge.parse(response.metadata.requestedInformation), allowedToRequestCode = response.metadata.allowedToRequestCode) } } @@ -477,15 +477,15 @@ class ChangeNumberViewModel : ViewModel() { return } - val result = if (!validSession.body.allowedToRequestCode) { - val challenges = validSession.body.requestedInformation.joinToString() + val result = if (!validSession.metadata.allowedToRequestCode) { + val challenges = validSession.metadata.requestedInformation.joinToString() Log.i(TAG, "Not allowed to request code! Remaining challenges: $challenges") - VerificationCodeRequestResult.ChallengeRequired(Challenge.parse(validSession.body.requestedInformation)) + VerificationCodeRequestResult.ChallengeRequired(Challenge.parse(validSession.metadata.requestedInformation)) } else { store.update { it.copy(changeNumberOutcome = null, challengesRequested = emptyList()) } - val response = RegistrationRepository.requestSmsCode(context = context, sessionId = validSession.body.id, e164 = e164, password = password, mode = mode) + val response = RegistrationRepository.requestSmsCode(context = context, sessionId = validSession.metadata.id, e164 = e164, password = password, mode = mode) Log.d(TAG, "SMS code request submitted") response } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt index 1b95594bf5..54d406c3d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/RegistrationRepository.kt @@ -307,7 +307,7 @@ object RegistrationRepository { val result = RegistrationSessionCreationResult.from(registrationSessionResult) if (result is RegistrationSessionCreationResult.Success) { Log.d(TAG, "Updating registration session and E164 in value store.") - SignalStore.registration.sessionId = result.getMetadata().body.id + SignalStore.registration.sessionId = result.getMetadata().metadata.id SignalStore.registration.sessionE164 = e164 } @@ -472,8 +472,8 @@ object RegistrationRepository { if (receivedPush) { val challenge = subscriber.challenge if (challenge != null) { - Log.w(TAG, "Push challenge token received.") - return@withContext accountManager.submitPushChallengeToken(sessionCreationResponse.result.body.id, challenge) + Log.i(TAG, "Push challenge token received.") + return@withContext accountManager.submitPushChallengeToken(sessionCreationResponse.result.metadata.id, challenge) } else { Log.w(TAG, "Push received but challenge token was null.") } @@ -494,7 +494,7 @@ object RegistrationRepository { return 0L } - val timestamp: Long = headers.timestamp + val timestamp: Long = headers.serverDeliveredTimestamp return timestamp + deltaSeconds.seconds.inWholeMilliseconds } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/RegistrationSessionResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/RegistrationSessionResult.kt index f262cb2bd0..0eabe11c7d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/RegistrationSessionResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/RegistrationSessionResult.kt @@ -6,6 +6,7 @@ package org.thoughtcrime.securesms.registration.data.network import org.signal.core.util.logging.Log +import org.signal.core.util.orNull import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.push.exceptions.MalformedRequestException import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionException @@ -35,7 +36,7 @@ sealed class RegistrationSessionCreationResult(cause: Throwable?) : Registration is NetworkResult.NetworkError -> UnknownError(networkResult.exception) is NetworkResult.StatusCodeError -> { when (val cause = networkResult.exception) { - is RateLimitException -> createRateLimitProcessor(cause) + is RateLimitException -> RateLimited(cause, cause.retryAfterMilliseconds.orNull()) is MalformedRequestException -> MalformedRequest(cause) else -> if (networkResult.code == 422) { ServerUnableToParse(cause) @@ -46,14 +47,6 @@ sealed class RegistrationSessionCreationResult(cause: Throwable?) : Registration } } } - - private fun createRateLimitProcessor(exception: RateLimitException): RegistrationSessionCreationResult { - return if (exception.retryAfterMilliseconds.isPresent) { - RateLimited(exception, exception.retryAfterMilliseconds.get()) - } else { - AttemptsExhausted(exception) - } - } } class Success(private val metadata: RegistrationSessionMetadataResponse) : RegistrationSessionCreationResult(null), SessionMetadataHolder { @@ -62,7 +55,7 @@ sealed class RegistrationSessionCreationResult(cause: Throwable?) : Registration } } - class RateLimited(cause: Throwable, val timeRemaining: Long) : RegistrationSessionCreationResult(cause) + class RateLimited(cause: Throwable, val timeRemaining: Long?) : RegistrationSessionCreationResult(cause) class AttemptsExhausted(cause: Throwable) : RegistrationSessionCreationResult(cause) class ServerUnableToParse(cause: Throwable) : RegistrationSessionCreationResult(cause) class MalformedRequest(cause: Throwable) : RegistrationSessionCreationResult(cause) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt index da5f126095..d5ead2a23c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt @@ -6,6 +6,7 @@ package org.thoughtcrime.securesms.registration.data.network import org.signal.core.util.logging.Log +import org.signal.core.util.orNull import org.thoughtcrime.securesms.registration.data.RegistrationRepository import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException @@ -17,13 +18,15 @@ import org.whispersystems.signalservice.api.push.exceptions.MalformedRequestExce import org.whispersystems.signalservice.api.push.exceptions.NoSuchSessionException import org.whispersystems.signalservice.api.push.exceptions.NonNormalizedPhoneNumberException import org.whispersystems.signalservice.api.push.exceptions.RateLimitException -import org.whispersystems.signalservice.api.push.exceptions.RegistrationRetryException +import org.whispersystems.signalservice.api.push.exceptions.RequestVerificationCodeRateLimitException +import org.whispersystems.signalservice.api.push.exceptions.SubmitVerificationCodeRateLimitException import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException import org.whispersystems.signalservice.api.svr.Svr3Credentials import org.whispersystems.signalservice.internal.push.AuthCredentials import org.whispersystems.signalservice.internal.push.LockedException -import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataJson import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds /** * This is a processor to map a [RegistrationSessionMetadataResponse] to all the known outcomes. @@ -37,19 +40,19 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu fun from(networkResult: NetworkResult): VerificationCodeRequestResult { return when (networkResult) { is NetworkResult.Success -> { - val challenges = Challenge.parse(networkResult.result.body.requestedInformation) + val challenges = Challenge.parse(networkResult.result.metadata.requestedInformation) if (challenges.isNotEmpty()) { Log.d(TAG, "Received \"successful\" response that contains challenges: ${challenges.joinToString { it.key }}") ChallengeRequired(challenges) } else { Success( - sessionId = networkResult.result.body.id, - nextSmsTimestamp = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.body.nextSms), - nextCallTimestamp = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.body.nextCall), - nextVerificationAttempt = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.body.nextVerificationAttempt), - allowedToRequestCode = networkResult.result.body.allowedToRequestCode, - challengesRequested = Challenge.parse(networkResult.result.body.requestedInformation), - verified = networkResult.result.body.verified + sessionId = networkResult.result.metadata.id, + nextSmsTimestamp = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.metadata.nextSms), + nextCallTimestamp = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.metadata.nextCall), + nextVerificationAttempt = RegistrationRepository.deriveTimestamp(networkResult.result.headers, networkResult.result.metadata.nextVerificationAttempt), + allowedToRequestCode = networkResult.result.metadata.allowedToRequestCode, + challengesRequested = Challenge.parse(networkResult.result.metadata.requestedInformation), + verified = networkResult.result.metadata.verified ) } } @@ -59,14 +62,23 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu is NetworkResult.StatusCodeError -> { when (val cause = networkResult.exception) { is ChallengeRequiredException -> createChallengeRequiredProcessor(cause.response) - is RateLimitException -> createRateLimitProcessor(cause) + is RateLimitException -> RateLimited(cause, cause.retryAfterMilliseconds.orNull()) is ImpossiblePhoneNumberException -> ImpossibleNumber(cause) is NonNormalizedPhoneNumberException -> NonNormalizedNumber(cause = cause, originalNumber = cause.originalNumber, normalizedNumber = cause.normalizedNumber) is TokenNotAcceptedException -> TokenNotAccepted(cause) is ExternalServiceFailureException -> ExternalServiceFailure(cause) is InvalidTransportModeException -> InvalidTransportModeFailure(cause) is MalformedRequestException -> MalformedRequest(cause) - is RegistrationRetryException -> MustRetry(cause) + is SubmitVerificationCodeRateLimitException -> { + SubmitVerificationCodeRateLimited(cause) + } + is RequestVerificationCodeRateLimitException -> { + RequestVerificationCodeRateLimited( + cause = cause, + nextSmsTimestamp = RegistrationRepository.deriveTimestamp(cause.sessionMetadata.headers, cause.sessionMetadata.metadata.nextSms), + nextCallTimestamp = RegistrationRepository.deriveTimestamp(cause.sessionMetadata.headers, cause.sessionMetadata.metadata.nextCall) + ) + } is LockedException -> RegistrationLocked(cause = cause, timeRemaining = cause.timeRemaining, svr2Credentials = cause.svr2Credentials, svr3Credentials = cause.svr3Credentials) is NoSuchSessionException -> NoSuchSession(cause) is AlreadyVerifiedException -> AlreadyVerified(cause) @@ -76,16 +88,8 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu } } - private fun createChallengeRequiredProcessor(response: RegistrationSessionMetadataJson): VerificationCodeRequestResult { - return ChallengeRequired(Challenge.parse(response.requestedInformation)) - } - - private fun createRateLimitProcessor(exception: RateLimitException): VerificationCodeRequestResult { - return if (exception.retryAfterMilliseconds.isPresent) { - RateLimited(exception, exception.retryAfterMilliseconds.get()) - } else { - AttemptsExhausted(exception) - } + private fun createChallengeRequiredProcessor(response: RegistrationSessionMetadataResponse): VerificationCodeRequestResult { + return ChallengeRequired(Challenge.parse(response.metadata.requestedInformation)) } } @@ -93,9 +97,7 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu class ChallengeRequired(val challenges: List) : VerificationCodeRequestResult(null) - class RateLimited(cause: Throwable, val timeRemaining: Long) : VerificationCodeRequestResult(cause) - - class AttemptsExhausted(cause: Throwable) : VerificationCodeRequestResult(cause) + class RateLimited(cause: Throwable, val timeRemaining: Long?) : VerificationCodeRequestResult(cause) class ImpossibleNumber(cause: Throwable) : VerificationCodeRequestResult(cause) @@ -109,7 +111,26 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu class MalformedRequest(cause: Throwable) : VerificationCodeRequestResult(cause) - class MustRetry(cause: Throwable) : VerificationCodeRequestResult(cause) + class RequestVerificationCodeRateLimited(cause: Throwable, val nextSmsTimestamp: Long, val nextCallTimestamp: Long) : VerificationCodeRequestResult(cause) { + val willBeAbleToRequestAgain: Boolean = nextSmsTimestamp > 0 || nextCallTimestamp > 0 + fun log(now: Duration = System.currentTimeMillis().milliseconds): String { + val sms = if (nextSmsTimestamp > 0) { + "${(nextSmsTimestamp.milliseconds - now).inWholeSeconds}s" + } else { + "Never" + } + + val call = if (nextCallTimestamp > 0) { + "${(nextCallTimestamp.milliseconds - now).inWholeSeconds}s" + } else { + "Never" + } + + return "Request verification code rate limited! nextSms: $sms nextCall: $call" + } + } + + class SubmitVerificationCodeRateLimited(cause: Throwable) : VerificationCodeRequestResult(cause) class RegistrationLocked(cause: Throwable, val timeRemaining: Long, val svr2Credentials: AuthCredentials, val svr3Credentials: Svr3Credentials) : VerificationCodeRequestResult(cause) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt index 3d6ea7a11d..81855f005a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt @@ -46,17 +46,17 @@ import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionC import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.AlreadyVerified -import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.AttemptsExhausted import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ChallengeRequired import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ExternalServiceFailure import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ImpossibleNumber import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.InvalidTransportModeFailure import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.MalformedRequest -import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.MustRetry import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.NoSuchSession import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.NonNormalizedNumber import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.RateLimited import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.RegistrationLocked +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.RequestVerificationCodeRateLimited +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.SubmitVerificationCodeRateLimited import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.Success import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.TokenNotAccepted import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.UnknownError @@ -271,26 +271,26 @@ class RegistrationViewModel : ViewModel() { val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for confirming the entered E164.") } - if (validSession.body.verified) { + if (validSession.metadata.verified) { Log.i(TAG, "Session is already verified, registering account.") - registerVerifiedSession(context, validSession.body.id) + registerVerifiedSession(context, validSession.metadata.id) return@launch } - if (!validSession.body.allowedToRequestCode) { - if (System.currentTimeMillis() > (validSession.body.nextVerificationAttempt ?: Int.MAX_VALUE)) { + if (!validSession.metadata.allowedToRequestCode) { + if (System.currentTimeMillis() > (validSession.metadata.nextVerificationAttempt ?: Int.MAX_VALUE)) { store.update { it.copy(registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) } } else { - val challenges = validSession.body.requestedInformation + val challenges = validSession.metadata.requestedInformation Log.i(TAG, "Not allowed to request code! Remaining challenges: ${challenges.joinToString()}") - handleSessionStateResult(context, ChallengeRequired(Challenge.parse(validSession.body.requestedInformation))) + handleSessionStateResult(context, ChallengeRequired(Challenge.parse(validSession.metadata.requestedInformation))) } return@launch } - requestSmsCodeInternal(context, validSession.body.id, e164) + requestSmsCodeInternal(context, validSession.metadata.id, e164) } } @@ -299,7 +299,7 @@ class RegistrationViewModel : ViewModel() { viewModelScope.launch { val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for requesting an SMS code.") } - requestSmsCodeInternal(context, validSession.body.id, e164) + requestSmsCodeInternal(context, validSession.metadata.id, e164) } } @@ -317,7 +317,7 @@ class RegistrationViewModel : ViewModel() { Log.d(TAG, "Requesting voice call code…") val codeRequestResponse = RegistrationRepository.requestSmsCode( context = context, - sessionId = validSession.body.id, + sessionId = validSession.metadata.id, e164 = e164, password = password, mode = RegistrationRepository.E164VerificationMode.PHONE_CALL @@ -391,13 +391,13 @@ class RegistrationViewModel : ViewModel() { successListener = { networkResult -> store.update { it.copy( - sessionId = networkResult.body.id, - nextSmsTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.body.nextSms), - nextCallTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.body.nextCall), - nextVerificationAttempt = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.body.nextVerificationAttempt), - allowedToRequestCode = networkResult.body.allowedToRequestCode, - challengesRequested = Challenge.parse(networkResult.body.requestedInformation), - verified = networkResult.body.verified + sessionId = networkResult.metadata.id, + nextSmsTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextSms), + nextCallTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextCall), + nextVerificationAttempt = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextVerificationAttempt), + allowedToRequestCode = networkResult.metadata.allowedToRequestCode, + challengesRequested = Challenge.parse(networkResult.metadata.requestedInformation), + verified = networkResult.metadata.verified ) } }, @@ -424,7 +424,7 @@ class RegistrationViewModel : ViewModel() { viewModelScope.launch { val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a captcha token.") } Log.d(TAG, "Submitting captcha token…") - val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.body.id, captchaToken) + val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.metadata.id, captchaToken) Log.d(TAG, "Captcha token submitted.") handleSessionStateResult(context, captchaSubmissionResult) @@ -442,12 +442,12 @@ class RegistrationViewModel : ViewModel() { Log.d(TAG, "Getting session in order to perform push token verification…") val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a push challenge token.") } - if (!Challenge.parse(session.body.requestedInformation).contains(Challenge.PUSH)) { + if (!Challenge.parse(session.metadata.requestedInformation).contains(Challenge.PUSH)) { return@launch bail { Log.i(TAG, "Push challenge token no longer needed, bailing.") } } Log.d(TAG, "Requesting push challenge token…") - val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.body.id, e164, password) + val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.metadata.id, e164, password) Log.d(TAG, "Push challenge token submitted.") handleSessionStateResult(context, pushSubmissionResult) } @@ -489,8 +489,6 @@ class RegistrationViewModel : ViewModel() { return false } - is AttemptsExhausted -> Log.i(TAG, "Received AttemptsExhausted.", sessionResult.getCause()) - is ImpossibleNumber -> Log.i(TAG, "Received ImpossibleNumber.", sessionResult.getCause()) is NonNormalizedNumber -> Log.i(TAG, "Received NonNormalizedNumber.", sessionResult.getCause()) @@ -503,7 +501,24 @@ class RegistrationViewModel : ViewModel() { is MalformedRequest -> Log.i(TAG, "Received MalformedRequest.", sessionResult.getCause()) - is MustRetry -> Log.i(TAG, "Received MustRetry.", sessionResult.getCause()) + is RequestVerificationCodeRateLimited -> { + Log.i(TAG, "Received RequestVerificationCodeRateLimited.", sessionResult.getCause()) + + if (sessionResult.willBeAbleToRequestAgain) { + store.update { + it.copy( + nextSmsTimestamp = sessionResult.nextSmsTimestamp, + nextCallTimestamp = sessionResult.nextCallTimestamp + ) + } + } else { + Log.w(TAG, "Request verification code rate limit is forever, need to start new session") + SignalStore.registration.sessionId = null + store.update { RegistrationState() } + } + } + + is SubmitVerificationCodeRateLimited -> Log.i(TAG, "Received SubmitVerificationCodeRateLimited.", sessionResult.getCause()) is TokenNotAccepted -> Log.i(TAG, "Received TokenNotAccepted.", sessionResult.getCause()) @@ -733,7 +748,7 @@ class RegistrationViewModel : ViewModel() { var reglock = registrationLocked - val session: RegistrationSessionMetadataJson? = getOrCreateValidSession(context)?.body + val session: RegistrationSessionMetadataJson? = getOrCreateValidSession(context)?.metadata val sessionId: String = session?.id ?: return val registrationData: RegistrationData = getRegistrationData() @@ -811,8 +826,22 @@ class RegistrationViewModel : ViewModel() { private suspend fun registerVerifiedSession(context: Context, sessionId: String) { Log.v(TAG, "registerVerifiedSession()") val registrationData = getRegistrationData() - val registrationResponse: RegisterAccountResult = RegistrationRepository.registerAccount(context = context, sessionId = sessionId, registrationData = registrationData, recoveryPassword = null) - handleRegistrationResult(context, registrationData, registrationResponse, false) + val registrationResult: RegisterAccountResult = RegistrationRepository.registerAccount(context = context, sessionId = sessionId, registrationData = registrationData, recoveryPassword = null) + + val reglockEnabled = if (registrationResult is RegisterAccountResult.RegistrationLocked) { + Log.i(TAG, "Received a registration lock response when trying to register verified session. Retrying with master key.") + store.update { + it.copy( + svr2AuthCredentials = registrationResult.svr2Credentials, + svr3AuthCredentials = registrationResult.svr3Credentials + ) + } + true + } else { + false + } + + handleRegistrationResult(context, registrationData, registrationResult, reglockEnabled) } private suspend fun onSuccessfulRegistration(context: Context, registrationData: RegistrationData, remoteResult: AccountRegistrationResult, reglockEnabled: Boolean) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeFragment.kt index eba1038902..5943d1491c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/entercode/EnterCodeFragment.kt @@ -27,6 +27,9 @@ import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeBinding import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult import org.thoughtcrime.securesms.registration.data.network.RegistrationResult +import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult +import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult +import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.fragments.ContactSupportBottomSheetFragment import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView @@ -37,6 +40,7 @@ import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.visible +import kotlin.time.Duration.Companion.milliseconds /** * The final screen of account registration, where the user enters their verification code. @@ -44,11 +48,11 @@ import org.thoughtcrime.securesms.util.visible class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_code) { companion object { + private val TAG = Log.tag(EnterCodeFragment::class.java) + private const val BOTTOM_SHEET_TAG = "support_bottom_sheet" } - private val TAG = Log.tag(EnterCodeFragment::class.java) - private val sharedViewModel by activityViewModels() private val fragmentViewModel by viewModels() private val bottomSheet = ContactSupportBottomSheetFragment() @@ -116,6 +120,11 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c } sharedViewModel.uiState.observe(viewLifecycleOwner) { + it.sessionCreationError?.let { error -> + handleSessionCreationError(error) + sharedViewModel.sessionCreationErrorShown() + } + it.sessionStateError?.let { error -> handleSessionErrorResponse(error) sharedViewModel.sessionStateErrorShown() @@ -160,18 +169,65 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c } } + private fun handleSessionCreationError(result: RegistrationSessionResult) { + if (!result.isSuccess()) { + Log.i(TAG, "[sessionCreateError] Handling error response of ${result.javaClass.name}", result.getCause()) + } + when (result) { + is RegistrationSessionCheckResult.Success, + is RegistrationSessionCreationResult.Success -> throw IllegalStateException("Session error handler called on successful response!") + is RegistrationSessionCreationResult.AttemptsExhausted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_service)) + is RegistrationSessionCreationResult.MalformedRequest -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service)) + + is RegistrationSessionCreationResult.RateLimited -> { + val timeRemaining = result.timeRemaining?.milliseconds + Log.i(TAG, "Session creation rate limited! Next attempt: $timeRemaining") + if (timeRemaining != null) { + presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, timeRemaining.toString())) + } else { + presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)) + } + } + + is RegistrationSessionCreationResult.ServerUnableToParse -> presentGenericError(result) + is RegistrationSessionCheckResult.SessionNotFound -> presentGenericError(result) + is RegistrationSessionCheckResult.UnknownError, + is RegistrationSessionCreationResult.UnknownError -> presentGenericError(result) + } + } + private fun handleSessionErrorResponse(result: VerificationCodeRequestResult) { + if (!result.isSuccess()) { + Log.i(TAG, "[sessionError] Handling error response of ${result.javaClass.name}", result.getCause()) + } + when (result) { is VerificationCodeRequestResult.Success -> throw IllegalStateException("Session error handler called on successful response!") - is VerificationCodeRequestResult.RateLimited -> presentRateLimitedDialog() - is VerificationCodeRequestResult.AttemptsExhausted -> presentAccountLocked() + is VerificationCodeRequestResult.RateLimited -> { + val timeRemaining = result.timeRemaining?.milliseconds + Log.i(TAG, "Session patch rate limited! Next attempt: $timeRemaining") + if (timeRemaining != null) { + presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, timeRemaining.toString())) + } else { + presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)) + } + } is VerificationCodeRequestResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining) is VerificationCodeRequestResult.ExternalServiceFailure -> presentSmsGenericError(result) + is VerificationCodeRequestResult.RequestVerificationCodeRateLimited -> { + Log.i(TAG, result.log()) + handleRequestVerificationCodeRateLimited(result) + } + is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentSubmitVerificationCodeRateLimited() else -> presentGenericError(result) } } private fun handleRegistrationErrorResponse(result: RegisterAccountResult) { + if (!result.isSuccess()) { + Log.i(TAG, "[registrationError] Handling error response of ${result.javaClass.name}", result.getCause()) + } + when (result) { is RegisterAccountResult.Success -> throw IllegalStateException("Register account error handler called on successful response!") is RegisterAccountResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining) @@ -248,6 +304,14 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c ) } + private fun presentRemoteErrorDialog(message: String, positiveButtonListener: DialogInterface.OnClickListener? = null) { + MaterialAlertDialogBuilder(requireContext()).apply { + setMessage(message) + setPositiveButton(android.R.string.ok, positiveButtonListener) + show() + } + } + private fun presentGenericError(requestResult: RegistrationResult) { binding.keyboard.displayFailure().addListener( object : AssertedSuccessListener() { @@ -263,6 +327,36 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c ) } + private fun handleRequestVerificationCodeRateLimited(result: VerificationCodeRequestResult.RequestVerificationCodeRateLimited) { + if (result.willBeAbleToRequestAgain) { + Log.i(TAG, "Attempted to request new code too soon, timers should be updated") + } else { + Log.w(TAG, "Request for new verification code impossible, need to restart registration") + MaterialAlertDialogBuilder(requireContext()).apply { + setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later) + setPositiveButton(android.R.string.ok) { _, _ -> popBackStack() } + setCancelable(false) + show() + } + } + } + + private fun presentSubmitVerificationCodeRateLimited() { + binding.keyboard.displayFailure().addListener( + object : AssertedSuccessListener() { + override fun onSuccess(result: Boolean?) { + Log.w(TAG, "Submit verification code impossible, need to request a new code and restart registration") + MaterialAlertDialogBuilder(requireContext()).apply { + setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later) + setPositiveButton(android.R.string.ok) { _, _ -> popBackStack() } + setCancelable(false) + show() + } + } + } + ) + } + private fun popBackStack() { sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PUSH_NETWORK_AUDITED) NavHostFragment.findNavController(this).popBackStack() diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt index 7108052cb4..21079fe2f3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/phonenumber/EnterPhoneNumberFragment.kt @@ -329,8 +329,13 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_ is RegistrationSessionCreationResult.MalformedRequest -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service), skipToNextScreen) is RegistrationSessionCreationResult.RateLimited -> { - Log.i(TAG, "Session creation rate limited! Next attempt: ${result.timeRemaining.milliseconds}") - presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, result.timeRemaining.milliseconds.toString())) + val timeRemaining = result.timeRemaining?.milliseconds + Log.i(TAG, "Session creation rate limited! Next attempt: $timeRemaining") + if (timeRemaining != null) { + presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, timeRemaining.toString())) + } else { + presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)) + } } is RegistrationSessionCreationResult.ServerUnableToParse -> presentGenericError(result) @@ -346,7 +351,6 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_ } when (result) { is VerificationCodeRequestResult.Success -> throw IllegalStateException("Session error handler called on successful response!") - is VerificationCodeRequestResult.AttemptsExhausted -> presentRateLimitedDialog() is VerificationCodeRequestResult.ChallengeRequired -> handleChallenges(result.challenges) is VerificationCodeRequestResult.ExternalServiceFailure -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_sms_provider_error)) is VerificationCodeRequestResult.ImpossibleNumber -> { @@ -369,11 +373,20 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_ } is VerificationCodeRequestResult.MalformedRequest -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service), skipToNextScreen) - is VerificationCodeRequestResult.MustRetry -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service), skipToNextScreen) + is VerificationCodeRequestResult.RequestVerificationCodeRateLimited -> { + Log.i(TAG, result.log()) + handleRequestVerificationCodeRateLimited(result) + } + is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentGenericError(result) is VerificationCodeRequestResult.NonNormalizedNumber -> handleNonNormalizedNumberError(result.originalNumber, result.normalizedNumber, fragmentViewModel.mode) is VerificationCodeRequestResult.RateLimited -> { - Log.i(TAG, "Code request rate limited! Next attempt: ${result.timeRemaining.milliseconds}") - presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, result.timeRemaining.milliseconds.toString())) + val timeRemaining = result.timeRemaining?.milliseconds + Log.i(TAG, "Session patch rate limited! Next attempt: $timeRemaining") + if (timeRemaining != null) { + presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, timeRemaining.toString())) + } else { + presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)) + } } is VerificationCodeRequestResult.TokenNotAccepted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_we_need_to_verify_that_youre_human)) { _, _ -> moveToCaptcha() } is VerificationCodeRequestResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining) @@ -426,6 +439,23 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_ } } + private fun handleRequestVerificationCodeRateLimited(result: VerificationCodeRequestResult.RequestVerificationCodeRateLimited) { + if (result.willBeAbleToRequestAgain) { + Log.i(TAG, "New verification code cannot be requested yet but can soon, moving to enter code to show timers") + moveToVerificationEntryScreen() + } else { + Log.w(TAG, "Unable to request new verification code, prompting to start new session") + MaterialAlertDialogBuilder(requireContext()).apply { + setMessage(R.string.RegistrationActivity_unable_to_connect_to_service) + setPositiveButton(R.string.NetworkFailure__retry) { _, _ -> + onRegistrationButtonClicked() + } + setNegativeButton(android.R.string.cancel, null) + show() + } + } + } + private fun handleNonNormalizedNumberError(originalNumber: String, normalizedNumber: String, mode: RegistrationRepository.E164VerificationMode) { try { val phoneNumber = PhoneNumberUtil.getInstance().parse(normalizedNumber, null) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/registrationlock/RegistrationLockFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/registrationlock/RegistrationLockFragment.kt index 3e9014ec3c..d647c43710 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/registrationlock/RegistrationLockFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/registrationlock/RegistrationLockFragment.kt @@ -148,9 +148,6 @@ class RegistrationLockFragment : LoggingFragment(R.layout.fragment_registration_ when (requestResult) { is VerificationCodeRequestResult.Success -> throw IllegalStateException("Session error handler called on successful response!") is VerificationCodeRequestResult.RateLimited -> onRateLimited() - is VerificationCodeRequestResult.AttemptsExhausted -> { - findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked()) - } is VerificationCodeRequestResult.RegistrationLocked -> { Log.i(TAG, "Registration locked response to verify account!") diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/RegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/RegistrationRepository.kt index c572883b2a..b3ad999f13 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/RegistrationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/data/RegistrationRepository.kt @@ -302,7 +302,7 @@ object RegistrationRepository { val result = RegistrationSessionCreationResult.from(registrationSessionResult) if (result is RegistrationSessionCreationResult.Success) { Log.d(TAG, "Updating registration session and E164 in value store.") - SignalStore.registration.sessionId = result.getMetadata().body.id + SignalStore.registration.sessionId = result.getMetadata().metadata.id SignalStore.registration.sessionE164 = e164 } @@ -467,8 +467,8 @@ object RegistrationRepository { if (receivedPush) { val challenge = subscriber.challenge if (challenge != null) { - Log.w(TAG, "Push challenge token received.") - return@withContext accountManager.submitPushChallengeToken(sessionCreationResponse.result.body.id, challenge) + Log.i(TAG, "Push challenge token received.") + return@withContext accountManager.submitPushChallengeToken(sessionCreationResponse.result.metadata.id, challenge) } else { Log.w(TAG, "Push received but challenge token was null.") } @@ -489,7 +489,7 @@ object RegistrationRepository { return 0L } - val timestamp: Long = headers.timestamp + val timestamp: Long = headers.serverDeliveredTimestamp return timestamp + deltaSeconds.seconds.inWholeMilliseconds } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt index 7a817173e4..4cc9711b04 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/RegistrationViewModel.kt @@ -48,17 +48,17 @@ import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionC import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.AlreadyVerified -import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.AttemptsExhausted import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ChallengeRequired import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ExternalServiceFailure import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.ImpossibleNumber import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.InvalidTransportModeFailure import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.MalformedRequest -import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.MustRetry import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.NoSuchSession import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.NonNormalizedNumber import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.RateLimited import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.RegistrationLocked +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.RequestVerificationCodeRateLimited +import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.SubmitVerificationCodeRateLimited import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.Success import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.TokenNotAccepted import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult.UnknownError @@ -277,26 +277,26 @@ class RegistrationViewModel : ViewModel() { val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for confirming the entered E164.") } - if (validSession.body.verified) { + if (validSession.metadata.verified) { Log.i(TAG, "Session is already verified, registering account.") - registerVerifiedSession(context, validSession.body.id) + registerVerifiedSession(context, validSession.metadata.id) return@launch } - if (!validSession.body.allowedToRequestCode) { - if (System.currentTimeMillis() > (validSession.body.nextVerificationAttempt ?: Int.MAX_VALUE)) { + if (!validSession.metadata.allowedToRequestCode) { + if (System.currentTimeMillis() > (validSession.metadata.nextVerificationAttempt ?: Int.MAX_VALUE)) { store.update { it.copy(registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) } } else { - val challenges = validSession.body.requestedInformation + val challenges = validSession.metadata.requestedInformation Log.i(TAG, "Not allowed to request code! Remaining challenges: ${challenges.joinToString()}") - handleSessionStateResult(context, ChallengeRequired(Challenge.parse(validSession.body.requestedInformation))) + handleSessionStateResult(context, ChallengeRequired(Challenge.parse(validSession.metadata.requestedInformation))) } return@launch } - requestSmsCodeInternal(context, validSession.body.id, e164) + requestSmsCodeInternal(context, validSession.metadata.id, e164) } } @@ -305,7 +305,7 @@ class RegistrationViewModel : ViewModel() { viewModelScope.launch { val validSession = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for requesting an SMS code.") } - requestSmsCodeInternal(context, validSession.body.id, e164) + requestSmsCodeInternal(context, validSession.metadata.id, e164) } } @@ -323,7 +323,7 @@ class RegistrationViewModel : ViewModel() { Log.d(TAG, "Requesting voice call code…") val codeRequestResponse = RegistrationRepository.requestSmsCode( context = context, - sessionId = validSession.body.id, + sessionId = validSession.metadata.id, e164 = e164, password = password, mode = RegistrationRepository.E164VerificationMode.PHONE_CALL @@ -397,13 +397,13 @@ class RegistrationViewModel : ViewModel() { successListener = { networkResult -> store.update { it.copy( - sessionId = networkResult.body.id, - nextSmsTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.body.nextSms), - nextCallTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.body.nextCall), - nextVerificationAttempt = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.body.nextVerificationAttempt), - allowedToRequestCode = networkResult.body.allowedToRequestCode, - challengesRequested = Challenge.parse(networkResult.body.requestedInformation), - verified = networkResult.body.verified + sessionId = networkResult.metadata.id, + nextSmsTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextSms), + nextCallTimestamp = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextCall), + nextVerificationAttempt = RegistrationRepository.deriveTimestamp(networkResult.headers, networkResult.metadata.nextVerificationAttempt), + allowedToRequestCode = networkResult.metadata.allowedToRequestCode, + challengesRequested = Challenge.parse(networkResult.metadata.requestedInformation), + verified = networkResult.metadata.verified ) } }, @@ -430,7 +430,7 @@ class RegistrationViewModel : ViewModel() { viewModelScope.launch { val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a captcha token.") } Log.d(TAG, "Submitting captcha token…") - val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.body.id, captchaToken) + val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, session.metadata.id, captchaToken) Log.d(TAG, "Captcha token submitted.") handleSessionStateResult(context, captchaSubmissionResult) @@ -448,12 +448,12 @@ class RegistrationViewModel : ViewModel() { Log.d(TAG, "Getting session in order to perform push token verification…") val session = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Could not create valid session for submitting a push challenge token.") } - if (!Challenge.parse(session.body.requestedInformation).contains(Challenge.PUSH)) { + if (!Challenge.parse(session.metadata.requestedInformation).contains(Challenge.PUSH)) { return@launch bail { Log.i(TAG, "Push challenge token no longer needed, bailing.") } } Log.d(TAG, "Requesting push challenge token…") - val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.body.id, e164, password) + val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, session.metadata.id, e164, password) Log.d(TAG, "Push challenge token submitted.") handleSessionStateResult(context, pushSubmissionResult) } @@ -495,8 +495,6 @@ class RegistrationViewModel : ViewModel() { return false } - is AttemptsExhausted -> Log.i(TAG, "Received AttemptsExhausted.", sessionResult.getCause()) - is ImpossibleNumber -> Log.i(TAG, "Received ImpossibleNumber.", sessionResult.getCause()) is NonNormalizedNumber -> Log.i(TAG, "Received NonNormalizedNumber.", sessionResult.getCause()) @@ -509,7 +507,24 @@ class RegistrationViewModel : ViewModel() { is MalformedRequest -> Log.i(TAG, "Received MalformedRequest.", sessionResult.getCause()) - is MustRetry -> Log.i(TAG, "Received MustRetry.", sessionResult.getCause()) + is RequestVerificationCodeRateLimited -> { + Log.i(TAG, "Received RequestVerificationCodeRateLimited.", sessionResult.getCause()) + + if (sessionResult.willBeAbleToRequestAgain) { + store.update { + it.copy( + nextSmsTimestamp = sessionResult.nextSmsTimestamp, + nextCallTimestamp = sessionResult.nextCallTimestamp + ) + } + } else { + Log.w(TAG, "Request verification code rate limit is forever, need to start new session") + SignalStore.registration.sessionId = null + store.update { RegistrationState() } + } + } + + is SubmitVerificationCodeRateLimited -> Log.i(TAG, "Received SubmitVerificationCodeRateLimited.", sessionResult.getCause()) is TokenNotAccepted -> Log.i(TAG, "Received TokenNotAccepted.", sessionResult.getCause()) @@ -527,8 +542,8 @@ class RegistrationViewModel : ViewModel() { store.update { it.copy( - sessionStateError = sessionResult, - inProgress = false + inProgress = false, + sessionStateError = sessionResult ) } return false @@ -577,8 +592,8 @@ class RegistrationViewModel : ViewModel() { } store.update { it.copy( - registerAccountError = registrationResult, - inProgress = stayInProgress + inProgress = stayInProgress, + registerAccountError = registrationResult ) } return false @@ -748,7 +763,7 @@ class RegistrationViewModel : ViewModel() { var reglock = registrationLocked - val sessionId = getOrCreateValidSession(context)?.body?.id ?: return + val sessionId = getOrCreateValidSession(context)?.metadata?.id ?: return val registrationData = getRegistrationData() Log.d(TAG, "Submitting verification code…") @@ -818,8 +833,22 @@ class RegistrationViewModel : ViewModel() { private suspend fun registerVerifiedSession(context: Context, sessionId: String) { Log.v(TAG, "registerVerifiedSession()") val registrationData = getRegistrationData() - val registrationResponse: RegisterAccountResult = RegistrationRepository.registerAccount(context, sessionId, registrationData) - handleRegistrationResult(context, registrationData, registrationResponse, false) + val registrationResult: RegisterAccountResult = RegistrationRepository.registerAccount(context = context, sessionId = sessionId, registrationData = registrationData) + + val reglockEnabled = if (registrationResult is RegisterAccountResult.RegistrationLocked) { + Log.i(TAG, "Received a registration lock response when trying to register verified session. Retrying with master key.") + store.update { + it.copy( + svr2AuthCredentials = registrationResult.svr2Credentials, + svr3AuthCredentials = registrationResult.svr3Credentials + ) + } + true + } else { + false + } + + handleRegistrationResult(context, registrationData, registrationResult, reglockEnabled) } private suspend fun onSuccessfulRegistration(context: Context, registrationData: RegistrationData, remoteResult: AccountRegistrationResult, reglockEnabled: Boolean) = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/entercode/EnterCodeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/entercode/EnterCodeFragment.kt index 4615fbbe1e..4585b185d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/entercode/EnterCodeFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/entercode/EnterCodeFragment.kt @@ -27,6 +27,9 @@ import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeBinding import org.thoughtcrime.securesms.registration.data.network.RegisterAccountResult import org.thoughtcrime.securesms.registration.data.network.RegistrationResult +import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCheckResult +import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult +import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionResult import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.fragments.ContactSupportBottomSheetFragment import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView @@ -37,6 +40,7 @@ import org.thoughtcrime.securesms.registrationv3.ui.RegistrationViewModel import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.visible +import kotlin.time.Duration.Companion.milliseconds /** * The final screen of account registration, where the user enters their verification code. @@ -44,11 +48,11 @@ import org.thoughtcrime.securesms.util.visible class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_code) { companion object { + private val TAG = Log.tag(EnterCodeFragment::class.java) + private const val BOTTOM_SHEET_TAG = "support_bottom_sheet" } - private val TAG = Log.tag(EnterCodeFragment::class.java) - private val sharedViewModel by activityViewModels() private val fragmentViewModel by viewModels() private val bottomSheet = ContactSupportBottomSheetFragment() @@ -116,6 +120,11 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c } sharedViewModel.uiState.observe(viewLifecycleOwner) { + it.sessionCreationError?.let { error -> + handleSessionCreationError(error) + sharedViewModel.sessionCreationErrorShown() + } + it.sessionStateError?.let { error -> handleSessionErrorResponse(error) sharedViewModel.sessionStateErrorShown() @@ -160,18 +169,65 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c } } + private fun handleSessionCreationError(result: RegistrationSessionResult) { + if (!result.isSuccess()) { + Log.i(TAG, "[sessionCreateError] Handling error response of ${result.javaClass.name}", result.getCause()) + } + when (result) { + is RegistrationSessionCheckResult.Success, + is RegistrationSessionCreationResult.Success -> throw IllegalStateException("Session error handler called on successful response!") + is RegistrationSessionCreationResult.AttemptsExhausted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_service)) + is RegistrationSessionCreationResult.MalformedRequest -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service)) + + is RegistrationSessionCreationResult.RateLimited -> { + val timeRemaining = result.timeRemaining?.milliseconds + Log.i(TAG, "Session creation rate limited! Next attempt: $timeRemaining") + if (timeRemaining != null) { + presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, timeRemaining.toString())) + } else { + presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)) + } + } + + is RegistrationSessionCreationResult.ServerUnableToParse -> presentGenericError(result) + is RegistrationSessionCheckResult.SessionNotFound -> presentGenericError(result) + is RegistrationSessionCheckResult.UnknownError, + is RegistrationSessionCreationResult.UnknownError -> presentGenericError(result) + } + } + private fun handleSessionErrorResponse(result: VerificationCodeRequestResult) { + if (!result.isSuccess()) { + Log.i(TAG, "[sessionError] Handling error response of ${result.javaClass.name}", result.getCause()) + } + when (result) { is VerificationCodeRequestResult.Success -> throw IllegalStateException("Session error handler called on successful response!") - is VerificationCodeRequestResult.RateLimited -> presentRateLimitedDialog() - is VerificationCodeRequestResult.AttemptsExhausted -> presentAccountLocked() + is VerificationCodeRequestResult.RateLimited -> { + val timeRemaining = result.timeRemaining?.milliseconds + Log.i(TAG, "Session patch rate limited! Next attempt: $timeRemaining") + if (timeRemaining != null) { + presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, timeRemaining.toString())) + } else { + presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)) + } + } is VerificationCodeRequestResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining) is VerificationCodeRequestResult.ExternalServiceFailure -> presentSmsGenericError(result) + is VerificationCodeRequestResult.RequestVerificationCodeRateLimited -> { + Log.i(TAG, result.log()) + handleRequestVerificationCodeRateLimited(result) + } + is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentSubmitVerificationCodeRateLimited() else -> presentGenericError(result) } } private fun handleRegistrationErrorResponse(result: RegisterAccountResult) { + if (!result.isSuccess()) { + Log.i(TAG, "[registrationError] Handling error response of ${result.javaClass.name}", result.getCause()) + } + when (result) { is RegisterAccountResult.Success -> throw IllegalStateException("Register account error handler called on successful response!") is RegisterAccountResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining) @@ -248,6 +304,14 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c ) } + private fun presentRemoteErrorDialog(message: String, positiveButtonListener: DialogInterface.OnClickListener? = null) { + MaterialAlertDialogBuilder(requireContext()).apply { + setMessage(message) + setPositiveButton(android.R.string.ok, positiveButtonListener) + show() + } + } + private fun presentGenericError(requestResult: RegistrationResult) { binding.keyboard.displayFailure().addListener( object : AssertedSuccessListener() { @@ -263,6 +327,36 @@ class EnterCodeFragment : LoggingFragment(R.layout.fragment_registration_enter_c ) } + private fun handleRequestVerificationCodeRateLimited(result: VerificationCodeRequestResult.RequestVerificationCodeRateLimited) { + if (result.willBeAbleToRequestAgain) { + Log.i(TAG, "Attempted to request new code too soon, timers should be updated") + } else { + Log.w(TAG, "Request for new verification code impossible, need to restart registration") + MaterialAlertDialogBuilder(requireContext()).apply { + setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later) + setPositiveButton(android.R.string.ok) { _, _ -> popBackStack() } + setCancelable(false) + show() + } + } + } + + private fun presentSubmitVerificationCodeRateLimited() { + binding.keyboard.displayFailure().addListener( + object : AssertedSuccessListener() { + override fun onSuccess(result: Boolean?) { + Log.w(TAG, "Submit verification code impossible, need to request a new code and restart registration") + MaterialAlertDialogBuilder(requireContext()).apply { + setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later) + setPositiveButton(android.R.string.ok) { _, _ -> popBackStack() } + setCancelable(false) + show() + } + } + } + ) + } + private fun popBackStack() { sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PUSH_NETWORK_AUDITED) NavHostFragment.findNavController(this).popBackStack() diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberFragment.kt index a34a0ddfa1..e4a03ca13d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/phonenumber/EnterPhoneNumberFragment.kt @@ -340,8 +340,13 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_ is RegistrationSessionCreationResult.MalformedRequest -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service), skipToNextScreen) is RegistrationSessionCreationResult.RateLimited -> { - Log.i(TAG, "Session creation rate limited! Next attempt: ${result.timeRemaining.milliseconds}") - presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, result.timeRemaining.milliseconds.toString())) + val timeRemaining = result.timeRemaining?.milliseconds + Log.i(TAG, "Session creation rate limited! Next attempt: $timeRemaining") + if (timeRemaining != null) { + presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, timeRemaining.toString())) + } else { + presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)) + } } is RegistrationSessionCreationResult.ServerUnableToParse -> presentGenericError(result) @@ -357,7 +362,6 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_ } when (result) { is VerificationCodeRequestResult.Success -> throw IllegalStateException("Session error handler called on successful response!") - is VerificationCodeRequestResult.AttemptsExhausted -> presentRateLimitedDialog() is VerificationCodeRequestResult.ChallengeRequired -> handleChallenges(result.challenges) is VerificationCodeRequestResult.ExternalServiceFailure -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_sms_provider_error)) is VerificationCodeRequestResult.ImpossibleNumber -> { @@ -380,11 +384,20 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_ } is VerificationCodeRequestResult.MalformedRequest -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service), skipToNextScreen) - is VerificationCodeRequestResult.MustRetry -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_unable_to_connect_to_service), skipToNextScreen) + is VerificationCodeRequestResult.RequestVerificationCodeRateLimited -> { + Log.i(TAG, result.log()) + handleRequestVerificationCodeRateLimited(result) + } + is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentGenericError(result) is VerificationCodeRequestResult.NonNormalizedNumber -> handleNonNormalizedNumberError(result.originalNumber, result.normalizedNumber, fragmentViewModel.e164VerificationMode) is VerificationCodeRequestResult.RateLimited -> { - Log.i(TAG, "Code request rate limited! Next attempt: ${result.timeRemaining.milliseconds}") - presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, result.timeRemaining.milliseconds.toString())) + val timeRemaining = result.timeRemaining?.milliseconds + Log.i(TAG, "Session patch rate limited! Next attempt: $timeRemaining") + if (timeRemaining != null) { + presentRemoteErrorDialog(getString(R.string.RegistrationActivity_rate_limited_to_try_again, timeRemaining.toString())) + } else { + presentRemoteErrorDialog(getString(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)) + } } is VerificationCodeRequestResult.TokenNotAccepted -> presentRemoteErrorDialog(getString(R.string.RegistrationActivity_we_need_to_verify_that_youre_human)) { _, _ -> moveToCaptcha() } @@ -438,6 +451,23 @@ class EnterPhoneNumberFragment : LoggingFragment(R.layout.fragment_registration_ } } + private fun handleRequestVerificationCodeRateLimited(result: VerificationCodeRequestResult.RequestVerificationCodeRateLimited) { + if (result.willBeAbleToRequestAgain) { + Log.i(TAG, "New verification code cannot be requested yet but can soon, moving to enter code to show timers") + moveToVerificationEntryScreen() + } else { + Log.w(TAG, "Unable to request new verification code, prompting to start new session") + MaterialAlertDialogBuilder(requireContext()).apply { + setMessage(R.string.RegistrationActivity_unable_to_connect_to_service) + setPositiveButton(R.string.NetworkFailure__retry) { _, _ -> + onRegistrationButtonClicked() + } + setNegativeButton(android.R.string.cancel, null) + show() + } + } + } + private fun handleNonNormalizedNumberError(originalNumber: String, normalizedNumber: String, mode: RegistrationRepository.E164VerificationMode) { try { val phoneNumber = PhoneNumberUtil.getInstance().parse(normalizedNumber, null) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/registrationlock/RegistrationLockFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/registrationlock/RegistrationLockFragment.kt index 9abccff461..b089af19bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/registrationlock/RegistrationLockFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registrationv3/ui/registrationlock/RegistrationLockFragment.kt @@ -148,9 +148,6 @@ class RegistrationLockFragment : LoggingFragment(R.layout.fragment_registration_ when (requestResult) { is VerificationCodeRequestResult.Success -> throw IllegalStateException("Session error handler called on successful response!") is VerificationCodeRequestResult.RateLimited -> onRateLimited() - is VerificationCodeRequestResult.AttemptsExhausted -> { - findNavController().safeNavigate(RegistrationLockFragmentDirections.actionAccountLocked()) - } is VerificationCodeRequestResult.RegistrationLocked -> { Log.i(TAG, "Registration locked response to verify account!") diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/ChallengeRequiredException.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/ChallengeRequiredException.kt index 93b74f8727..4c4cdfd3e6 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/ChallengeRequiredException.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/ChallengeRequiredException.kt @@ -5,10 +5,10 @@ package org.whispersystems.signalservice.api.push.exceptions -import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataJson +import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse /** * We tried to do something on registration endpoints that didn't go well, so now we have to do a challenge. And not a * fun one involving ice buckets. */ -class ChallengeRequiredException(val response: RegistrationSessionMetadataJson) : NonSuccessfulResponseCodeException(409) +class ChallengeRequiredException(val response: RegistrationSessionMetadataResponse) : NonSuccessfulResponseCodeException(409) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/RegistrationRetryException.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/RegistrationRetryException.kt deleted file mode 100644 index 5b2e2b91ea..0000000000 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/RegistrationRetryException.kt +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright 2023 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.signalservice.api.push.exceptions - -class RegistrationRetryException : NonSuccessfulResponseCodeException(429) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/RequestVerificationCodeRateLimitException.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/RequestVerificationCodeRateLimitException.kt new file mode 100644 index 0000000000..8a39a5555a --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/RequestVerificationCodeRateLimitException.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.push.exceptions + +import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse + +/** + * Rate limit exception specific to requesting a verification code for registration. + */ +class RequestVerificationCodeRateLimitException( + val sessionMetadata: RegistrationSessionMetadataResponse +) : NonSuccessfulResponseCodeException(429, "User request verification code rate limited") diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/SubmitVerificationCodeRateLimitException.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/SubmitVerificationCodeRateLimitException.kt new file mode 100644 index 0000000000..c4a2e92498 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/SubmitVerificationCodeRateLimitException.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.push.exceptions + +import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse + +/** + * Rate limit exception specific to submitting verification codes during registration. + */ +class SubmitVerificationCodeRateLimitException( + val sessionMetadata: RegistrationSessionMetadataResponse +) : NonSuccessfulResponseCodeException(429, "User submit verification code rate limited") diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 55080238af..151b48c3da 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -106,9 +106,10 @@ import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredExcepti import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.push.exceptions.RangeException; import org.whispersystems.signalservice.api.push.exceptions.RateLimitException; -import org.whispersystems.signalservice.api.push.exceptions.RegistrationRetryException; +import org.whispersystems.signalservice.api.push.exceptions.RequestVerificationCodeRateLimitException; import org.whispersystems.signalservice.api.push.exceptions.ResumeLocationInvalidException; import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException; +import org.whispersystems.signalservice.api.push.exceptions.SubmitVerificationCodeRateLimitException; import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotAssociatedWithAnAccountException; @@ -437,7 +438,7 @@ public class PushServiceSocket { body.put("client", androidSmsRetriever ? "android-2021-03" : "android"); - try (Response response = makeServiceRequest(path, "POST", jsonRequestBody(JsonUtil.toJson(body)), headers, new RegistrationCodeRequestResponseHandler(), SealedSenderAccess.NONE, false)) { + try (Response response = makeServiceRequest(path, "POST", jsonRequestBody(JsonUtil.toJson(body)), headers, new RequestVerificationCodeResponseHandler(), SealedSenderAccess.NONE, false)) { return parseSessionMetadataResponse(response); } } @@ -446,7 +447,7 @@ public class PushServiceSocket { String path = String.format(VERIFICATION_CODE_PATH, sessionId); Map body = new HashMap<>(); body.put("code", verificationCode); - try (Response response = makeServiceRequest(path, "PUT", jsonRequestBody(JsonUtil.toJson(body)), NO_HEADERS, new RegistrationCodeSubmissionResponseHandler(), SealedSenderAccess.NONE, false)) { + try (Response response = makeServiceRequest(path, "PUT", jsonRequestBody(JsonUtil.toJson(body)), NO_HEADERS, new SubmitVerificationCodeResponseHandler(), SealedSenderAccess.NONE, false)) { return parseSessionMetadataResponse(response); } } @@ -1040,7 +1041,7 @@ public class PushServiceSocket { public void checkRepeatedUsePreKeys(ServiceIdType serviceIdType, byte[] digest) throws IOException { String body = JsonUtil.toJson(new CheckRepeatedUsedPreKeysRequest(serviceIdType.toString(), digest)); - makeServiceRequest(PREKEY_CHECK_PATH, "POST", body, NO_HEADERS, (responseCode, body1) -> { + makeServiceRequest(PREKEY_CHECK_PATH, "POST", body, NO_HEADERS, (responseCode, errorBody, getHeader) -> { // Must override this handling because otherwise code assumes a device mismatch error if (responseCode == 409) { throw new NonSuccessfulResponseCodeException(409); @@ -1278,7 +1279,7 @@ public class PushServiceSocket { "GET", null, NO_HEADERS, - (responseCode, body) -> { + (responseCode, body, getHeader) -> { if (responseCode == 404) { throw new UsernameIsNotAssociatedWithAnAccountException(); } @@ -1301,7 +1302,7 @@ public class PushServiceSocket { public @NonNull ReserveUsernameResponse reserveUsername(@NonNull List usernameHashes) throws IOException { ReserveUsernameRequest reserveUsernameRequest = new ReserveUsernameRequest(usernameHashes); - String responseString = makeServiceRequest(RESERVE_USERNAME_PATH, "PUT", JsonUtil.toJson(reserveUsernameRequest), NO_HEADERS, (responseCode, body) -> { + String responseString = makeServiceRequest(RESERVE_USERNAME_PATH, "PUT", JsonUtil.toJson(reserveUsernameRequest), NO_HEADERS, (responseCode, body, getHeader) -> { switch (responseCode) { case 422: throw new UsernameMalformedException(); case 409: throw new UsernameTakenException(); @@ -1330,7 +1331,7 @@ public class PushServiceSocket { Base64.encodeUrlSafeWithoutPadding(link.getEncryptedUsername()) ); - String response = makeServiceRequest(CONFIRM_USERNAME_PATH, "PUT", JsonUtil.toJson(confirmUsernameRequest), NO_HEADERS, (responseCode, body) -> { + String response = makeServiceRequest(CONFIRM_USERNAME_PATH, "PUT", JsonUtil.toJson(confirmUsernameRequest), NO_HEADERS, (responseCode, body, getHeader) -> { switch (responseCode) { case 409: throw new UsernameIsNotReservedException(); @@ -1392,7 +1393,7 @@ public class PushServiceSocket { public void submitRateLimitPushChallenge(String challenge) throws IOException { String payload = JsonUtil.toJson(new SubmitPushChallengePayload(challenge)); - makeServiceRequest(SUBMIT_RATE_LIMIT_CHALLENGE, "PUT", payload, NO_HEADERS, (responseCode, body) -> { + makeServiceRequest(SUBMIT_RATE_LIMIT_CHALLENGE, "PUT", payload, NO_HEADERS, (responseCode, body, getHeader) -> { if (responseCode == 428) { throw new CaptchaRejectedException(); } @@ -1449,7 +1450,7 @@ public class PushServiceSocket { "POST", payload, NO_HEADERS, - (code, body) -> { + (code, body, getHeader) -> { if (code == 204) throw new NonSuccessfulResponseCodeException(204); if (code == 402) { InAppPaymentReceiptCredentialError inAppPaymentReceiptCredentialError; @@ -1540,7 +1541,7 @@ public class PushServiceSocket { "POST", payload, NO_HEADERS, - (code, body) -> { + (code, body, getHeader) -> { if (code == 204) throw new NonSuccessfulResponseCodeException(204); }); @@ -2293,7 +2294,7 @@ public class PushServiceSocket { Response response = null; try { response = getServiceConnection(urlFragment, method, body, headers, sealedSenderAccess, doNotAddAuthenticationOrUnidentifiedAccessKey); - responseCodeHandler.handle(response.code(), response.body()); + responseCodeHandler.handle(response.code(), response.body(), response::header); return validateServiceResponse(response); } catch (Exception e) { if (response != null && response.body() != null) { @@ -2822,16 +2823,12 @@ public class PushServiceSocket { } private interface ResponseCodeHandler { - void handle(int responseCode, ResponseBody body) throws NonSuccessfulResponseCodeException, PushNetworkException; - - default void handle(int responseCode, ResponseBody body, Function getHeader) throws NonSuccessfulResponseCodeException, PushNetworkException { - handle(responseCode, body); - } + void handle(int responseCode, ResponseBody body, Function getHeader) throws NonSuccessfulResponseCodeException, PushNetworkException; } private static class EmptyResponseCodeHandler implements ResponseCodeHandler { @Override - public void handle(int responseCode, ResponseBody body) { } + public void handle(int responseCode, ResponseBody body, Function getHeader) { } } /** @@ -2840,7 +2837,7 @@ public class PushServiceSocket { */ private static class UnopinionatedResponseCodeHandler implements ResponseCodeHandler { @Override - public void handle(int responseCode, ResponseBody body) throws NonSuccessfulResponseCodeException, PushNetworkException { + public void handle(int responseCode, ResponseBody body, Function getHeader) throws NonSuccessfulResponseCodeException, PushNetworkException { if (responseCode < 200 || responseCode > 299) { String bodyString = null; if (body != null) { @@ -2862,7 +2859,7 @@ public class PushServiceSocket { */ private static class LongPollingResponseCodeHandler implements ResponseCodeHandler { @Override - public void handle(int responseCode, ResponseBody body) throws NonSuccessfulResponseCodeException, PushNetworkException { + public void handle(int responseCode, ResponseBody body, Function getHeader) throws NonSuccessfulResponseCodeException, PushNetworkException { if (responseCode == 204 || responseCode < 200 || responseCode > 299) { String bodyString = null; if (body != null) { @@ -2884,7 +2881,7 @@ public class PushServiceSocket { */ private static class UnopinionatedBinaryErrorResponseCodeHandler implements ResponseCodeHandler { @Override - public void handle(int responseCode, ResponseBody body) throws NonSuccessfulResponseCodeException, PushNetworkException { + public void handle(int responseCode, ResponseBody body, Function getHeader) throws NonSuccessfulResponseCodeException, PushNetworkException { if (responseCode < 200 || responseCode > 299) { byte[] bodyBytes = null; if (body != null) { @@ -2912,27 +2909,22 @@ public class PushServiceSocket { return JsonUtil.fromJson(response, CredentialResponse.class); } - private static final ResponseCodeHandler GROUPS_V2_PUT_RESPONSE_HANDLER = (responseCode, body) -> { + private static final ResponseCodeHandler GROUPS_V2_PUT_RESPONSE_HANDLER = (responseCode, body, getHeader) -> { if (responseCode == 409) throw new GroupExistsException(); }; - private static final ResponseCodeHandler GROUPS_V2_GET_CURRENT_HANDLER = (responseCode, body) -> { + private static final ResponseCodeHandler GROUPS_V2_GET_CURRENT_HANDLER = (responseCode, body, getHeader) -> { switch (responseCode) { case 403: throw new NotInGroupException(); case 404: throw new GroupNotFoundException(); } }; - private static final ResponseCodeHandler GROUPS_V2_PATCH_RESPONSE_HANDLER = (responseCode, body) -> { + private static final ResponseCodeHandler GROUPS_V2_PATCH_RESPONSE_HANDLER = (responseCode, body, getHeader) -> { if (responseCode == 400) throw new GroupPatchNotAcceptedException(); }; private static final ResponseCodeHandler GROUPS_V2_GET_JOIN_INFO_HANDLER = new ResponseCodeHandler() { - @Override - public void handle(int responseCode, ResponseBody body) throws NonSuccessfulResponseCodeException { - if (responseCode == 403) throw new ForbiddenException(); - } - @Override public void handle(int responseCode, ResponseBody body, Function getHeader) throws NonSuccessfulResponseCodeException { if (responseCode == 403) { @@ -3054,7 +3046,7 @@ public class PushServiceSocket { public GroupJoinInfo getGroupJoinInfo(Optional groupLinkPassword, GroupsV2AuthorizationString authorization) throws NonSuccessfulResponseCodeException, PushNetworkException, IOException, MalformedResponseException { - String passwordParam = groupLinkPassword.map(org.signal.core.util.Base64::encodeUrlSafeWithoutPadding).orElse(""); + String passwordParam = groupLinkPassword.map(Base64::encodeUrlSafeWithoutPadding).orElse(""); try (Response response = makeStorageRequest(authorization.toString(), String.format(GROUPSV2_GROUP_JOIN, passwordParam), "GET", @@ -3101,7 +3093,7 @@ public class PushServiceSocket { */ private static class LinkGooglePlayBillingPurchaseTokenResponseCodeHandler implements ResponseCodeHandler { @Override - public void handle(int responseCode, ResponseBody body) throws NonSuccessfulResponseCodeException, PushNetworkException { + public void handle(int responseCode, ResponseBody body, Function getHeader) throws NonSuccessfulResponseCodeException, PushNetworkException { if (responseCode < 400) { return; } @@ -3115,7 +3107,7 @@ public class PushServiceSocket { */ private static class InAppPaymentResponseCodeHandler implements ResponseCodeHandler { @Override - public void handle(int responseCode, ResponseBody body) throws NonSuccessfulResponseCodeException, PushNetworkException { + public void handle(int responseCode, ResponseBody body, Function getHeader) throws NonSuccessfulResponseCodeException, PushNetworkException { if (responseCode < 400) { return; } @@ -3138,26 +3130,27 @@ public class PushServiceSocket { private static class RegistrationSessionResponseHandler implements ResponseCodeHandler { @Override - public void handle(int responseCode, ResponseBody body) throws NonSuccessfulResponseCodeException, PushNetworkException { + public void handle(int responseCode, ResponseBody body, Function getHeader) throws NonSuccessfulResponseCodeException, PushNetworkException { if (responseCode == 403) { throw new IncorrectRegistrationRecoveryPasswordException(); } else if (responseCode == 404) { throw new NoSuchSessionException(); } else if (responseCode == 409) { - RegistrationSessionMetadataJson response; + RegistrationSessionMetadataResponse response; try { - response = JsonUtil.fromJson(body.string(), RegistrationSessionMetadataJson.class); + response = parseSessionMetadataResponse(body, getHeader); } catch (IOException e) { - Log.e(TAG, "Unable to read response body.", e); + Log.w(TAG, "Unable to read response body.", e); throw new NonSuccessfulResponseCodeException(409); } - if (response.getVerified()) { + + if (response.getMetadata().getVerified()) { throw new AlreadyVerifiedException(); - } else if (response.pushChallengedRequired() || response.captchaRequired()) { + } else if (response.getMetadata().pushChallengedRequired() || response.getMetadata().captchaRequired()) { throw new ChallengeRequiredException(response); } else { - Log.i(TAG, "Received 409 in reg session handler that is not verified, with required information: " + String.join(", ", response.getRequestedInformation())); + Log.i(TAG, "Received 409 in reg session handler that is not verified, with required information: " + String.join(", ", response.getMetadata().getRequestedInformation())); throw new HttpConflictException(); } } else if (responseCode == 502) { @@ -3173,12 +3166,13 @@ public class PushServiceSocket { } } - - private static class RegistrationCodeRequestResponseHandler implements ResponseCodeHandler { + /** + * Error handler used exclusively for dealing with request verification code during registration flow. + */ + private static class RequestVerificationCodeResponseHandler implements ResponseCodeHandler { @Override - public void handle(int responseCode, ResponseBody body) throws NonSuccessfulResponseCodeException, PushNetworkException { - + public void handle(int responseCode, ResponseBody body, Function getHeader) throws NonSuccessfulResponseCodeException { if (responseCode == 400) { throw new MalformedRequestException(); } else if (responseCode == 403) { @@ -3186,25 +3180,34 @@ public class PushServiceSocket { } else if (responseCode == 404) { throw new NoSuchSessionException(); } else if (responseCode == 409) { - RegistrationSessionMetadataJson response; + RegistrationSessionMetadataResponse response; try { - response = JsonUtil.fromJson(body.string(), RegistrationSessionMetadataJson.class); + response = parseSessionMetadataResponse(body, getHeader); } catch (IOException e) { Log.e(TAG, "Unable to read response body.", e); throw new NonSuccessfulResponseCodeException(409); } - if (response.getVerified()) { + + if (response.getMetadata().getVerified()) { throw new AlreadyVerifiedException(); - } else if (response.pushChallengedRequired() || response.captchaRequired()) { + } else if (response.getMetadata().pushChallengedRequired() || response.getMetadata().captchaRequired()) { throw new ChallengeRequiredException(response); } else { - Log.i(TAG, "Received 409 in for reg code request that is not verified, with required information: " + String.join(", ", response.getRequestedInformation())); + Log.i(TAG, "Received 409 in for reg code request that is not verified, with required information: " + String.join(", ", response.getMetadata().getRequestedInformation())); throw new HttpConflictException(); } } else if (responseCode == 418) { throw new InvalidTransportModeException(); } else if (responseCode == 429) { - throw new RegistrationRetryException(); + RegistrationSessionMetadataResponse response; + try { + response = parseSessionMetadataResponse(body, getHeader); + } catch (IOException e) { + Log.w(TAG, "Unable to read response body.", e); + throw new NonSuccessfulResponseCodeException(429); + } + + throw new RequestVerificationCodeRateLimitException(response); } else if (responseCode == 440) { VerificationCodeFailureResponseBody response; try { @@ -3222,35 +3225,38 @@ public class PushServiceSocket { private static class PatchRegistrationSessionResponseHandler implements ResponseCodeHandler { @Override - public void handle(int responseCode, ResponseBody body) throws NonSuccessfulResponseCodeException, PushNetworkException { + public void handle(int responseCode, ResponseBody body, Function getHeader) throws NonSuccessfulResponseCodeException, PushNetworkException { switch (responseCode) { case 403: throw new TokenNotAcceptedException(); case 404: throw new NoSuchSessionException(); case 409: - RegistrationSessionMetadataJson response; + RegistrationSessionMetadataResponse response; try { - response = JsonUtil.fromJson(body.string(), RegistrationSessionMetadataJson.class); + response = parseSessionMetadataResponse(body, getHeader); } catch (IOException e) { Log.e(TAG, "Unable to read response body.", e); throw new NonSuccessfulResponseCodeException(409); } - if (response.getVerified()) { + if (response.getMetadata().getVerified()) { throw new AlreadyVerifiedException(); - } else if (response.pushChallengedRequired() || response.captchaRequired()) { + } else if (response.getMetadata().pushChallengedRequired() || response.getMetadata().captchaRequired()) { throw new ChallengeRequiredException(response); } else { - Log.i(TAG, "Received 409 for patching reg session that is not verified, with required information: " + String.join(", ", response.getRequestedInformation())); + Log.i(TAG, "Received 409 for patching reg session that is not verified, with required information: " + String.join(", ", response.getMetadata().getRequestedInformation())); throw new HttpConflictException(); } } } } - private static class RegistrationCodeSubmissionResponseHandler implements ResponseCodeHandler { + /** + * Error response handler used exclusively for submitting a verification code during a registration session. + */ + private static class SubmitVerificationCodeResponseHandler implements ResponseCodeHandler { @Override - public void handle(int responseCode, ResponseBody body) throws NonSuccessfulResponseCodeException, PushNetworkException { + public void handle(int responseCode, ResponseBody body, Function getHeader) throws NonSuccessfulResponseCodeException, PushNetworkException { switch (responseCode) { case 400: @@ -3274,6 +3280,16 @@ public class PushServiceSocket { Log.i(TAG, "Received 409 for reg code submission that is not verified, with required information: " + String.join(", ", sessionMetadata.getRequestedInformation())); throw new HttpConflictException(); } + case 429: + RegistrationSessionMetadataResponse response; + try { + response = parseSessionMetadataResponse(body, getHeader); + } catch (IOException e) { + Log.w(TAG, "Unable to read response body.", e); + throw new NonSuccessfulResponseCodeException(429); + } + + throw new SubmitVerificationCodeRateLimitException(response); case 440: VerificationCodeFailureResponseBody codeFailureResponse; try { @@ -3289,9 +3305,13 @@ public class PushServiceSocket { } private static RegistrationSessionMetadataResponse parseSessionMetadataResponse(Response response) throws IOException { + return parseSessionMetadataResponse(response.body(), response::header); + } + + private static RegistrationSessionMetadataResponse parseSessionMetadataResponse(ResponseBody body, Function getHeader) throws IOException { long serverDeliveredTimestamp = 0; try { - String stringValue = response.header(SERVER_DELIVERED_TIMESTAMP_HEADER); + String stringValue = getHeader.apply(SERVER_DELIVERED_TIMESTAMP_HEADER); stringValue = stringValue != null ? stringValue : "0"; serverDeliveredTimestamp = Long.parseLong(stringValue); @@ -3299,8 +3319,11 @@ public class PushServiceSocket { Log.w(TAG, e); } - RegistrationSessionMetadataHeaders responseHeaders = new RegistrationSessionMetadataHeaders(serverDeliveredTimestamp); - RegistrationSessionMetadataJson responseBody = JsonUtil.fromJson(readBodyString(response), RegistrationSessionMetadataJson.class); + long retryAfterLong = Util.parseLong(getHeader.apply("Retry-After"), -1); + Long retryAfter = retryAfterLong != -1 ? TimeUnit.SECONDS.toMillis(retryAfterLong) : null; + + RegistrationSessionMetadataHeaders responseHeaders = new RegistrationSessionMetadataHeaders(serverDeliveredTimestamp, retryAfter); + RegistrationSessionMetadataJson responseBody = JsonUtil.fromJson(body.string(), RegistrationSessionMetadataJson.class); return new RegistrationSessionMetadataResponse(responseHeaders, responseBody, null); } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RegistrationSessionMetadataResponse.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RegistrationSessionMetadataResponse.kt index fe4b048d9e..b0012b70f3 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RegistrationSessionMetadataResponse.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RegistrationSessionMetadataResponse.kt @@ -8,12 +8,13 @@ import com.fasterxml.jackson.annotation.JsonProperty */ data class RegistrationSessionMetadataResponse( val headers: RegistrationSessionMetadataHeaders, - val body: RegistrationSessionMetadataJson, + val metadata: RegistrationSessionMetadataJson, val state: RegistrationSessionState? ) data class RegistrationSessionMetadataHeaders( - val timestamp: Long + val serverDeliveredTimestamp: Long, + val retryAfterTimestamp: Long? = null ) data class RegistrationSessionMetadataJson( diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/PaymentsRegionException.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/PaymentsRegionException.java index 6ef914403a..580fa9d645 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/PaymentsRegionException.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/PaymentsRegionException.java @@ -2,6 +2,8 @@ package org.whispersystems.signalservice.internal.push.exceptions; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; +import java.util.function.Function; + import okhttp3.ResponseBody; public final class PaymentsRegionException extends NonSuccessfulResponseCodeException { @@ -12,7 +14,7 @@ public final class PaymentsRegionException extends NonSuccessfulResponseCodeExce /** * Promotes a 403 to this exception type. */ - public static void responseCodeHandler(int responseCode, ResponseBody body) throws PaymentsRegionException { + public static void responseCodeHandler(int responseCode, ResponseBody body, Function getHeader) throws PaymentsRegionException { if (responseCode == 403) { throw new PaymentsRegionException(responseCode); }