diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_synchronizePniChangeNumber.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_synchronizePniChangeNumber.kt index ae0a139c5f..2e97eb6fcc 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_synchronizePniChangeNumber.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_synchronizePniChangeNumber.kt @@ -236,8 +236,9 @@ class SyncMessageProcessorTest_synchronizePniChangeNumber { } @Test - fun skipsRedeliveryWhenPniAlreadyMatches() { - sendPniChangeNumber() + fun skipsRedeliveryWithSameServerTimestamp() { + val timestamp = messageHelper.nextStartTime() + sendPniChangeNumber(timestamp = timestamp) val afterFirstApply = captureOriginalState() val otherIdentity = IdentityKeyPair.generate() @@ -247,12 +248,41 @@ class SyncMessageProcessorTest_synchronizePniChangeNumber { identityKeyPair = otherIdentity.serialize().toByteString(), signedPreKey = otherSignedPreKey.serialize().toByteString(), e164 = "+15555550100", - timestamp = messageHelper.nextStartTime() + 1000 + timestamp = timestamp ) assertOriginalStatePreserved(afterFirstApply) } + @Test + fun reappliesWhenServerTimestampIsNewer() { + sendPniChangeNumber() + + val secondPniUuid = UUID.randomUUID() + val secondPni = ServiceId.PNI.from(secondPniUuid) + val secondPniBytes = UuidUtil.toByteArray(secondPniUuid).toByteString() + val secondIdentity = IdentityKeyPair.generate() + val secondSignedPreKey = PreKeyUtil.generateSignedPreKey(9999, secondIdentity.privateKey) + val secondE164 = "+15555550100" + val secondRegistrationId = 7777 + + sendPniChangeNumber( + identityKeyPair = secondIdentity.serialize().toByteString(), + signedPreKey = secondSignedPreKey.serialize().toByteString(), + lastResortKyberPreKey = null, + registrationId = secondRegistrationId, + e164 = secondE164, + envelopePniBinary = secondPniBytes, + timestamp = messageHelper.nextStartTime() + 1000 + ) + + assertThat(SignalStore.account.e164).isEqualTo(secondE164) + assertThat(SignalStore.account.pni).isEqualTo(secondPni) + assertThat(SignalStore.account.pniRegistrationId).isEqualTo(secondRegistrationId) + assertThat(SignalStore.account.pniIdentityKey.publicKey.serialize().toByteString()) + .isEqualTo(secondIdentity.publicKey.serialize().toByteString()) + } + @Test fun bailsWhenServerTimestampStale() { sendPniChangeNumber() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt index f05044acc2..44889c6246 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberEnterCodeFragment.kt @@ -128,11 +128,14 @@ class ChangeNumberEnterCodeFragment : LoggingFragment(R.layout.fragment_change_n binding.codeEntryLayout.resendSmsCountDown.startCountDownTo(state.nextSmsTimestamp.milliseconds) binding.codeEntryLayout.callMeCountDown.startCountDownTo(state.nextCallTimestamp.milliseconds) when (val outcome = state.changeNumberOutcome) { - is ChangeNumberOutcome.RecoveryPasswordWorked, - is ChangeNumberOutcome.VerificationCodeWorked -> changeNumberSuccess() + is ChangeNumberOutcome.Succeeded -> changeNumberSuccess() is ChangeNumberOutcome.ChangeNumberRequestOutcome -> if (!state.inProgress && !outcome.result.isSuccess()) { - presentGenericError(outcome.result) + if (outcome.result is VerificationCodeRequestResult.RequestVerificationCodeRateLimited) { + Log.i(TAG, "Verification code request rate limited; staying on code entry screen.") + } else { + presentGenericError(outcome.result) + } } null -> Unit @@ -158,6 +161,8 @@ class ChangeNumberEnterCodeFragment : LoggingFragment(R.layout.fragment_change_n when (result) { is VerificationCodeRequestResult.Success -> binding.codeEntryLayout.keyboard.displaySuccess() is VerificationCodeRequestResult.RateLimited -> presentRateLimitedDialog() + is VerificationCodeRequestResult.RequestVerificationCodeRateLimited -> presentRateLimitedDialog(retryAfterSeconds = (result.nextSmsTimestamp - System.currentTimeMillis().milliseconds).inWholeSeconds.coerceAtLeast(0)) + is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> presentRateLimitedDialog() is VerificationCodeRequestResult.RegistrationLocked -> presentRegistrationLocked(result.timeRemaining) else -> presentGenericError(result) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRegistrationLockFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRegistrationLockFragment.kt index a864c67269..f8c29ac0be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRegistrationLockFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRegistrationLockFragment.kt @@ -113,7 +113,7 @@ class ChangeNumberRegistrationLockFragment : LoggingFragment(R.layout.fragment_c } private fun onStateUpdate(state: ChangeNumberState) { - if (state.changeNumberOutcome == ChangeNumberOutcome.VerificationCodeWorked) { + if (state.changeNumberOutcome == ChangeNumberOutcome.Succeeded) { handleSuccessfulPinEntry(state.enteredPin) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt index 3ef13bc8ad..1bd525abfb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt @@ -188,8 +188,7 @@ class ChangeNumberRepository( StorageSyncHelper.scheduleSyncForDataChange() } - SignalStore.account.setE164(e164) - SignalStore.account.setPni(pni) + SignalStore.account.setNumberAndPniIdentity(e164, pni, pniRegistrationId, pniIdentityKeyPair) AppDependencies.resetProtocolStores() AppDependencies.groupsV2Authorization.clear() @@ -197,9 +196,6 @@ class ChangeNumberRepository( val pniProtocolStore = AppDependencies.protocolStore.pni() val pniMetadataStore = SignalStore.account.pniPreKeys - SignalStore.account.pniRegistrationId = pniRegistrationId - SignalStore.account.setPniIdentityKeyAfterChangeNumber(pniIdentityKeyPair) - PreKeyUtil.storeSignedPreKey(pniProtocolStore, pniMetadataStore, pniSignedPreKey) pniMetadataStore.activeSignedPreKeyId = pniSignedPreKey.id @@ -326,6 +322,10 @@ class ChangeNumberRepository( } } + if (result !is NetworkResult.Success) { + result = verifyChangeAppliedDespiteError(newE164 = newE164, originalResult = result) + } + if (result is NetworkResult.StatusCodeError) { SignalStore.misc.unlockChangeNumber() } @@ -336,13 +336,39 @@ class ChangeNumberRepository( NumberChangeResult( uuid = accountRegistrationResponse.uuid, pni = accountRegistrationResponse.pni, - storageCapable = accountRegistrationResponse.storageCapable, number = accountRegistrationResponse.number ) } ) } + /** + * The server can commit a number change but still return an error. Before trusting a non-success result, check + * with the server and see if it already reports [newE164] as our number. If it does, then the change actually + * went through, so treat it as a success instead of surfacing the error. + */ + private suspend fun verifyChangeAppliedDespiteError(newE164: String, originalResult: NetworkResult): NetworkResult { + return try { + val whoAmI = withContext(Dispatchers.IO) { whoAmI() } + if (whoAmI.number == newE164 && whoAmI.pni != null) { + Log.w(TAG, "Change number request did not succeed, but whoami reports the new number is already active. Treating the change as successful.") + NetworkResult.Success( + VerifyAccountResponse().apply { + uuid = whoAmI.aci + pni = whoAmI.pni + number = whoAmI.number + } + ) + } else { + Log.i(TAG, "Change number request did not succeed and whoami does not report the new number; treating as a genuine failure.") + originalResult + } + } catch (e: IOException) { + Log.w(TAG, "Failed to query whoami while verifying change number outcome; treating as a genuine failure.", e) + originalResult + } + } + @WorkerThread private fun createChangeNumberRequest( sessionId: String? = null, @@ -433,7 +459,6 @@ class ChangeNumberRepository( data class NumberChangeResult( val uuid: String, val pni: String, - val storageCapable: Boolean, val number: String ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberState.kt index 3c68ad863c..8e3fd6890d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberState.kt @@ -43,8 +43,7 @@ data class ChangeNumberState( ) sealed interface ChangeNumberOutcome { - data object RecoveryPasswordWorked : ChangeNumberOutcome - data object VerificationCodeWorked : ChangeNumberOutcome + data object Succeeded : ChangeNumberOutcome class ChangeNumberRequestOutcome(val result: VerificationCodeRequestResult) : ChangeNumberOutcome } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt index ed7aefe8dd..5ccff569d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt @@ -89,8 +89,8 @@ class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phon private fun handleRequestCodeResult(changeNumberOutcome: ChangeNumberOutcome) { Log.d(TAG, "Handling request code result: ${changeNumberOutcome.javaClass.name}") when (changeNumberOutcome) { - is ChangeNumberOutcome.RecoveryPasswordWorked -> { - Log.i(TAG, "Successfully changed number with recovery password.") + is ChangeNumberOutcome.Succeeded -> { + Log.i(TAG, "Successfully changed number.") changeNumberSuccess() } @@ -109,11 +109,27 @@ class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phon } } - is VerificationCodeRequestResult.RateLimited -> { + is VerificationCodeRequestResult.RequestVerificationCodeRateLimited -> { + if (castResult.willBeAbleToRequestAgain) { + Log.i(TAG, "Verification code request rate limited; proceeding to code entry screen so the user can wait/resend rather than bailing.") + findNavController().safeNavigate(ChangeNumberVerifyFragmentDirections.actionChangePhoneNumberVerifyFragmentToChangeNumberEnterCodeFragment()) + } else { + Log.i(TAG, "Verification code request rate limited with no pending resend; showing rate limit error.") + showErrorDialog(R.string.RegistrationActivity_rate_limited_to_service) + } + } + + is VerificationCodeRequestResult.RateLimited, + is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited -> { Log.i(TAG, "Unable to request sms code due to rate limit") showErrorDialog(R.string.RegistrationActivity_rate_limited_to_service) } + is VerificationCodeRequestResult.RegistrationLocked -> { + Log.i(TAG, "Destination number is registration locked; navigating to PIN entry.") + findNavController().safeNavigate(ChangeNumberVerifyFragmentDirections.actionChangePhoneNumberVerifyFragmentToChangeNumberRegistrationLock(castResult.timeRemaining)) + } + is VerificationCodeRequestResult.TokenNotAccepted -> { Log.i(TAG, "Token was not accepted.") showErrorDialog(R.string.RegistrationActivity_additional_verification_required) @@ -125,11 +141,6 @@ class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phon } } } - - is ChangeNumberOutcome.VerificationCodeWorked -> { - Log.i(TAG, "Successfully changed number with verification code.") - changeNumberSuccess() - } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt index 44cbb0c9c0..d02435ed97 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt @@ -12,14 +12,12 @@ import androidx.lifecycle.viewModelScope import com.google.i18n.phonenumbers.NumberParseException import com.google.i18n.phonenumbers.PhoneNumberUtil import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.signal.core.models.ServiceId -import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.keyvalue.SignalStore @@ -36,6 +34,7 @@ import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet import org.thoughtcrime.securesms.util.dualsim.MccMncProducer import java.io.IOException +import java.util.concurrent.atomic.AtomicBoolean /** * [ViewModel] for the change number flow. @@ -50,12 +49,13 @@ class ChangeNumberViewModel : ViewModel() { private val repository = ChangeNumberRepository() private val store = MutableStateFlow(ChangeNumberState()) - private val serialContext = SignalExecutors.SERIAL.asCoroutineDispatcher() private val smsRetrieverReceiver: SmsRetrieverReceiver = SmsRetrieverReceiver(AppDependencies.application) private val initialLocalNumber = SignalStore.account.e164 private val password = SignalStore.account.servicePassword!! + private val changeNumberSessionInFlight = AtomicBoolean(false) + val uiState = store.asLiveData() val liveOldNumberState = store.map { it.oldPhoneNumber }.asLiveData() val liveNewNumberState = store.map { it.number }.asLiveData() @@ -261,14 +261,24 @@ class ChangeNumberViewModel : ViewModel() { } private suspend fun verifyCodeInternal(context: Context, pin: String?, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) { - val sessionId = getOrCreateValidSession(context)?.sessionId ?: return bail { Log.i(TAG, "Bailing from code verification due to invalid session.") } - val registrationData = getRegistrationData(context) + val session = getOrCreateValidSession(context) ?: return bail { Log.i(TAG, "Bailing from code verification due to invalid session.") } + val sessionId = session.sessionId - val verificationResponse = RegistrationRepository.submitVerificationCode(context, sessionId, registrationData) + if (!session.verified) { + if (store.value.enteredCode == null) { + Log.w(TAG, "Session is not verified and no code is available to submit; cannot complete change number.") + handleVerificationError(VerificationCodeRequestResult.UnknownError(IllegalStateException("No verification code available for an unverified session")), verificationErrorHandler) + return bail { Log.i(TAG, "Bailing from code verification due to missing code for unverified session.") } + } - if (verificationResponse !is VerificationCodeRequestResult.Success && verificationResponse !is VerificationCodeRequestResult.AlreadyVerified) { - handleVerificationError(verificationResponse, verificationErrorHandler) - return bail { Log.i(TAG, "Bailing from code verification due to non-successful response.") } + val registrationData = getRegistrationData(context) + + val verificationResponse = RegistrationRepository.submitVerificationCode(context, sessionId, registrationData) + + if (verificationResponse !is VerificationCodeRequestResult.Success && verificationResponse !is VerificationCodeRequestResult.AlreadyVerified) { + handleVerificationError(verificationResponse, verificationErrorHandler) + return bail { Log.i(TAG, "Bailing from code verification due to non-successful response.") } + } } val result: ChangeNumberResult = if (pin == null) { @@ -286,7 +296,7 @@ class ChangeNumberViewModel : ViewModel() { } if (result is ChangeNumberResult.Success) { - handleSuccessfulChangedRemoteNumber(e164 = result.numberChangeResult.number, pni = ServiceId.PNI.parseOrThrow(result.numberChangeResult.pni), changeNumberOutcome = ChangeNumberOutcome.RecoveryPasswordWorked) + handleSuccessfulChangedRemoteNumber(e164 = result.numberChangeResult.number, pni = ServiceId.PNI.parseOrThrow(result.numberChangeResult.pni), changeNumberOutcome = ChangeNumberOutcome.Succeeded) } else { handleChangeNumberError(result, numberChangeErrorHandler) } @@ -359,18 +369,26 @@ class ChangeNumberViewModel : ViewModel() { fun initiateChangeNumberSession(context: Context, mode: RegistrationRepository.E164VerificationMode) { Log.v(TAG, "changeNumber()") + if (!changeNumberSessionInFlight.compareAndSet(false, true)) { + Log.i(TAG, "A change number session is already in progress; ignoring duplicate request.") + return + } store.update { it.copy(inProgress = true) } viewModelScope.launch(Dispatchers.IO) { - val encryptionDrained = repository.ensureDecryptionsDrained() ?: false + try { + val encryptionDrained = repository.ensureDecryptionsDrained() ?: false - if (!encryptionDrained) { - return@launch bail { Log.i(TAG, "Failed to drain encryption.") } - } + if (!encryptionDrained) { + return@launch bail { Log.i(TAG, "Failed to drain encryption.") } + } - when (changeNumberWithRecoveryPassword()) { - ChangeLocalNumberOutcome.NotPerformed -> requestVerificationCode(context, mode) - ChangeLocalNumberOutcome.Success -> Log.d(TAG, "Successfully changed number using recovery password") - ChangeLocalNumberOutcome.Failure -> Log.w(TAG, "Change number failed, bailing") + when (changeNumberWithRecoveryPassword()) { + ChangeLocalNumberOutcome.NotPerformed -> requestVerificationCode(context, mode) + ChangeLocalNumberOutcome.Success -> Log.d(TAG, "Successfully changed number using recovery password") + ChangeLocalNumberOutcome.Failure -> Log.w(TAG, "Change number failed, bailing") + } + } finally { + changeNumberSessionInFlight.set(false) } } } @@ -417,7 +435,7 @@ class ChangeNumberViewModel : ViewModel() { val result = repository.changeNumberWithRecoveryPassword(recoveryPassword = recoveryPassword, newE164 = number.e164Number) if (result is ChangeNumberResult.Success) { - return handleSuccessfulChangedRemoteNumber(e164 = result.numberChangeResult.number, pni = ServiceId.PNI.parseOrThrow(result.numberChangeResult.pni), changeNumberOutcome = ChangeNumberOutcome.RecoveryPasswordWorked) + return handleSuccessfulChangedRemoteNumber(e164 = result.numberChangeResult.number, pni = ServiceId.PNI.parseOrThrow(result.numberChangeResult.pni), changeNumberOutcome = ChangeNumberOutcome.Succeeded) } else if (result is ChangeNumberResult.UnknownError) { store.update { it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(VerificationCodeRequestResult.UnknownError(result.getCause()))) @@ -512,6 +530,12 @@ class ChangeNumberViewModel : ViewModel() { return } + if (validSession.verified) { + Log.i(TAG, "Existing session is already verified; completing the change with the verified session instead of requesting a new code.") + changeNumberWithVerifiedSession(validSession.sessionId) + return + } + val result = if (!validSession.allowedToRequestCode) { val challenges = validSession.challengesRequested.joinToString() Log.i(TAG, "Not allowed to request code! Remaining challenges: $challenges") @@ -532,14 +556,72 @@ class ChangeNumberViewModel : ViewModel() { } if (result is VerificationCodeRequestResult.AlreadyVerified) { - Log.w(TAG, "Already verified, not handled properly in change number flow, attempt to recover") - resetLocalSessionState() + Log.i(TAG, "Session became verified while requesting a code; completing the change with the verified session.") + changeNumberWithVerifiedSession(validSession.sessionId) + return } Log.d(TAG, "Received result: ${result.javaClass.canonicalName}\nwith challenges: ${challengesRequested.joinToString { it.key }}") + val (nextSmsTimestamp, nextCallTimestamp) = when (result) { + is VerificationCodeRequestResult.Success -> result.nextSmsTimestamp.inWholeMilliseconds to result.nextCallTimestamp.inWholeMilliseconds + is VerificationCodeRequestResult.RequestVerificationCodeRateLimited -> result.nextSmsTimestamp.inWholeMilliseconds to result.nextCallTimestamp.inWholeMilliseconds + else -> store.value.nextSmsTimestamp to store.value.nextCallTimestamp + } + store.update { - it.copy(changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(result), challengesRequested = challengesRequested, inProgress = false) + it.copy( + changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(result), + challengesRequested = challengesRequested, + inProgress = false, + nextSmsTimestamp = nextSmsTimestamp, + nextCallTimestamp = nextCallTimestamp + ) + } + } + + /** + * Completes a number change using a registration session that the service already considers verified. + */ + private suspend fun changeNumberWithVerifiedSession(sessionId: String) { + Log.v(TAG, "changeNumberWithVerifiedSession()") + when (val result = repository.changeNumberWithoutRegistrationLock(sessionId = sessionId, newE164 = number.e164Number)) { + is ChangeNumberResult.Success -> { + handleSuccessfulChangedRemoteNumber( + e164 = result.numberChangeResult.number, + pni = ServiceId.PNI.parseOrThrow(result.numberChangeResult.pni), + changeNumberOutcome = ChangeNumberOutcome.Succeeded + ) + } + + is ChangeNumberResult.RegistrationLocked -> { + if (result.svr2Credentials != null) { + Log.i(TAG, "Destination number is registration locked; prompting for PIN.") + store.update { + it.copy( + inProgress = false, + svr2Credentials = result.svr2Credentials, + svr3Credentials = result.svr3Credentials, + lockedTimeRemaining = result.timeRemaining, + changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome( + VerificationCodeRequestResult.RegistrationLocked(result.getCause(), result.timeRemaining, result.svr2Credentials, result.svr3Credentials) + ) + ) + } + } else { + Log.w(TAG, "Destination number is registration locked but SVR credentials were missing, cannot prompt for PIN.") + store.update { + it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(VerificationCodeRequestResult.UnknownError(result.getCause()))) + } + } + } + + else -> { + Log.w(TAG, "Unable to complete change number with verified session.", result.getCause()) + store.update { + it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(VerificationCodeRequestResult.UnknownError(result.getCause()))) + } + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt index 8120e3d3a4..8998dfff90 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt @@ -327,15 +327,17 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context) } } - /** Set an identity key pair for the PNI identity via change number. */ - fun setPniIdentityKeyAfterChangeNumber(key: IdentityKeyPair) { + fun setNumberAndPniIdentity(e164: String, pni: PNI, pniRegistrationId: Int, pniIdentityKeyPair: IdentityKeyPair) { synchronized(this) { - Log.i(TAG, "Setting a new PNI identity key pair.") + Log.i(TAG, "Setting the E164, PNI, PNI registration ID, and PNI identity key pair.") store .beginWrite() - .putBlob(KEY_PNI_IDENTITY_PUBLIC_KEY, key.publicKey.serialize()) - .putBlob(KEY_PNI_IDENTITY_PRIVATE_KEY, key.privateKey.serialize()) + .putString(KEY_E164, e164) + .putString(KEY_PNI, pni.toString()) + .putInteger(KEY_PNI_REGISTRATION_ID, pniRegistrationId) + .putBlob(KEY_PNI_IDENTITY_PUBLIC_KEY, pniIdentityKeyPair.publicKey.serialize()) + .putBlob(KEY_PNI_IDENTITY_PRIVATE_KEY, pniIdentityKeyPair.privateKey.serialize()) .commit() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt index a443f00d6b..40e54a00e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -1896,11 +1896,6 @@ object SyncMessageProcessor { return } - if (SignalStore.account.pni == PNI(pni)) { - log(timestamp, "PniChangeNumber sync already applied locally. Skipping.") - return - } - val identityKeyPairBytes = pniChangeNumber.identityKeyPair val signedPreKeyBytes = pniChangeNumber.signedPreKey val registrationId = pniChangeNumber.registrationId @@ -1934,8 +1929,6 @@ object SyncMessageProcessor { pniRegistrationId = registrationId ) - SignalStore.misc.lastAppliedPniChangeServerTimestamp = envelopeServerTimestamp - // The primary already submitted these per-device prekeys to the server as part of the // change-number request, so they are registered server-side from this device's perspective. val pniMetadataStore = SignalStore.account.pniPreKeys @@ -1947,6 +1940,8 @@ object SyncMessageProcessor { // Rotate the primary-generated keys as soon as possible so we don't rely on them long-term. SignalStore.misc.forcePniSignedPreKeyRotation = true AppDependencies.jobManager.add(PreKeysSyncJob.create(forceRotationRequested = true)) + + SignalStore.misc.lastAppliedPniChangeServerTimestamp = envelopeServerTimestamp } private fun applyAttachmentData( diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt index 44e140291a..3005d99592 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/network/VerificationCodeRequestResult.kt @@ -134,7 +134,7 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu class SubmitVerificationCodeRateLimited(cause: Throwable) : VerificationCodeRequestResult(cause) - class RegistrationLocked(cause: Throwable, val timeRemaining: Long, val svr2Credentials: AuthCredentials, val svr3Credentials: Svr3Credentials) : VerificationCodeRequestResult(cause) + class RegistrationLocked(cause: Throwable, val timeRemaining: Long, val svr2Credentials: AuthCredentials, val svr3Credentials: Svr3Credentials?) : VerificationCodeRequestResult(cause) class NoSuchSession(cause: Throwable) : VerificationCodeRequestResult(cause) diff --git a/app/src/main/res/navigation/app_settings_change_number.xml b/app/src/main/res/navigation/app_settings_change_number.xml index c473e2520b..a2695b37f5 100644 --- a/app/src/main/res/navigation/app_settings_change_number.xml +++ b/app/src/main/res/navigation/app_settings_change_number.xml @@ -95,6 +95,15 @@ app:popEnterAnim="@anim/fragment_close_enter" app:popExitAnim="@anim/fragment_close_exit" app:popUpTo="@+id/enterPhoneNumberChangeFragment" /> + +