Further registration lock improvements in Registration V2.

This commit is contained in:
Nicholas Tinsley
2024-05-31 15:15:32 -04:00
committed by Cody Henthorne
parent b71ba79b8a
commit 303100bb6b
13 changed files with 336 additions and 159 deletions

View File

@@ -4,6 +4,8 @@ import androidx.annotation.NonNull;
public final class ReceivedSmsEvent {
public static final int CODE_LENGTH = 6;
private final @NonNull String code;
public ReceivedSmsEvent(@NonNull String code) {

View File

@@ -43,6 +43,7 @@ sealed class RegisterAccountResult(cause: Throwable?) : RegistrationResult(cause
}
}
}
private fun createRateLimitProcessor(exception: RateLimitException): RegisterAccountResult {
return if (exception.retryAfterMilliseconds.isPresent) {
RateLimited(exception, exception.retryAfterMilliseconds.get())

View File

@@ -21,6 +21,7 @@ import org.whispersystems.signalservice.api.push.exceptions.PushChallengeRequire
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException
import org.whispersystems.signalservice.api.push.exceptions.RegistrationRetryException
import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException
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
@@ -45,9 +46,11 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
} else {
Success(
sessionId = networkResult.result.body.id,
allowedToRequestCode = networkResult.result.body.allowedToRequestCode,
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
)
}
@@ -67,7 +70,7 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
is InvalidTransportModeException -> InvalidTransportModeFailure(cause)
is MalformedRequestException -> MalformedRequest(cause)
is RegistrationRetryException -> MustRetry(cause)
is LockedException -> RegistrationLocked(cause = cause, timeRemaining = cause.timeRemaining)
is LockedException -> RegistrationLocked(cause = cause, timeRemaining = cause.timeRemaining, svr2Credentials = cause.svr2Credentials)
is NoSuchSessionException -> NoSuchSession(cause)
is AlreadyVerifiedException -> AlreadyVerified(cause)
else -> UnknownError(cause)
@@ -100,7 +103,7 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
}
}
class Success(val sessionId: String, val allowedToRequestCode: Boolean, val nextSmsTimestamp: Long, val nextCallTimestamp: Long, val verified: Boolean) : VerificationCodeRequestResult(null)
class Success(val sessionId: String, val nextSmsTimestamp: Long, val nextCallTimestamp: Long, nextVerificationAttempt: Long, val allowedToRequestCode: Boolean, challengesRequested: List<Challenge>, val verified: Boolean) : VerificationCodeRequestResult(null)
class ChallengeRequired(val challenges: List<Challenge>) : VerificationCodeRequestResult(null)
@@ -122,7 +125,7 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
class MustRetry(cause: Throwable) : VerificationCodeRequestResult(cause)
class RegistrationLocked(cause: Throwable, val timeRemaining: Long) : VerificationCodeRequestResult(cause)
class RegistrationLocked(cause: Throwable, val timeRemaining: Long, val svr2Credentials: AuthCredentials) : VerificationCodeRequestResult(cause)
class NoSuchSession(cause: Throwable) : VerificationCodeRequestResult(cause)

View File

