mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-04 20:34:14 +01:00
Harden and improve change number flow.
This commit is contained in:
committed by
Michelle Tang
parent
c1c6db2b3b
commit
ee11d1fd3b
+33
-3
@@ -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()
|
||||
|
||||
+8
-3
@@ -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)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+32
-7
@@ -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<VerifyAccountResponse>): NetworkResult<VerifyAccountResponse> {
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
+1
-2
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+19
-8
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+104
-22
@@ -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())))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
|
||||
@@ -95,6 +95,15 @@
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@+id/enterPhoneNumberChangeFragment" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_changePhoneNumberVerifyFragment_to_changeNumberRegistrationLock"
|
||||
app:destination="@id/changeNumberRegistrationLock"
|
||||
app:enterAnim="@anim/fragment_open_enter"
|
||||
app:exitAnim="@anim/fragment_open_exit"
|
||||
app:popEnterAnim="@anim/fragment_close_enter"
|
||||
app:popExitAnim="@anim/fragment_close_exit"
|
||||
app:popUpTo="@+id/enterPhoneNumberChangeFragment" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
|
||||
Reference in New Issue
Block a user