Fix multiple bugs and erroneous sad path handling in registration flows.

This commit is contained in:
Cody Henthorne
2025-01-22 13:25:43 -05:00
committed by GitHub
parent e0553a59d5
commit f1782d06a4
23 changed files with 586 additions and 215 deletions

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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<RegistrationSessionMetadataResponse>): 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<Challenge>) : 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)

View File

@@ -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) {

View File

@@ -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<RegistrationViewModel>()
private val fragmentViewModel by viewModels<EnterCodeViewModel>()
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<Boolean>() {
@@ -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<Boolean>() {
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()

View File

@@ -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)

View File

@@ -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!")

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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<RegistrationViewModel>()
private val fragmentViewModel by viewModels<EnterCodeViewModel>()
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<Boolean>() {
@@ -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<Boolean>() {
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()

View File

@@ -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)

View File

@@ -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!")