@@ -9,6 +9,8 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.ActivityNavigator
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BaseActivity
@@ -20,6 +22,7 @@ import org.thoughtcrime.securesms.pin.PinRestoreActivity
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.registration.SmsRetrieverReceiver
/**
* Activity to hold the entire registration process.
@@ -30,6 +33,12 @@ class RegistrationV2Activity : BaseActivity() {
val sharedViewModel: RegistrationV2ViewModel by viewModels()
private var smsRetrieverReceiver: SmsRetrieverReceiver? = null
init {
lifecycle.addObserver(SmsRetrieverObserver())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_registration_navigation_v2)
@@ -80,6 +89,18 @@ class RegistrationV2Activity : BaseActivity() {
}
}
private inner class SmsRetrieverObserver : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
smsRetrieverReceiver = SmsRetrieverReceiver(application)
smsRetrieverReceiver?.registerReceiver()
}
override fun onDestroy(owner: LifecycleOwner) {
smsRetrieverReceiver?.unregisterReceiver()
smsRetrieverReceiver = null
}
}
companion object {
@JvmStatic

View File

@@ -15,7 +15,7 @@ import org.whispersystems.signalservice.internal.push.AuthCredentials
*/
data class RegistrationV2State(
val sessionId: String? = null,
val enteredCode: String? = null,
val enteredCode: String = "",
val phoneNumber: Phonenumber.PhoneNumber? = null,
val inProgress: Boolean = false,
val isReRegister: Boolean = false,
@@ -23,6 +23,7 @@ data class RegistrationV2State(
val canSkipSms: Boolean = false,
val svrAuthCredentials: AuthCredentials? = null,
val svrTriesRemaining: Int = 10,
val incorrectCodeAttempts: Int = 0,
val isRegistrationLockEnabled: Boolean = false,
val lockedTimeRemaining: Long = 0L,
val userSkippedReregistration: Boolean = false,
@@ -32,8 +33,11 @@ data class RegistrationV2State(
val challengesRequested: List<Challenge> = emptyList(),
val challengesPresented: Set<Challenge> = emptySet(),
val captchaToken: String? = null,
val allowedToRequestCode: Boolean = false,
val nextSmsTimestamp: Long = 0L,
val nextCallTimestamp: Long = 0L,
val nextVerificationAttempt: Long = 0L,
val verified: Boolean = false,
val smsListenerTimeout: Long = 0L,
val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION,
val networkError: Throwable? = null

View File

@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.registration.v2.ui
import android.Manifest
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
@@ -17,14 +18,18 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.util.isNotNullOrBlank
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob
import org.thoughtcrime.securesms.jobs.ProfileUploadJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.pin.SvrRepository
import org.thoughtcrime.securesms.pin.SvrWrongPinException
import org.thoughtcrime.securesms.registration.RegistrationData
import org.thoughtcrime.securesms.registration.RegistrationUtil
@@ -51,6 +56,7 @@ import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeR
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.Success
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.TokenNotAccepted
import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult.UnknownError
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
@@ -58,6 +64,7 @@ import org.whispersystems.signalservice.api.SvrNoDataException
import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import java.io.IOException
import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration.Companion.minutes
/**
@@ -84,6 +91,8 @@ class RegistrationV2ViewModel : ViewModel() {
val lockedTimeRemaining = store.map { it.lockedTimeRemaining }.asLiveData()
val incorrectCodeAttempts = store.map { it.incorrectCodeAttempts }.asLiveData()
val svrTriesRemaining: Int
get() = store.value.svrTriesRemaining
@@ -104,6 +113,22 @@ class RegistrationV2ViewModel : ViewModel() {
}
}
fun maybePrefillE164(context: Context) {
Log.v(TAG, "maybePrefillE164()")
if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {
val localNumber = Util.getDeviceNumber(context).getOrNull()
if (localNumber != null) {
Log.v(TAG, "Phone number detected.")
setPhoneNumber(localNumber)
} else {
Log.i(TAG, "Could not read phone number.")
}
} else {
Log.i(TAG, "No phone permission.")
}
}
fun setInProgress(inProgress: Boolean) {
store.update {
it.copy(inProgress = inProgress)
@@ -131,6 +156,12 @@ class RegistrationV2ViewModel : ViewModel() {
}
}
fun incrementIncorrectCodeAttempts() {
store.update {
it.copy(incorrectCodeAttempts = it.incorrectCodeAttempts + 1)
}
}
fun addPresentedChallenge(challenge: Challenge) {
store.update {
it.copy(challengesPresented = it.challengesPresented.plus(challenge))
@@ -165,60 +196,73 @@ class RegistrationV2ViewModel : ViewModel() {
fun onBackupSuccessfullyRestored() {
val recoveryPassword = SignalStore.svr().recoveryPassword
store.update {
it.copy(registrationCheckpoint = RegistrationCheckpoint.BACKUP_RESTORED_OR_SKIPPED, recoveryPassword = SignalStore.svr().recoveryPassword, canSkipSms = recoveryPassword != null)
it.copy(registrationCheckpoint = RegistrationCheckpoint.BACKUP_RESTORED_OR_SKIPPED, recoveryPassword = SignalStore.svr().recoveryPassword, canSkipSms = recoveryPassword != null, isReRegister = true)
}
}
fun onUserConfirmedPhoneNumber(context: Context, errorHandler: (RegistrationResult) -> Unit) {
setRegistrationCheckpoint(RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED)
val state = store.value
if (state.phoneNumber == null) {
Log.w(TAG, "Phone number was null after confirmation.")
onErrorOccurred()
return
}
val e164 = state.phoneNumber.toE164()
if (hasRecoveryPassword() && matchesSavedE164(e164)) {
// Re-registration when the local database is intact.
Log.d(TAG, "Has recovery password, and therefore can skip SMS verification.")
store.update {
it.copy(
canSkipSms = true,
inProgress = false
)
val e164 = state.phoneNumber?.toE164() ?: return bail { Log.i(TAG, "Phone number was null after confirmation.") }
if (!state.userSkippedReregistration) {
if (hasRecoveryPassword() && matchesSavedE164(e164)) {
// Re-registration when the local database is intact.
Log.d(TAG, "Has recovery password, and therefore can skip SMS verification.")
store.update {
it.copy(
canSkipSms = true,
isReRegister = true,
inProgress = false
)
}
return
}
return
}
viewModelScope.launch {
val svrCredentialsResult: BackupAuthCheckResult = RegistrationRepository.hasValidSvrAuthCredentials(context, e164, password)
if (!state.userSkippedReregistration) {
val svrCredentialsResult: BackupAuthCheckResult = RegistrationRepository.hasValidSvrAuthCredentials(context, e164, password)
when (svrCredentialsResult) {
is BackupAuthCheckResult.UnknownError -> {
handleGenericError(svrCredentialsResult.getCause())
return@launch
}
is BackupAuthCheckResult.SuccessWithCredentials -> {
Log.d(TAG, "Found local valid SVR auth credentials.")
store.update {
it.copy(canSkipSms = true, svrAuthCredentials = svrCredentialsResult.authCredentials, inProgress = false)
when (svrCredentialsResult) {
is BackupAuthCheckResult.UnknownError -> {
handleGenericError(svrCredentialsResult.getCause())
return@launch
}
return@launch
}
is BackupAuthCheckResult.SuccessWithoutCredentials -> {
Log.d(TAG, "No local SVR auth credentials could be found and/or validated.")
is BackupAuthCheckResult.SuccessWithCredentials -> {
Log.d(TAG, "Found local valid SVR auth credentials.")
store.update {
it.copy(isReRegister = true, canSkipSms = true, svrAuthCredentials = svrCredentialsResult.authCredentials, inProgress = false)
}
return@launch
}
is BackupAuthCheckResult.SuccessWithoutCredentials -> {
Log.d(TAG, "No local SVR auth credentials could be found and/or validated.")
}
}
}
val validSession = getOrCreateValidSession(context, errorHandler) ?: return@launch
val validSession = getOrCreateValidSession(context, errorHandler) ?: return@launch bail { Log.i(TAG, "Could not create valid session for confirming the entered E164.") }
if (validSession.body.verified) {
Log.i(TAG, "Session is already verified, registering account.")
registerVerifiedSession(context, validSession.body.id, errorHandler)
return@launch
}
if (!validSession.body.allowedToRequestCode) {
val challenges = validSession.body.requestedInformation.joinToString()
Log.i(TAG, "Not allowed to request code! Remaining challenges: $challenges")
handleSessionStateResult(context, ChallengeRequired(Challenge.parse(validSession.body.requestedInformation)), errorHandler)
if (System.currentTimeMillis() > (validSession.body.nextVerificationAttempt ?: Int.MAX_VALUE)) {
store.update {
it.copy(registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED)
}
} else {
val challenges = validSession.body.requestedInformation
Log.i(TAG, "Not allowed to request code! Remaining challenges: ${challenges.joinToString()}")
handleSessionStateResult(context, ChallengeRequired(Challenge.parse(validSession.body.requestedInformation)), errorHandler)
}
return@launch
}
@@ -227,16 +271,10 @@ class RegistrationV2ViewModel : ViewModel() {
}
fun requestSmsCode(context: Context, errorHandler: (RegistrationResult) -> Unit) {
val e164 = getCurrentE164()
if (e164 == null) {
Log.w(TAG, "Phone number was null after confirmation.")
onErrorOccurred()
return
}
val e164 = getCurrentE164() ?: return bail { Log.i(TAG, "Phone number was null after confirmation.") }
viewModelScope.launch {
val validSession = getOrCreateValidSession(context, errorHandler) ?: return@launch
val validSession = getOrCreateValidSession(context, errorHandler) ?: return@launch bail { Log.i(TAG, "Could not create valid session for requesting an SMS code.") }
requestSmsCodeInternal(context, validSession.body.id, e164, errorHandler)
}
}
@@ -251,7 +289,7 @@ class RegistrationV2ViewModel : ViewModel() {
}
viewModelScope.launch {
val validSession = getOrCreateValidSession(context, errorHandler) ?: return@launch
val validSession = getOrCreateValidSession(context, errorHandler) ?: return@launch bail { Log.i(TAG, "Could not create valid session for requesting a verification call.") }
Log.d(TAG, "Requesting voice call code…")
val codeRequestResponse = RegistrationRepository.requestSmsCode(
context = context,
@@ -260,9 +298,12 @@ class RegistrationV2ViewModel : ViewModel() {
password = password,
mode = RegistrationRepository.Mode.PHONE_CALL
)
Log.d(TAG, "Voice call code request submitted.")
Log.d(TAG, "Voice code request network call completed.")
handleSessionStateResult(context, codeRequestResponse, errorHandler)
if (codeRequestResponse is Success) {
Log.d(TAG, "Voice code request was successful.")
}
}
}
@@ -305,6 +346,7 @@ class RegistrationV2ViewModel : ViewModel() {
}
private suspend fun getOrCreateValidSession(context: Context, errorHandler: (RegistrationResult) -> Unit): RegistrationSessionMetadataResponse? {
Log.v(TAG, "getOrCreateValidSession()")
val e164 = getCurrentE164() ?: throw IllegalStateException("E164 required to create session!")
val mccMncProducer = MccMncProducer(context)
@@ -316,12 +358,17 @@ class RegistrationV2ViewModel : ViewModel() {
password = password,
mcc = mccMncProducer.mcc,
mnc = mccMncProducer.mnc,
successListener = { freshSession ->
val freshSessionId = freshSession.body.id
if (freshSessionId != existingSessionId) {
store.update {
it.copy(sessionId = freshSessionId)
}
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
)
}
},
errorHandler = errorHandler
@@ -333,7 +380,7 @@ class RegistrationV2ViewModel : ViewModel() {
val captchaToken = store.value.captchaToken ?: throw IllegalStateException("Can't submit captcha token if no captcha token is set!")
viewModelScope.launch {
val session = getOrCreateValidSession(context, errorHandler) ?: return@launch
val session = getOrCreateValidSession(context, errorHandler) ?: 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)
Log.d(TAG, "Captcha token submitted.")
@@ -353,7 +400,7 @@ class RegistrationV2ViewModel : ViewModel() {
viewModelScope.launch {
Log.d(TAG, "Getting session in order to perform push token verification…")
val session = getOrCreateValidSession(context, errorHandler) ?: return@launch
val session = getOrCreateValidSession(context, errorHandler) ?: 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)) {
Log.d(TAG, "Push submission no longer necessary, bailing.")
@@ -362,7 +409,7 @@ class RegistrationV2ViewModel : ViewModel() {
inProgress = false
)
}
return@launch
return@launch bail { Log.i(TAG, "Push challenge token no longer needed, bailing.") }
}
Log.d(TAG, "Requesting push challenge token…")
@@ -376,6 +423,7 @@ class RegistrationV2ViewModel : ViewModel() {
* @return whether the request was successful and execution should continue
*/
private suspend fun handleSessionStateResult(context: Context, sessionResult: RegistrationResult, errorHandler: (RegistrationResult) -> Unit): Boolean {
Log.v(TAG, "handleSessionStateResult()")
when (sessionResult) {
is UnknownError -> {
handleGenericError(sessionResult.getCause())
@@ -564,8 +612,6 @@ class RegistrationV2ViewModel : ViewModel() {
private suspend fun registerAccountInternal(context: Context, sessionId: String?, registrationData: RegistrationData, pin: String?, masterKey: MasterKey): Pair<RegisterAccountResult, Boolean> {
val registrationResult: RegisterAccountResult = RegistrationRepository.registerAccount(context = context, sessionId = sessionId, registrationData = registrationData, pin = pin) { masterKey }
// TODO: check for wrong recovery password
// Check if reg lock is enabled
if (registrationResult !is RegisterAccountResult.RegistrationLocked) {
return Pair(registrationResult, false)
@@ -593,7 +639,7 @@ class RegistrationV2ViewModel : ViewModel() {
verifyCodeInternal(
context = context,
pin = null,
reglockEnabled = false,
registrationLocked = false,
submissionErrorHandler = submissionErrorHandler,
registrationErrorHandler = registrationErrorHandler
)
@@ -612,24 +658,82 @@ class RegistrationV2ViewModel : ViewModel() {
verifyCodeInternal(
context = context,
pin = pin,
reglockEnabled = true,
registrationLocked = true,
submissionErrorHandler = submissionErrorHandler,
registrationErrorHandler = registrationErrorHandler
)
}
}
private suspend fun verifyCodeInternal(context: Context, reglockEnabled: Boolean, pin: String?, submissionErrorHandler: (RegistrationResult) -> Unit, registrationErrorHandler: (RegisterAccountResult) -> Unit) {
private suspend fun verifyCodeInternal(context: Context, registrationLocked: Boolean, pin: String?, submissionErrorHandler: (RegistrationResult) -> Unit, registrationErrorHandler: (RegisterAccountResult) -> Unit) {
Log.d(TAG, "Getting valid session in order to submit verification code.")
if (registrationLocked && pin.isNullOrBlank()) {
throw IllegalStateException("Must have PIN to register with registration lock!")
}
var reglock = registrationLocked
val sessionId = getOrCreateValidSession(context, submissionErrorHandler)?.body?.id ?: return
val registrationData = getRegistrationData()
val registrationResponse = verifyCode(context, sessionId, registrationData, pin) {
viewModelScope.launch { // TODO: validate the scopes are correct here
handleSessionStateResult(context, it, submissionErrorHandler)
}
} ?: return
Log.d(TAG, "Submitting verification code…")
handleRegistrationResult(context, registrationData, registrationResponse, reglockEnabled, registrationErrorHandler)
val verificationResponse = RegistrationRepository.submitVerificationCode(context, sessionId, registrationData)
val submissionSuccessful = verificationResponse is Success
val alreadyVerified = verificationResponse is AlreadyVerified
Log.d(TAG, "Verification code submission network call completed. Submission successful? $submissionSuccessful Account already verified? $alreadyVerified")
if (!submissionSuccessful && !alreadyVerified) {
handleSessionStateResult(context, verificationResponse, submissionErrorHandler)
return
}
Log.d(TAG, "Submitting registration…")
var result: RegisterAccountResult? = null
var state = store.value
if (!reglock) {
Log.d(TAG, "Registration lock not enabled, attempting to register account without master key producer.")
result = RegistrationRepository.registerAccount(context, sessionId, registrationData, pin)
}
if (result is RegisterAccountResult.RegistrationLocked) {
Log.d(TAG, "Registration lock response received.")
reglock = true
if (pin == null && SignalStore.svr().registrationLockToken != null) {
Log.d(TAG, "Retrying registration with stored credentials.")
result = RegistrationRepository.registerAccount(context, sessionId, registrationData, SignalStore.svr().pin) { SignalStore.svr().getOrCreateMasterKey() }
} else if (result.svr2Credentials != null) {
Log.d(TAG, "Retrying registration with received credentials.")
val credentials = result.svr2Credentials
state = store.updateAndGet {
it.copy(svrAuthCredentials = credentials)
}
}
}
if (reglock && pin.isNotNullOrBlank()) {
Log.d(TAG, "Registration lock enabled, attempting to register account restore master key from SVR.")
result = RegistrationRepository.registerAccount(context, sessionId, registrationData, pin) {
SvrRepository.restoreMasterKeyPreRegistration(SvrAuthCredentialSet(null, state.svrAuthCredentials), pin)
}
}
if (result != null) {
handleRegistrationResult(context, registrationData, result, reglock, registrationErrorHandler)
} else {
Log.w(TAG, "No registration response received!")
}
}
private suspend fun registerVerifiedSession(context: Context, sessionId: String, registrationErrorHandler: (RegisterAccountResult) -> Unit) {
val registrationData = getRegistrationData()
val registrationResponse: RegisterAccountResult = RegistrationRepository.registerAccount(context, sessionId, registrationData)
handleRegistrationResult(context, registrationData, registrationResponse, false, registrationErrorHandler)
}
private suspend fun onSuccessfulRegistration(context: Context, registrationData: RegistrationData, remoteResult: RegistrationRepository.AccountRegistrationResult, reglockEnabled: Boolean) {
@@ -679,7 +783,7 @@ class RegistrationV2ViewModel : ViewModel() {
private suspend fun getRegistrationData(): RegistrationData {
val currentState = store.value
val code = currentState.enteredCode ?: throw IllegalStateException("Can't construct registration data without entered code!")
val code = currentState.enteredCode
val e164: String = currentState.phoneNumber?.toE164() ?: throw IllegalStateException("Can't construct registration data without E164!")
val recoveryPassword = if (currentState.sessionId == null) SignalStore.svr().getRecoveryPassword() else null
return RegistrationData(code, e164, password, RegistrationRepository.getRegistrationId(), RegistrationRepository.getProfileKey(e164), currentState.fcmToken, RegistrationRepository.getPniRegistrationId(), recoveryPassword)
@@ -693,6 +797,16 @@ class RegistrationV2ViewModel : ViewModel() {
setInProgress(false)
}
/**
* Used for early returns in order to end the in-progress visual state, as well as print a log message explaining what happened.
*
* @param logMessage Logging code is wrapped in lambda so that our automated tools detect the various [Log] calls with their accompanying messages.
*/
private fun bail(logMessage: () -> Unit) {
logMessage()
setInProgress(false)
}
companion object {
private val TAG = Log.tag(RegistrationV2ViewModel::class.java)
@@ -737,29 +851,5 @@ class RegistrationV2ViewModel : ViewModel() {
}
return null
}
suspend fun verifyCode(context: Context, sessionId: String, registrationData: RegistrationData, pin: String?, submissionErrorHandler: (RegistrationResult) -> Unit): RegisterAccountResult? {
Log.d(TAG, "Getting valid session in order to submit verification code.")
Log.d(TAG, "Submitting verification code…")
val verificationResponse = RegistrationRepository.submitVerificationCode(context, sessionId, registrationData)
val submissionSuccessful = verificationResponse is Success
val alreadyVerified = verificationResponse is AlreadyVerified
Log.i(TAG, "Verification code submission network call completed. Submission successful? $submissionSuccessful Account already verified? $alreadyVerified")
if (!submissionSuccessful && !alreadyVerified) {
submissionErrorHandler(verificationResponse)
return null
}
Log.d(TAG, "Submitting registration…")
val registrationResponse: RegisterAccountResult = RegistrationRepository.registerAccount(context, sessionId, registrationData, pin)
Log.d(TAG, "Registration network call completed.")
return registrationResponse
}
}
}

View File

@@ -8,16 +8,20 @@ package org.thoughtcrime.securesms.registration.v2.ui.entercode
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeV2Binding
import org.thoughtcrime.securesms.registration.ReceivedSmsEvent
import org.thoughtcrime.securesms.registration.fragments.ContactSupportBottomSheetFragment
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.fragments.SignalStrengthPhoneStateListener
@@ -28,6 +32,7 @@ import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.visible
/**
* The final screen of account registration, where the user enters their verification code.
@@ -101,6 +106,12 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
}
}
sharedViewModel.incorrectCodeAttempts.observe(viewLifecycleOwner) { attempts: Int ->
if (attempts >= 3) {
binding.havingTroubleButton.visible = true
}
}
sharedViewModel.uiState.observe(viewLifecycleOwner) {
binding.resendSmsCountDown.startCountDownTo(it.nextSmsTimestamp)
binding.callMeCountDown.startCountDownTo(it.nextCallTimestamp)
@@ -116,6 +127,7 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
when (result) {
is VerificationCodeRequestResult.Success -> binding.keyboard.displaySuccess()
is VerificationCodeRequestResult.RateLimited -> presentRateLimitedDialog()
is VerificationCodeRequestResult.AttemptsExhausted -> presentAccountLocked()
is VerificationCodeRequestResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
else -> presentGenericError(result)
}
@@ -125,12 +137,24 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
when (result) {
is RegisterAccountResult.Success -> binding.keyboard.displaySuccess()
is RegisterAccountResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining)
is RegisterAccountResult.AttemptsExhausted,
is RegisterAccountResult.AuthorizationFailed -> presentIncorrectCodeDialog()
is RegisterAccountResult.AttemptsExhausted -> presentAccountLocked()
is RegisterAccountResult.RateLimited -> presentRateLimitedDialog()
else -> presentGenericError(result)
}
}
private fun presentAccountLocked() {
binding.keyboard.displayLocked().addListener(
object : AssertedSuccessListener<Boolean>() {
override fun onSuccess(result: Boolean?) {
findNavController().safeNavigate(EnterCodeV2FragmentDirections.actionAccountLocked())
}
}
)
}
private fun presentRegistrationLocked(timeRemaining: Long) {
binding.keyboard.displayLocked().addListener(
object : AssertedSuccessListener<Boolean>() {
@@ -162,6 +186,21 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
)
}
private fun presentIncorrectCodeDialog() {
sharedViewModel.incrementIncorrectCodeAttempts()
Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_code, Toast.LENGTH_LONG).show()
binding.keyboard.displayFailure().addListener(object : AssertedSuccessListener<Boolean?>() {
override fun onSuccess(result: Boolean?) {
binding.callMeCountDown.setVisibility(View.VISIBLE)
binding.resendSmsCountDown.setVisibility(View.VISIBLE)
binding.wrongNumber.setVisibility(View.VISIBLE)
binding.code.clear()
binding.keyboard.displayKeyboard()
}
})
}
private fun presentGenericError(requestResult: RegistrationResult) {
binding.keyboard.displayFailure().addListener(
object : AssertedSuccessListener<Boolean>() {
@@ -172,7 +211,7 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
setTitle(it)
}
setMessage(getString(R.string.RegistrationActivity_error_connecting_to_service))
setPositiveButton(android.R.string.ok, null)
setPositiveButton(android.R.string.ok) { _, _ -> binding.keyboard.displayKeyboard() }
show()
}
}
@@ -185,6 +224,34 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
NavHostFragment.findNavController(this).popBackStack()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onVerificationCodeReceived(event: ReceivedSmsEvent) {
binding.code.clear()
if (event.code.isBlank() || event.code.length != ReceivedSmsEvent.CODE_LENGTH) {
Log.i(TAG, "Received invalid code of length ${event.code.length}. Ignoring.")
return
}
val finalIndex = ReceivedSmsEvent.CODE_LENGTH - 1
autopilotCodeEntryActive = true
try {
event.code
.map { it.digitToInt() }
.forEachIndexed { i, digit ->
binding.code.postDelayed({
binding.code.append(digit)
if (i == finalIndex) {
autopilotCodeEntryActive = false
}
}, i * 200L)
}
} catch (notADigit: IllegalArgumentException) {
Log.w(TAG, "Failed to convert code into digits.", notADigit)
autopilotCodeEntryActive = false
}
}
private inner class PhoneStateCallback : SignalStrengthPhoneStateListener.Callback {
override fun onNoCellSignalPresent() {
bottomSheet.show(childFragmentManager, BOTTOM_SHEET_TAG)

View File

@@ -94,6 +94,7 @@ class GrantPermissionsV2Fragment : ComposeFragment() {
permissions.forEach {
Log.d(TAG, "${it.key} = ${it.value}")
}
sharedViewModel.maybePrefillE164(requireContext())
sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED)
proceedToNextScreen()
}

View File

@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.registration.v2.ui.phonenumber
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.text.Editable
import android.text.SpannableStringBuilder
import android.text.TextWatcher
import android.view.KeyEvent
@@ -202,20 +203,22 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
spinnerView.threshold = 100
spinnerView.setAdapter(spinnerAdapter)
spinnerView.addTextChangedListener { s ->
if (s.isNullOrEmpty()) {
return@addTextChangedListener
}
spinnerView.addTextChangedListener(afterTextChanged = ::onCountryDropDownChanged)
}
if (s[0] != '+') {
s.insert(0, "+")
}
private fun onCountryDropDownChanged(s: Editable?) {
if (s.isNullOrEmpty()) {
return
}
fragmentViewModel.supportedCountryPrefixes.firstOrNull { it.toString() == s.toString() }?.let {
fragmentViewModel.setCountry(it.digits)
val numberLength: Int = phoneNumberInputLayout.text?.length ?: 0
phoneNumberInputLayout.setSelection(numberLength, numberLength)
}
if (s[0] != '+') {
s.insert(0, "+")
}
fragmentViewModel.supportedCountryPrefixes.firstOrNull { it.toString() == s.toString() }?.let {
fragmentViewModel.setCountry(it.digits)
val numberLength: Int = phoneNumberInputLayout.text?.length ?: 0
phoneNumberInputLayout.setSelection(numberLength, numberLength)
}
}
@@ -380,9 +383,12 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
sharedViewModel.fetchFcmToken(requireContext())
} else {
sharedViewModel.uiState.value?.let { value ->
val now = System.currentTimeMillis()
if (value.phoneNumber == null) {
fragmentViewModel.setError(EnterPhoneNumberV2State.Error.INVALID_PHONE_NUMBER)
sharedViewModel.setInProgress(false)
} else if (now < value.nextSmsTimestamp) {
moveToVerificationEntryScreen()
} else {
presentConfirmNumberDialog(value.phoneNumber, value.isReRegister, value.canSkipSms, missingFcmConsentRequired = true)
}
@@ -441,7 +447,7 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
}
}
private fun onConfirmNumberDialogCanceled() {
private fun handleConfirmNumberDialogCanceled() {
Log.d(TAG, "User canceled confirm number, returning to edit number.")
sharedViewModel.setInProgress(false)
ViewUtil.focusAndMoveCursorToEndAndOpenKeyboard(phoneNumberInputLayout)
@@ -473,8 +479,8 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio
sharedViewModel.onUserConfirmedPhoneNumber(requireContext(), ::handleErrorResponse)
}
}
setNegativeButton(R.string.RegistrationActivity_edit_number) { _, _ -> onConfirmNumberDialogCanceled() }
setOnCancelListener { _ -> onConfirmNumberDialogCanceled() }
setNegativeButton(R.string.RegistrationActivity_edit_number) { _, _ -> handleConfirmNumberDialogCanceled() }
setOnCancelListener { _ -> handleConfirmNumberDialogCanceled() }
}.show()
}

View File

@@ -11,12 +11,8 @@ import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository
/**
* State holder for the phone number entry screen, including phone number and Play Services errors.
*/
data class EnterPhoneNumberV2State(val countryPrefixIndex: Int = 1, val phoneNumber: String = "", val phoneNumberFormatter: TextWatcher? = null, val mode: RegistrationRepository.Mode = RegistrationRepository.Mode.SMS_WITHOUT_LISTENER, val error: Error = Error.NONE) {
data class EnterPhoneNumberV2State(val countryPrefixIndex: Int = 0, val phoneNumber: String = "", val phoneNumberFormatter: TextWatcher? = null, val mode: RegistrationRepository.Mode = RegistrationRepository.Mode.SMS_WITHOUT_LISTENER, val error: Error = Error.NONE) {
enum class Error {
NONE,
INVALID_PHONE_NUMBER,
PLAY_SERVICES_MISSING,
PLAY_SERVICES_NEEDS_UPDATE,
PLAY_SERVICES_TRANSIENT
NONE, INVALID_PHONE_NUMBER, PLAY_SERVICES_MISSING, PLAY_SERVICES_NEEDS_UPDATE, PLAY_SERVICES_TRANSIENT
}
}

View File

@@ -49,6 +49,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio
companion object {
private val TAG = Log.tag(RegistrationLockV2Fragment::class.java)
}
private val binding: FragmentRegistrationLockBinding by ViewBinderDelegate(FragmentRegistrationLockBinding::bind)
private val viewModel by activityViewModels<RegistrationV2ViewModel>()
@@ -145,17 +146,25 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio
when (requestResult) {
is VerificationCodeRequestResult.Success -> Unit
is VerificationCodeRequestResult.RateLimited -> onRateLimited()
is VerificationCodeRequestResult.AttemptsExhausted,
is VerificationCodeRequestResult.RegistrationLocked -> onKbsAccountLocked()
is VerificationCodeRequestResult.AttemptsExhausted -> navigateToAccountLocked()
is VerificationCodeRequestResult.RegistrationLocked -> {
Log.i(TAG, "Registration locked response to verify account!")
binding.kbsLockPinConfirm.cancelSpinning()
enableAndFocusPinEntry()
Toast.makeText(requireContext(), "Reg lock!", Toast.LENGTH_LONG).show()
}
else -> when (val cause = requestResult.getCause()) {
is SvrWrongPinException -> {
Log.w(TAG, "TODO figure out which Result class this results in and create a concrete class.")
onIncorrectKbsRegistrationLockPin(cause.triesRemaining)
}
is SvrNoDataException -> {
Log.w(TAG, "TODO figure out which Result class this results in and create a concrete class.")
onKbsAccountLocked()
navigateToAccountLocked()
}
else -> {
Log.w(TAG, "Unable to verify code with registration lock", cause)
onError()
@@ -168,17 +177,25 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio
when (result) {
is RegisterAccountResult.Success -> Unit
is RegisterAccountResult.RateLimited -> onRateLimited()
is RegisterAccountResult.AttemptsExhausted,
is RegisterAccountResult.RegistrationLocked -> onKbsAccountLocked()
is RegisterAccountResult.AttemptsExhausted -> navigateToAccountLocked()
is RegisterAccountResult.RegistrationLocked -> {
Log.i(TAG, "Registration locked response to register account!")
binding.kbsLockPinConfirm.cancelSpinning()
enableAndFocusPinEntry()
Toast.makeText(requireContext(), "Reg lock!", Toast.LENGTH_LONG).show()
}
else -> when (val cause = result.getCause()) {
is SvrWrongPinException -> {
Log.w(TAG, "TODO figure out which Result class this results in and create a concrete class.")
onIncorrectKbsRegistrationLockPin(cause.triesRemaining)
}
is SvrNoDataException -> {
Log.w(TAG, "TODO figure out which Result class this results in and create a concrete class.")
onKbsAccountLocked()
navigateToAccountLocked()
}
else -> {
Log.w(TAG, "Unable to register account with registration lock", cause)
onError()
@@ -194,7 +211,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio
if (svrTriesRemaining == 0) {
Log.w(TAG, "Account locked. User out of attempts on KBS.")
onAccountLocked()
navigateToAccountLocked()
return
}
@@ -227,10 +244,6 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio
.show()
}
private fun onKbsAccountLocked() {
onAccountLocked()
}
fun onError() {
binding.kbsLockPinConfirm.cancelSpinning()
enableAndFocusPinEntry()
@@ -252,10 +265,6 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio
return TimeUnit.MILLISECONDS.toDays(timeRemainingMs).toInt() + 1
}
private fun onAccountLocked() {
navigateToAccountLocked()
}
private fun getTriesRemainingDialogMessage(triesRemaining: Int, daysRemaining: Int): String {
val resources = requireContext().resources
val tries = resources.getQuantityString(R.plurals.RegistrationLockFragment__you_have_d_attempts_remaining, triesRemaining, triesRemaining)
@@ -316,7 +325,7 @@ class RegistrationLockV2Fragment : LoggingFragment(R.layout.fragment_registratio
stopwatch.stop(TAG)
null
}, { none: Any? ->
}, {
binding.kbsLockPinConfirm.cancelSpinning()
findNavController().safeNavigate(RegistrationLockV2FragmentDirections.actionSuccessfulRegistration())
})

View File

@@ -5,7 +5,6 @@
package org.thoughtcrime.securesms.registration.v2.ui.welcome
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Bundle
@@ -30,9 +29,7 @@ import org.thoughtcrime.securesms.restore.RestoreActivity
import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import kotlin.jvm.optionals.getOrNull
/**
* First screen that is displayed on the very first app launch.
@@ -57,7 +54,6 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
maybePrefillE164()
setDebugLogSubmitMultiTapView(binding.image)
setDebugLogSubmitMultiTapView(binding.title)
binding.welcomeContinueButton.setOnClickListener { onContinueClicked() }
@@ -70,7 +66,8 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) {
findNavController().safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.CONTINUE))
} else {
skipRestore()
sharedViewModel.maybePrefillE164(requireContext())
findNavController().safeNavigate(WelcomeV2FragmentDirections.actionSkipRestore())
}
}
@@ -79,10 +76,6 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
return WelcomePermissions.getWelcomePermissions(isUserSelectionRequired).all { ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED }
}
private fun skipRestore() {
findNavController().safeNavigate(WelcomeV2FragmentDirections.actionSkipRestore())
}
private fun onTermsClicked() {
CommunicationActions.openBrowserLink(requireContext(), TERMS_AND_CONDITIONS_URL)
}
@@ -98,21 +91,6 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome
}
}
private fun maybePrefillE164() {
if (Permissions.hasAll(requireContext(), Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) {
val localNumber = Util.getDeviceNumber(requireContext()).getOrNull()
if (localNumber != null) {
Log.v(TAG, "Phone number detected.")
sharedViewModel.setPhoneNumber(localNumber)
} else {
Log.i(TAG, "Could not read phone number.")
}
} else {
Log.i(TAG, "No phone permission.")
}
}
companion object {
private val TAG = Log.tag(WelcomeV2Fragment::class.java)
private const val TERMS_AND_CONDITIONS_URL = "https://signal.org/legal"