diff --git a/app/src/main/java/org/thoughtcrime/securesms/absbackup/SignalBackupAgent.kt b/app/src/main/java/org/thoughtcrime/securesms/absbackup/SignalBackupAgent.kt index 2b4692b95f..43bb72f15e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/absbackup/SignalBackupAgent.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/absbackup/SignalBackupAgent.kt @@ -54,6 +54,7 @@ class SignalBackupAgent : BackupAgent() { items.find { dataInput.key == it.getKey() }?.restoreData(buffer) } DataOutputStream(FileOutputStream(newState.fileDescriptor)).use { it.writeInt(cumulativeHashCode()) } + Log.i(TAG, "Android Backup Service complete.") } private fun cumulativeHashCode(): Int { diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java index 4fd2ef27ef..0787c666f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java @@ -271,13 +271,6 @@ public class FullBackupImporter extends FullBackupBase { return; } - if (FeatureFlags.registrationV2()) { - if (SignalStore.account().getKeysToIncludeInBackup().contains(keyValue.key)) { - Log.i(TAG, "[regv2] skipping restore of " + keyValue.key); - return; - } - } - if (keyValue.blobValue != null) { dataSet.putBlob(keyValue.key, keyValue.blobValue.toByteArray()); } else if (keyValue.booleanValue != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java index f0236ed9ee..c1e33ba60d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java @@ -217,6 +217,6 @@ public final class InternalValues extends SignalStoreValues { } public boolean enterRestoreV2Flow() { - return FeatureFlags.registrationV2() && getBoolean(FORCE_ENTER_RESTORE_V2_FLOW, false); + return FeatureFlags.restoreAfterRegistration() && getBoolean(FORCE_ENTER_RESTORE_V2_FLOW, false); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java index 41f3c5ef6d..011e76c8e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java @@ -72,7 +72,7 @@ public class PinRestoreEntryFragment extends LoggingFragment { RegistrationViewDelegate.setDebugLogSubmitMultiTapView(root.findViewById(R.id.pin_restore_pin_title)); pinEntry = root.findViewById(R.id.pin_restore_pin_input); - pinButton = root.findViewById(R.id.pin_restore_pin_confirm); + pinButton = root.findViewById(R.id.pin_restore_pin_continue); errorLabel = root.findViewById(R.id.pin_restore_pin_input_label); keyboardToggle = root.findViewById(R.id.pin_restore_keyboard_toggle); helpButton = root.findViewById(R.id.pin_restore_forgot_pin); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt index 16e4cb8302..391c5ec7a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt @@ -207,7 +207,7 @@ class VerifyAccountRepository(private val context: Application) { }.subscribeOn(Schedulers.io()) } - interface MasterKeyProducer { + fun interface MasterKeyProducer { @Throws(IOException::class, SvrWrongPinException::class, SvrNoDataException::class) fun produceMasterKey(): MasterKey } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt index 0307823580..b043ae431b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt @@ -5,6 +5,7 @@ package org.thoughtcrime.securesms.registration.v2.data +import android.app.backup.BackupManager import android.content.Context import androidx.annotation.WorkerThread import androidx.core.app.NotificationManagerCompat @@ -12,6 +13,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe +import org.signal.core.util.Base64 import org.signal.core.util.logging.Log import org.signal.libsignal.protocol.IdentityKeyPair import org.signal.libsignal.protocol.util.KeyHelper @@ -39,6 +41,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.registration.PushChallengeRequest import org.thoughtcrime.securesms.registration.RegistrationData import org.thoughtcrime.securesms.registration.VerifyAccountRepository +import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet import org.thoughtcrime.securesms.service.DirectoryRefreshListener import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener import org.thoughtcrime.securesms.util.TextSecurePreferences @@ -47,13 +50,16 @@ import org.whispersystems.signalservice.api.account.AccountAttributes import org.whispersystems.signalservice.api.account.PreKeyCollection import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess import org.whispersystems.signalservice.api.kbs.MasterKey +import org.whispersystems.signalservice.api.kbs.PinHashUtil import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.push.ServiceId.ACI import org.whispersystems.signalservice.api.push.ServiceId.PNI import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.api.registration.RegistrationApi +import org.whispersystems.signalservice.internal.push.AuthCredentials import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataHeaders import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse +import java.nio.charset.StandardCharsets import java.util.Locale import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -227,6 +233,24 @@ object RegistrationRepository { metadataStore.lastResortKyberPreKeyRotationTime = System.currentTimeMillis() } + fun canUseLocalRecoveryPassword(): Boolean { + val recoveryPassword = SignalStore.svr().recoveryPassword + val pinHash = SignalStore.svr().localPinHash + return recoveryPassword != null && pinHash != null + } + + fun doesPinMatchLocalHash(pin: String): Boolean { + val pinHash = SignalStore.svr().localPinHash ?: throw IllegalStateException("Local PIN hash is not present!") + return PinHashUtil.verifyLocalPinHash(pinHash, pin) + } + + suspend fun fetchMasterKeyFromSvrRemote(pin: String, authCredentials: AuthCredentials): MasterKey = + withContext(Dispatchers.IO) { + val masterKey = SvrRepository.restoreMasterKeyPreRegistration(SvrAuthCredentialSet(null, authCredentials), pin) + SignalStore.svr().setMasterKey(masterKey, pin) + return@withContext masterKey + } + /** * Asks the service to send a verification code through one of our supported channels (SMS, phone call). * This requires two or more network calls: @@ -280,9 +304,9 @@ object RegistrationRepository { /** * Submit the necessary assets as a verified account so that the user can actually use the service. */ - suspend fun registerAccount(context: Context, e164: String, password: String, sessionId: String, registrationData: RegistrationData, pin: String? = null, masterKeyProducer: VerifyAccountRepository.MasterKeyProducer? = null): NetworkResult = + suspend fun registerAccount(context: Context, sessionId: String?, registrationData: RegistrationData, pin: String? = null, masterKeyProducer: VerifyAccountRepository.MasterKeyProducer? = null): NetworkResult = withContext(Dispatchers.IO) { - val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi + val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, registrationData.e164, SignalServiceAddress.DEFAULT_DEVICE_ID, registrationData.password).registrationApi val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context) val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey) @@ -335,27 +359,32 @@ object RegistrationRepository { val eventBus = EventBus.getDefault() eventBus.register(subscriber) - val sessionCreationResponse = accountManager.createRegistrationSession(fcmToken, mcc, mnc) - if (sessionCreationResponse !is NetworkResult.Success) { - return@withContext sessionCreationResponse - } - - val receivedPush = subscriber.latch.await(PUSH_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS) - eventBus.unregister(subscriber) - - if (receivedPush) { - val challenge = subscriber.challenge - if (challenge != null) { - Log.w(TAG, "Push challenge token received.") - return@withContext accountManager.submitPushChallengeToken(sessionCreationResponse.result.body.id, challenge) - } else { - Log.w(TAG, "Push received but challenge token was null.") + try { + val sessionCreationResponse = accountManager.createRegistrationSession(fcmToken, mcc, mnc) + if (sessionCreationResponse !is NetworkResult.Success) { + return@withContext sessionCreationResponse } - } else { - Log.i(TAG, "Push challenge timed out.") + + val receivedPush = subscriber.latch.await(PUSH_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS) + eventBus.unregister(subscriber) + + if (receivedPush) { + val challenge = subscriber.challenge + if (challenge != null) { + Log.w(TAG, "Push challenge token received.") + return@withContext accountManager.submitPushChallengeToken(sessionCreationResponse.result.body.id, challenge) + } else { + Log.w(TAG, "Push received but challenge token was null.") + } + } else { + Log.i(TAG, "Push challenge timed out.") + } + Log.i(TAG, "Push challenge unsuccessful. Updating registration state accordingly.") + return@withContext NetworkResult.ApplicationError(NullPointerException()) + } catch (ex: Exception) { + Log.w(TAG, "Exception caught, but the earlier try block should have caught it?", ex) // TODO [regv2]: figure out why this exception is not caught + return@withContext NetworkResult.ApplicationError(ex) } - Log.i(TAG, "Push challenge unsuccessful. Updating registration state accordingly.") - return@withContext NetworkResult.ApplicationError(NullPointerException()) } @JvmStatic @@ -368,6 +397,39 @@ object RegistrationRepository { return timestamp + deltaSeconds.seconds.inWholeMilliseconds } + suspend fun hasValidSvrAuthCredentials(context: Context, e164: String, password: String): AuthCredentials? = + withContext(Dispatchers.IO) { + val usernamePasswords = SignalStore.svr() + .authTokenList + .take(10) + .map { + it.replace("Basic ", "").trim() + } + .map { + Base64.decode(it) // TODO [regv2]: figure out why Android Studio doesn't like mapCatching + } + .map { + String(it, StandardCharsets.ISO_8859_1) + } + + if (usernamePasswords.isEmpty()) { + return@withContext null + } + val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi + + val authCheck = api.getSvrAuthCredential(e164, usernamePasswords) + if (authCheck !is NetworkResult.Success) { + return@withContext null + } + + val removedInvalidTokens = SignalStore.svr().removeAuthTokens(authCheck.result.invalid) + if (removedInvalidTokens) { + BackupManager(context).dataChanged() + } + + return@withContext authCheck.result.match + } + enum class Mode(val isSmsRetrieverSupported: Boolean) { SMS_WITH_LISTENER(true), SMS_WITHOUT_LISTENER(false), PHONE_CALL(false) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationCheckpoint.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationCheckpoint.kt index 1af548b418..f679d3fcd0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationCheckpoint.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationCheckpoint.kt @@ -17,6 +17,7 @@ enum class RegistrationCheckpoint { BACKUP_RESTORED, PUSH_NETWORK_AUDITED, PHONE_NUMBER_CONFIRMED, + PIN_CONFIRMED, VERIFICATION_CODE_REQUESTED, CHALLENGE_RECEIVED, CHALLENGE_COMPLETED, diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt index 032da33b02..163b4e4691 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt @@ -9,9 +9,16 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.viewModels +import androidx.navigation.ActivityNavigator import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.BaseActivity +import org.thoughtcrime.securesms.MainActivity import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity +import org.thoughtcrime.securesms.profiles.AvatarHelper +import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity +import org.thoughtcrime.securesms.recipients.Recipient /** * Activity to hold the entire registration process. @@ -25,6 +32,43 @@ class RegistrationV2Activity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_registration_navigation_v2) + sharedViewModel.uiState.observe(this) { + if (it.registrationCheckpoint == RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED) { + handleSuccessfulVerify() + } + } + } + + private fun handleSuccessfulVerify() { + // TODO [regv2]: add functionality of [RegistrationCompleteFragment] + val isProfileNameEmpty = Recipient.self().profileName.isEmpty + val isAvatarEmpty = !AvatarHelper.hasAvatar(this, Recipient.self().id) + val needsProfile = isProfileNameEmpty || isAvatarEmpty + val needsPin = !sharedViewModel.hasPin() + + Log.i(TAG, "Pin restore flow not required. Profile name: $isProfileNameEmpty | Profile avatar: $isAvatarEmpty | Needs PIN: $needsPin") + + SignalStore.internalValues().setForceEnterRestoreV2Flow(true) + + if (!needsProfile && !needsPin) { + sharedViewModel.completeRegistration() + } + sharedViewModel.setInProgress(false) + + val startIntent = MainActivity.clearTop(this).apply { + if (needsPin) { + putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(this@RegistrationV2Activity)) + } + + if (needsProfile) { + putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(this@RegistrationV2Activity)) + } + } + + Log.d(TAG, "Launching ${startIntent.component}") + startActivity(startIntent) + finish() + ActivityNavigator.applyPopAnimationsToPendingTransition(this) } companion object { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt index 6eba43dcd8..e39cb5697a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt @@ -6,6 +6,8 @@ package org.thoughtcrime.securesms.registration.v2.ui import com.google.i18n.phonenumbers.Phonenumber +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.whispersystems.signalservice.internal.push.AuthCredentials /** * State holder shared across all of registration. @@ -15,10 +17,16 @@ data class RegistrationV2State( val phoneNumber: Phonenumber.PhoneNumber? = null, val inProgress: Boolean = false, val isReRegister: Boolean = false, + val recoveryPassword: String? = SignalStore.svr().getRecoveryPassword(), val canSkipSms: Boolean = false, + val svrAuthCredentials: AuthCredentials? = null, + val svrTriesRemaining: Int = 10, + val isRegistrationLockEnabled: Boolean = false, + val userSkippedReregistration: Boolean = false, val isFcmSupported: Boolean = false, val fcmToken: String? = null, val nextSms: Long = 0L, val nextCall: Long = 0L, - val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION + val registrationCheckpoint: RegistrationCheckpoint = RegistrationCheckpoint.INITIALIZATION, + val networkError: Throwable? = null ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt index 8d75e5cf7f..4229b17622 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt @@ -23,12 +23,17 @@ 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.pin.SvrWrongPinException import org.thoughtcrime.securesms.registration.RegistrationData import org.thoughtcrime.securesms.registration.RegistrationUtil import org.thoughtcrime.securesms.registration.v2.data.RegistrationRepository import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.dualsim.MccMncProducer +import org.whispersystems.signalservice.api.NetworkResult +import org.whispersystems.signalservice.api.SvrNoDataException +import org.whispersystems.signalservice.api.kbs.MasterKey +import org.whispersystems.signalservice.internal.push.LockedException import java.io.IOException /** @@ -78,50 +83,168 @@ class RegistrationV2ViewModel : ViewModel() { viewModelScope.launch { val fcmToken = RegistrationRepository.getFcmToken(context) store.update { - it.copy( - registrationCheckpoint = RegistrationCheckpoint.PUSH_NETWORK_AUDITED, - isFcmSupported = true, - fcmToken = fcmToken - ) + it.copy(registrationCheckpoint = RegistrationCheckpoint.PUSH_NETWORK_AUDITED, isFcmSupported = true, fcmToken = fcmToken) } } } + private suspend fun updateFcmToken(context: Context): String? { + val fcmToken = RegistrationRepository.getFcmToken(context) + store.update { + it.copy(fcmToken = fcmToken) + } + return fcmToken + } + + fun onBackupSuccessfullyRestored() { + val recoveryPassword = SignalStore.svr().recoveryPassword + store.update { + it.copy(registrationCheckpoint = RegistrationCheckpoint.BACKUP_RESTORED, recoveryPassword = SignalStore.svr().recoveryPassword, canSkipSms = recoveryPassword != null) + } + } + fun onUserConfirmedPhoneNumber(context: Context) { setRegistrationCheckpoint(RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED) - // TODO [regv2]: check if can skip sms flow val state = store.value if (state.phoneNumber == null) { Log.w(TAG, "Phone number was null after confirmation.") onErrorOccurred() return } - if (state.canSkipSms) { - Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2] + + // TODO [regv2]: initialize Play Services sms retriever + val mccMncProducer = MccMncProducer(context) + val e164 = state.phoneNumber.toE164() + if (hasRecoveryPassword() && matchesSavedE164(e164)) { + // Re-registration when the local database is intact. + store.update { + it.copy(canSkipSms = true) + } } else { - // TODO [regv2]: initialize Play Services sms retriever - val mccMncProducer = MccMncProducer(context) - val e164 = state.phoneNumber.toE164() viewModelScope.launch { - val codeRequestResponse = RegistrationRepository.requestSmsCode(context, e164, password, mccMncProducer.mcc, mccMncProducer.mnc).successOrThrow() - store.update { - it.copy( - sessionId = codeRequestResponse.body.id, - nextSms = RegistrationRepository.deriveTimestamp(codeRequestResponse.headers, codeRequestResponse.body.nextSms), - nextCall = RegistrationRepository.deriveTimestamp(codeRequestResponse.headers, codeRequestResponse.body.nextCall), - registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED - ) + val svrCredentials = RegistrationRepository.hasValidSvrAuthCredentials(context, e164, password) + + if (svrCredentials != null) { + // Re-registration when credentials stored in backup. + store.update { + it.copy(canSkipSms = true, svrAuthCredentials = svrCredentials) + } + } else { + val codeRequestResponse = RegistrationRepository.requestSmsCode(context, e164, password, mccMncProducer.mcc, mccMncProducer.mnc).successOrThrow() + store.update { + it.copy(sessionId = codeRequestResponse.body.id, nextSms = RegistrationRepository.deriveTimestamp(codeRequestResponse.headers, codeRequestResponse.body.nextSms), nextCall = RegistrationRepository.deriveTimestamp(codeRequestResponse.headers, codeRequestResponse.body.nextCall), registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) + } } } } } + private fun setRecoveryPassword(recoveryPassword: String?) { + store.update { + it.copy(recoveryPassword = recoveryPassword) + } + } + + private fun updateSvrTriesRemaining(remainingTries: Int) { + store.update { + it.copy(svrTriesRemaining = remainingTries) + } + } + + fun setUserSkippedReRegisterFlow(value: Boolean) { + store.update { + it.copy(userSkippedReregistration = value, canSkipSms = !value) + } + } + + fun verifyReRegisterWithPin(context: Context, pin: String, wrongPinHandler: () -> Unit) { + setInProgress(true) + + // Local recovery password + if (RegistrationRepository.canUseLocalRecoveryPassword()) { + if (RegistrationRepository.doesPinMatchLocalHash(pin)) { + Log.d(TAG, "Found recovery password, attempting to re-register.") + viewModelScope.launch { + verifyReRegisterInternal(context, pin, SignalStore.svr().getOrCreateMasterKey()) + setInProgress(false) + } + } else { + Log.d(TAG, "Entered PIN did not match local PIN hash.") + wrongPinHandler() + setInProgress(false) + } + return + } + + // remote recovery password + val authCredentials = store.value.svrAuthCredentials + if (authCredentials != null) { + Log.d(TAG, "Found SVR auth credentials, fetching recovery password from SVR.") + viewModelScope.launch { + try { + val masterKey = RegistrationRepository.fetchMasterKeyFromSvrRemote(pin, authCredentials) + setRecoveryPassword(masterKey.deriveRegistrationRecoveryPassword()) + updateSvrTriesRemaining(10) + verifyReRegisterInternal(context, pin, masterKey) + } catch (rejectedPin: SvrWrongPinException) { + Log.w(TAG, "Submitted PIN was rejected by SVR.", rejectedPin) + updateSvrTriesRemaining(rejectedPin.triesRemaining) + wrongPinHandler() + } catch (noData: SvrNoDataException) { + Log.w(TAG, "SVR has no data for these credentials. Aborting skip SMS flow.", noData) + setUserSkippedReRegisterFlow(true) + } + setInProgress(false) + } + return + } + + Log.w(TAG, "Could not get credentials to skip SMS registration, aborting!") + // TODO [regv2]: Investigate why in v1, this case throws a [IncorrectRegistrationRecoveryPasswordException], which seems weird. + store.update { + it.copy(canSkipSms = false, inProgress = false) + } + } + + private suspend fun verifyReRegisterInternal(context: Context, pin: String, masterKey: MasterKey) { + updateFcmToken(context) + + val registrationData = getRegistrationData("") + + val resultAndRegLockStatus = registerAccountInternal(context, null, registrationData, pin, masterKey) + val result = resultAndRegLockStatus.first + val reglockEnabled = resultAndRegLockStatus.second + + if (result !is NetworkResult.Success) { + Log.w(TAG, "Error during registration!", result.getCause()) + return + } + + onSuccessfulRegistration(context, registrationData, result.result, reglockEnabled) + } + + private suspend fun registerAccountInternal(context: Context, sessionId: String?, registrationData: RegistrationData, pin: String?, masterKey: MasterKey): Pair, Boolean> { + val registrationResult = 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 NetworkResult.StatusCodeError || registrationResult.exception !is LockedException) { + return Pair(registrationResult, false) + } + + Log.i(TAG, "Received a registration lock response when trying to register an account. Retrying with master key.") + val lockedException = registrationResult.exception as LockedException + store.update { + it.copy(svrAuthCredentials = lockedException.svr2Credentials) + } + + return Pair(RegistrationRepository.registerAccount(context = context, sessionId = sessionId, registrationData = registrationData, pin = pin) { masterKey }, true) + } + fun verifyCodeWithoutRegistrationLock(context: Context, code: String) { store.update { - it.copy( - inProgress = true, - registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_ENTERED - ) + it.copy(inProgress = true, registrationCheckpoint = RegistrationCheckpoint.VERIFICATION_CODE_ENTERED) } val sessionId = store.value.sessionId @@ -143,17 +266,18 @@ class RegistrationV2ViewModel : ViewModel() { setRegistrationCheckpoint(RegistrationCheckpoint.VERIFICATION_CODE_VALIDATED) - val registrationResponse = RegistrationRepository.registerAccount(context, e164, password, sessionId, registrationData).successOrThrow() + val registrationResponse = RegistrationRepository.registerAccount(context, sessionId, registrationData).successOrThrow() + onSuccessfulRegistration(context, registrationData, registrationResponse, false) + } + } - localRegisterAccount(context, registrationData, registrationResponse, false) + private suspend fun onSuccessfulRegistration(context: Context, registrationData: RegistrationData, remoteResult: RegistrationRepository.AccountRegistrationResult, reglockEnabled: Boolean) { + RegistrationRepository.registerAccountLocally(context, registrationData, remoteResult, reglockEnabled) - refreshFeatureFlags() + refreshFeatureFlags() - store.update { - it.copy( - registrationCheckpoint = RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED - ) - } + store.update { + it.copy(registrationCheckpoint = RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED) } } @@ -162,38 +286,31 @@ class RegistrationV2ViewModel : ViewModel() { } fun completeRegistration() { - ApplicationDependencies.getJobManager() - .startChain(ProfileUploadJob()) - .then(listOf(MultiDeviceProfileKeyUpdateJob(), MultiDeviceProfileContentUpdateJob())) - .enqueue() + ApplicationDependencies.getJobManager().startChain(ProfileUploadJob()).then(listOf(MultiDeviceProfileKeyUpdateJob(), MultiDeviceProfileContentUpdateJob())).enqueue() RegistrationUtil.maybeMarkRegistrationComplete() } + private fun matchesSavedE164(e164: String?): Boolean { + return if (e164 == null) { + false + } else { + e164 == SignalStore.account().e164 + } + } + + private fun hasRecoveryPassword(): Boolean { + return store.value.recoveryPassword != null + } + private fun getCurrentE164(): String? { return store.value.phoneNumber?.toE164() } - private suspend fun localRegisterAccount( - context: Context, - registrationData: RegistrationData, - remoteResult: RegistrationRepository.AccountRegistrationResult, - reglockEnabled: Boolean - ) { - RegistrationRepository.registerAccountLocally(context, registrationData, remoteResult, reglockEnabled) - } - private suspend fun getRegistrationData(code: String): RegistrationData { - val e164: String = getCurrentE164() ?: throw IllegalStateException() - return RegistrationData( - code, - e164, - password, - RegistrationRepository.getRegistrationId(), - RegistrationRepository.getProfileKey(e164), - store.value.fcmToken, - RegistrationRepository.getPniRegistrationId(), - null // TODO [regv2]: recovery password - ) + val currentState = store.value + val e164: String = currentState.phoneNumber?.toE164() ?: throw IllegalStateException() + 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) } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt index ee0b9f2e1e..ac31b27215 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/entercode/EnterCodeV2Fragment.kt @@ -9,19 +9,12 @@ import android.os.Bundle import android.view.View import androidx.activity.OnBackPressedCallback import androidx.fragment.app.activityViewModels -import androidx.navigation.ActivityNavigator import androidx.navigation.fragment.NavHostFragment import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.LoggingFragment -import org.thoughtcrime.securesms.MainActivity import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.ViewBinderDelegate import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeV2Binding -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity -import org.thoughtcrime.securesms.profiles.AvatarHelper -import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity -import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView import org.thoughtcrime.securesms.registration.fragments.SignalStrengthPhoneStateListener import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint @@ -74,45 +67,6 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter } } } - - sharedViewModel.uiState.observe(viewLifecycleOwner) { - if (it.registrationCheckpoint == RegistrationCheckpoint.SERVICE_REGISTRATION_COMPLETED) { - handleSuccessfulVerify() - } - } - } - - private fun handleSuccessfulVerify() { - // TODO [regv2]: add functionality of [RegistrationCompleteFragment] - val activity = requireActivity() - val isProfileNameEmpty = Recipient.self().profileName.isEmpty - val isAvatarEmpty = !AvatarHelper.hasAvatar(activity, Recipient.self().id) - val needsProfile = isProfileNameEmpty || isAvatarEmpty - val needsPin = !sharedViewModel.hasPin() - - Log.i(TAG, "Pin restore flow not required. Profile name: $isProfileNameEmpty | Profile avatar: $isAvatarEmpty | Needs PIN: $needsPin") - - SignalStore.internalValues().setForceEnterRestoreV2Flow(true) - - if (!needsProfile && !needsPin) { - sharedViewModel.completeRegistration() - } - sharedViewModel.setInProgress(false) - - val startIntent = MainActivity.clearTop(activity).apply { - if (needsPin) { - putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(activity)) - } - - if (needsProfile) { - putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(activity)) - } - } - - Log.d(TAG, "Launching ${startIntent.component}") - activity.startActivity(startIntent) - activity.finish() - ActivityNavigator.applyPopAnimationsToPendingTransition(activity) } private fun popBackStack() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt index 903bd31a32..114e5ca311 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/grantpermissions/GrantPermissionsV2Fragment.kt @@ -5,15 +5,18 @@ package org.thoughtcrime.securesms.registration.v2.ui.grantpermissions +import android.app.Activity +import android.content.pm.PackageManager import android.os.Build import android.os.Bundle -import android.view.View +import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.platform.LocalContext +import androidx.core.content.ContextCompat import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.navArgs @@ -22,8 +25,8 @@ import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.registration.compose.GrantPermissionsScreen import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint -import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2State import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel +import org.thoughtcrime.securesms.restore.RestoreActivity import org.thoughtcrime.securesms.util.BackupUtil import org.thoughtcrime.securesms.util.navigation.safeNavigate @@ -33,29 +36,31 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate @RequiresApi(23) class GrantPermissionsV2Fragment : ComposeFragment() { - private val TAG = Log.tag(GrantPermissionsV2Fragment::class.java) - private val sharedViewModel by activityViewModels() private val args by navArgs() private val isSearchingForBackup = mutableStateOf(false) private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions(), - ::permissionsGranted + ::onPermissionsGranted ) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - sharedViewModel.uiState.observe(viewLifecycleOwner) { - if (it.registrationCheckpoint >= RegistrationCheckpoint.PERMISSIONS_GRANTED) { - proceedToNextScreen(it) + private val launchRestoreActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + when (val resultCode = result.resultCode) { + Activity.RESULT_OK -> { + sharedViewModel.onBackupSuccessfullyRestored() + NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsV2FragmentDirections.actionEnterPhoneNumber()) } + Activity.RESULT_CANCELED -> Log.w(TAG, "Backup restoration canceled.") + else -> Log.w(TAG, "Backup restoration activity ended with unknown result code: $resultCode") } } - private fun proceedToNextScreen(it: RegistrationV2State) { - // TODO [nicholas]: conditionally go to backup flow - NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsV2FragmentDirections.actionSkipRestore()) + private lateinit var welcomeAction: WelcomeAction + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + welcomeAction = args.welcomeAction } @Composable @@ -66,40 +71,41 @@ class GrantPermissionsV2Fragment : ComposeFragment() { deviceBuildVersion = Build.VERSION.SDK_INT, isSearchingForBackup = isSearchingForBackup, isBackupSelectionRequired = BackupUtil.isUserSelectionRequired(LocalContext.current), - onNextClicked = this::onNextClicked, - onNotNowClicked = this::onNotNowClicked + onNextClicked = this::launchPermissionRequests, + onNotNowClicked = this::proceedToNextScreen ) } - private fun onNextClicked() { - when (args.welcomeAction) { - WelcomeAction.CONTINUE -> continueNext() - WelcomeAction.RESTORE_BACKUP -> Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2] - } - } - - private fun continueNext() { + private fun launchPermissionRequests() { val isUserSelectionRequired = BackupUtil.isUserSelectionRequired(requireContext()) - val requiredPermissions = WelcomePermissions.getWelcomePermissions(isUserSelectionRequired) - requestPermissionLauncher.launch(requiredPermissions) - } - private fun onNotNowClicked() { - when (args.welcomeAction) { - WelcomeAction.CONTINUE -> continueNotNow() - WelcomeAction.RESTORE_BACKUP -> Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2] + val neededPermissions = WelcomePermissions.getWelcomePermissions(isUserSelectionRequired).filterNot { + ContextCompat.checkSelfPermission(requireContext(), it) == PackageManager.PERMISSION_GRANTED + } + + if (neededPermissions.isEmpty()) { + proceedToNextScreen() + } else { + requestPermissionLauncher.launch(neededPermissions.toTypedArray()) } } - private fun continueNotNow() { - NavHostFragment.findNavController(this).popBackStack() - } - - private fun permissionsGranted(permissions: Map) { + private fun onPermissionsGranted(permissions: Map) { permissions.forEach { Log.d(TAG, "${it.key} = ${it.value}") } sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED) + proceedToNextScreen() + } + + private fun proceedToNextScreen() { + when (welcomeAction) { + WelcomeAction.CONTINUE -> NavHostFragment.findNavController(this).safeNavigate(GrantPermissionsV2FragmentDirections.actionEnterPhoneNumber()) + WelcomeAction.RESTORE_BACKUP -> { + val restoreIntent = RestoreActivity.getIntentForRestore(requireActivity()) + launchRestoreActivity.launch(restoreIntent) + } + } } /** @@ -110,4 +116,8 @@ class GrantPermissionsV2Fragment : ComposeFragment() { CONTINUE, RESTORE_BACKUP } + + companion object { + private val TAG = Log.tag(GrantPermissionsV2Fragment::class.java) + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt index 90702878a0..d60b3ab968 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/phonenumber/EnterPhoneNumberV2Fragment.kt @@ -24,6 +24,7 @@ import androidx.core.widget.addTextChangedListener import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.fragment.findNavController import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -95,7 +96,9 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio sharedViewModel.uiState.observe(viewLifecycleOwner) { sharedState -> presentRegisterButton(sharedState) presentProgressBar(sharedState.inProgress, sharedState.isReRegister) - if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) { + if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.PHONE_NUMBER_CONFIRMED && sharedState.canSkipSms) { + moveToEnterPinScreen() + } else if (sharedState.registrationCheckpoint >= RegistrationCheckpoint.VERIFICATION_CODE_REQUESTED) { moveToVerificationEntryScreen() } } @@ -320,6 +323,11 @@ class EnterPhoneNumberV2Fragment : LoggingFragment(R.layout.fragment_registratio }.show() } + private fun moveToEnterPinScreen() { + sharedViewModel.setInProgress(false) + findNavController().safeNavigate(EnterPhoneNumberV2FragmentDirections.actionReRegisterWithPinV2Fragment()) + } + private fun moveToVerificationEntryScreen() { NavHostFragment.findNavController(this).safeNavigate(EnterPhoneNumberV2FragmentDirections.actionEnterVerificationCode()) sharedViewModel.setInProgress(false) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2Fragment.kt new file mode 100644 index 0000000000..f9bc892c23 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2Fragment.kt @@ -0,0 +1,243 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.registration.v2.ui.reregisterwithpin + +import android.os.Bundle +import android.text.InputType +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.Toast +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import com.google.android.material.dialog.MaterialAlertDialogBuilder +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.FragmentRegistrationPinRestoreEntryV2Binding +import org.thoughtcrime.securesms.lock.v2.PinKeyboardType +import org.thoughtcrime.securesms.lock.v2.SvrConstants +import org.thoughtcrime.securesms.registration.fragments.BaseRegistrationLockFragment +import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate +import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint +import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2State +import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel +import org.thoughtcrime.securesms.util.CommunicationActions +import org.thoughtcrime.securesms.util.SupportEmailUtil +import org.thoughtcrime.securesms.util.ViewUtil +import org.thoughtcrime.securesms.util.navigation.safeNavigate + +class ReRegisterWithPinV2Fragment : LoggingFragment(R.layout.fragment_registration_pin_restore_entry_v2) { + companion object { + private val TAG = Log.tag(ReRegisterWithPinV2Fragment::class.java) + } + + private val registrationViewModel by activityViewModels() + private val reRegisterViewModel by viewModels() + + private val binding: FragmentRegistrationPinRestoreEntryV2Binding by ViewBinderDelegate(FragmentRegistrationPinRestoreEntryV2Binding::bind) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + RegistrationViewDelegate.setDebugLogSubmitMultiTapView(binding.pinRestorePinTitle) + binding.pinRestorePinDescription.setText(R.string.RegistrationLockFragment__enter_the_pin_you_created_for_your_account) + + binding.pinRestoreForgotPin.visibility = View.GONE + binding.pinRestoreForgotPin.setOnClickListener { onNeedHelpClicked() } + + binding.pinRestoreSkipButton.setOnClickListener { onSkipClicked() } + + binding.pinRestorePinInput.imeOptions = EditorInfo.IME_ACTION_DONE + binding.pinRestorePinInput.setOnEditorActionListener { v, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + ViewUtil.hideKeyboard(requireContext(), v!!) + handlePinEntry() + return@setOnEditorActionListener true + } + false + } + + enableAndFocusPinEntry() + + binding.pinRestorePinContinue.setOnClickListener { + handlePinEntry() + } + + binding.pinRestoreKeyboardToggle.setOnClickListener { + val currentKeyboardType: PinKeyboardType = getPinEntryKeyboardType() + updateKeyboard(currentKeyboardType.other) + binding.pinRestoreKeyboardToggle.setIconResource(currentKeyboardType.iconResource) + } + + binding.pinRestoreKeyboardToggle.setIconResource(getPinEntryKeyboardType().other.iconResource) + + registrationViewModel.uiState.observe(viewLifecycleOwner, ::updateViewState) + } + + private fun updateViewState(state: RegistrationV2State) { + if (state.networkError != null) { + genericErrorDialog() + } else if (!state.canSkipSms) { + abortSkipSmsFlow() + } else if (state.isRegistrationLockEnabled && state.svrTriesRemaining == 0) { + Log.w(TAG, "Unable to continue skip flow, KBS is locked") + onAccountLocked() + } else { + presentProgress(state.inProgress) + presentTriesRemaining(state.svrTriesRemaining) + } + } + + private fun abortSkipSmsFlow() { + findNavController().safeNavigate(ReRegisterWithPinV2FragmentDirections.actionReRegisterWithPinFragmentToEnterPhoneNumberV2Fragment()) + } + + private fun presentProgress(inProgress: Boolean) { + if (inProgress) { + ViewUtil.hideKeyboard(requireContext(), binding.pinRestorePinInput) + binding.pinRestorePinInput.isEnabled = false + binding.pinRestorePinContinue.setSpinning() + } else { + binding.pinRestorePinInput.isEnabled = true + binding.pinRestorePinContinue.cancelSpinning() + } + } + + private fun handlePinEntry() { + val pin: String? = binding.pinRestorePinInput.text?.toString() + + if (pin.isNullOrBlank()) { + Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show() + enableAndFocusPinEntry() + return + } + + if (pin.trim().length < BaseRegistrationLockFragment.MINIMUM_PIN_LENGTH) { + Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, BaseRegistrationLockFragment.MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show() + enableAndFocusPinEntry() + return + } + + registrationViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PIN_CONFIRMED) + + registrationViewModel.verifyReRegisterWithPin(requireContext(), pin) { + reRegisterViewModel.markIncorrectGuess() + } + + // TODO [regv2]: check for registration lock + wrong pin and decrement SVR tries remaining + } + + private fun presentTriesRemaining(triesRemaining: Int) { + if (reRegisterViewModel.hasIncorrectGuess) { + if (triesRemaining == 1 && !reRegisterViewModel.isLocalVerification) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.PinRestoreEntryFragment_incorrect_pin) + .setMessage(resources.getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining, triesRemaining)) + .setPositiveButton(android.R.string.ok, null) + .show() + } + + if (triesRemaining > 5) { + binding.pinRestorePinInputLabel.setText(R.string.PinRestoreEntryFragment_incorrect_pin) + } else { + binding.pinRestorePinInputLabel.text = resources.getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining, triesRemaining) + } + binding.pinRestoreForgotPin.visibility = View.VISIBLE + } else { + if (triesRemaining == 1) { + binding.pinRestoreForgotPin.visibility = View.VISIBLE + if (!reRegisterViewModel.isLocalVerification) { + MaterialAlertDialogBuilder(requireContext()) + .setMessage(resources.getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining, triesRemaining)) + .setPositiveButton(android.R.string.ok, null) + .show() + } + } + } + + if (triesRemaining == 0) { + Log.w(TAG, "Account locked. User out of attempts on KBS.") + onAccountLocked() + } + } + + private fun onAccountLocked() { + Log.d(TAG, "Showing Incorrect PIN dialog. Is local verification: ${reRegisterViewModel.isLocalVerification}") + val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_out_of_guesses_local else R.string.PinRestoreLockedFragment_youve_run_out_of_pin_guesses + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.PinRestoreEntryFragment_incorrect_pin) + .setMessage(message) + .setCancelable(false) + .setPositiveButton(R.string.ReRegisterWithPinFragment_send_sms_code) { _, _ -> onSkipPinEntry() } + .setNegativeButton(R.string.AccountLockedFragment__learn_more) { _, _ -> CommunicationActions.openBrowserLink(requireContext(), getString(R.string.PinRestoreLockedFragment_learn_more_url)) } + .show() + } + + private fun enableAndFocusPinEntry() { + binding.pinRestorePinInput.isEnabled = true + binding.pinRestorePinInput.isFocusable = true + ViewUtil.focusAndShowKeyboard(binding.pinRestorePinInput) + } + + private fun getPinEntryKeyboardType(): PinKeyboardType { + val isNumeric = binding.pinRestorePinInput.inputType and InputType.TYPE_MASK_CLASS == InputType.TYPE_CLASS_NUMBER + return if (isNumeric) PinKeyboardType.NUMERIC else PinKeyboardType.ALPHA_NUMERIC + } + + private fun updateKeyboard(keyboard: PinKeyboardType) { + val isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC + binding.pinRestorePinInput.inputType = if (isAlphaNumeric) InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD else InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD + binding.pinRestorePinInput.text?.clear() + } + + private fun onNeedHelpClicked() { + val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_need_help_local else R.string.PinRestoreEntryFragment_your_pin_is_a_d_digit_code + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.PinRestoreEntryFragment_need_help) + .setMessage(getString(message, SvrConstants.MINIMUM_PIN_LENGTH)) + .setPositiveButton(R.string.PinRestoreEntryFragment_skip) { _, _ -> onSkipPinEntry() } + .setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, _ -> + val body = SupportEmailUtil.generateSupportEmailBody(requireContext(), R.string.ReRegisterWithPinFragment_support_email_subject, null, null) + + CommunicationActions.openEmail( + requireContext(), + SupportEmailUtil.getSupportEmailAddress(requireContext()), + getString(R.string.ReRegisterWithPinFragment_support_email_subject), + body + ) + } + .setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null) + .show() + } + + private fun onSkipClicked() { + val message = if (reRegisterViewModel.isLocalVerification) R.string.ReRegisterWithPinFragment_skip_local else R.string.PinRestoreEntryFragment_if_you_cant_remember_your_pin + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.PinRestoreEntryFragment_skip_pin_entry) + .setMessage(message) + .setPositiveButton(R.string.PinRestoreEntryFragment_skip) { _, _ -> onSkipPinEntry() } + .setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null) + .show() + } + + private fun onSkipPinEntry() { + Log.d(TAG, "User skipping PIN entry.") + registrationViewModel.setUserSkippedReRegisterFlow(true) + } + + private fun genericErrorDialog() { + MaterialAlertDialogBuilder(requireContext()) + .setMessage(R.string.RegistrationActivity_error_connecting_to_service) + .setPositiveButton(android.R.string.ok, null) + .create() + .show() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2State.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2State.kt new file mode 100644 index 0000000000..7f8bf609fa --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2State.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.registration.v2.ui.reregisterwithpin + +data class ReRegisterWithPinV2State( + val isLocalVerification: Boolean = false, + val hasIncorrectGuess: Boolean = false, + val localPinMatches: Boolean = false +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2ViewModel.kt new file mode 100644 index 0000000000..2f8b2b4098 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/reregisterwithpin/ReRegisterWithPinV2ViewModel.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.registration.v2.ui.reregisterwithpin + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import org.signal.core.util.logging.Log + +class ReRegisterWithPinV2ViewModel : ViewModel() { + companion object { + private val TAG = Log.tag(ReRegisterWithPinV2ViewModel::class.java) + } + + private val store = MutableStateFlow(ReRegisterWithPinV2State()) + val uiState = store.asLiveData() + + val isLocalVerification: Boolean + get() = store.value.isLocalVerification + val hasIncorrectGuess: Boolean + get() = store.value.hasIncorrectGuess + + fun markIncorrectGuess() { + store.update { + it.copy(hasIncorrectGuess = true) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt index cf4ca9b2bf..d2a7d36ba1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt @@ -6,12 +6,15 @@ package org.thoughtcrime.securesms.registration.v2.ui.welcome import android.Manifest +import android.app.Activity import android.content.pm.PackageManager import android.os.Bundle import android.view.View +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.fragment.findNavController import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.R @@ -20,8 +23,10 @@ import org.thoughtcrime.securesms.databinding.FragmentRegistrationWelcomeV2Bindi import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView import org.thoughtcrime.securesms.registration.fragments.WelcomePermissions +import org.thoughtcrime.securesms.registration.v2.ui.RegistrationCheckpoint import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2ViewModel import org.thoughtcrime.securesms.registration.v2.ui.grantpermissions.GrantPermissionsV2Fragment +import org.thoughtcrime.securesms.restore.RestoreActivity import org.thoughtcrime.securesms.util.BackupUtil import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.TextSecurePreferences @@ -36,6 +41,20 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome private val sharedViewModel by activityViewModels() private val binding: FragmentRegistrationWelcomeV2Binding by ViewBinderDelegate(FragmentRegistrationWelcomeV2Binding::bind) + private val launchRestoreActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + when (val resultCode = result.resultCode) { + Activity.RESULT_OK -> { + sharedViewModel.onBackupSuccessfullyRestored() + findNavController().safeNavigate(WelcomeV2FragmentDirections.actionGoToRegistration()) + } + Activity.RESULT_CANCELED -> { + Log.w(TAG, "Backup restoration canceled.") + findNavController().popBackStack() + } + else -> Log.w(TAG, "Backup restoration activity ended with unknown result code: $resultCode") + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) maybePrefillE164() @@ -43,12 +62,13 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome setDebugLogSubmitMultiTapView(binding.title) binding.welcomeContinueButton.setOnClickListener { onContinueClicked() } binding.welcomeTermsButton.setOnClickListener { onTermsClicked() } + binding.welcomeTransferOrRestore.setOnClickListener { onTransferOrRestoreClicked() } } private fun onContinueClicked() { TextSecurePreferences.setHasSeenWelcomeScreen(requireContext(), true) if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) { - NavHostFragment.findNavController(this).safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.CONTINUE)) + findNavController().safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.CONTINUE)) } else { skipRestore() } @@ -60,13 +80,24 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome } private fun skipRestore() { - NavHostFragment.findNavController(this).safeNavigate(WelcomeV2FragmentDirections.actionSkipRestore()) + findNavController().safeNavigate(WelcomeV2FragmentDirections.actionSkipRestore()) } private fun onTermsClicked() { CommunicationActions.openBrowserLink(requireContext(), TERMS_AND_CONDITIONS_URL) } + private fun onTransferOrRestoreClicked() { + if (Permissions.isRuntimePermissionsRequired() && !hasAllPermissions()) { + findNavController().safeNavigate(WelcomeV2FragmentDirections.actionWelcomeFragmentToGrantPermissionsV2Fragment(GrantPermissionsV2Fragment.WelcomeAction.RESTORE_BACKUP)) + } else { + sharedViewModel.setRegistrationCheckpoint(RegistrationCheckpoint.PERMISSIONS_GRANTED) + + val restoreIntent = RestoreActivity.getIntentForRestore(requireActivity()) + launchRestoreActivity.launch(restoreIntent) + } + } + private fun maybePrefillE164() { if (Permissions.hasAll(requireContext(), Manifest.permission.READ_PHONE_STATE, Manifest.permission.READ_PHONE_NUMBERS)) { val localNumber = Util.getDeviceNumber(requireContext()).getOrNull() diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreActivity.kt index e28896c2df..fea5fb263b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/restore/RestoreActivity.kt @@ -23,12 +23,20 @@ class RestoreActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + setResult(RESULT_CANCELED) + setContentView(R.layout.activity_restore) intent.getParcelableExtraCompat(PassphraseRequiredActivity.NEXT_INTENT_EXTRA, Intent::class.java)?.let { sharedViewModel.setNextIntent(it) } } + fun finishActivitySuccessfully() { + setResult(RESULT_OK) + finish() + } + companion object { @JvmStatic fun getIntentForRestore(context: Context): Intent { diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/RestoreLocalBackupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/RestoreLocalBackupFragment.kt index 576ff86fcb..276acfa927 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/RestoreLocalBackupFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/RestoreLocalBackupFragment.kt @@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.databinding.FragmentRestoreLocalBackupV2Bindin import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment.PassphraseAsYouTypeFormatter +import org.thoughtcrime.securesms.restore.RestoreActivity import org.thoughtcrime.securesms.restore.RestoreRepository import org.thoughtcrime.securesms.restore.RestoreViewModel import org.thoughtcrime.securesms.util.DateUtils @@ -52,12 +53,11 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc setDebugLogSubmitMultiTapView(binding.verifyHeader) Log.i(TAG, "Backup restore.") + binding.restoreButton.setOnClickListener { presentBackupPassPhrasePromptDialog() } + restoreLocalBackupViewModel.uiState.observe(viewLifecycleOwner) { fragmentState -> fragmentState.backupInfo?.let { presentBackupFileInfo(backupSize = it.size, backupTimestamp = it.timestamp) - if (fragmentState.backupPassphrase.isEmpty()) { - presentBackupPassPhrasePromptDialog() - } } if (fragmentState.restoreInProgress) { @@ -71,23 +71,23 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc if (importResult == null) { onBackupCompletedSuccessfully() } else { - handleBackupImportResult(importResult) + handleBackupImportError(importResult) } } } - restoreLocalBackupViewModel.startRestore(requireContext()) + restoreLocalBackupViewModel.prepareRestore(requireContext()) } private fun onBackupCompletedSuccessfully() { Log.d(TAG, "onBackupCompletedSuccessfully()") SignalStore.internalValues().setForceEnterRestoreV2Flow(false) - val activity = requireActivity() + val activity = requireActivity() as RestoreActivity navigationViewModel.getNextIntent()?.let { Log.d(TAG, "Launching ${it.component}") activity.startActivity(it) } - activity.finish() + activity.finishActivitySuccessfully() } override fun onStart() { @@ -105,12 +105,12 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc restoreLocalBackupViewModel.onBackupProgressUpdate(event) } - private fun handleBackupImportResult(importResult: RestoreRepository.BackupImportResult) { + private fun handleBackupImportError(importResult: RestoreRepository.BackupImportResult) { when (importResult) { RestoreRepository.BackupImportResult.FAILURE_VERSION_DOWNGRADE -> Toast.makeText(requireContext(), R.string.RegistrationActivity_backup_failure_downgrade, Toast.LENGTH_LONG).show() RestoreRepository.BackupImportResult.FAILURE_FOREIGN_KEY -> Toast.makeText(requireContext(), R.string.RegistrationActivity_backup_failure_foreign_key, Toast.LENGTH_LONG).show() RestoreRepository.BackupImportResult.FAILURE_UNKNOWN -> Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show() - RestoreRepository.BackupImportResult.SUCCESS -> Log.w(TAG, "Successful backup import should not be handled here.", IllegalStateException()) + RestoreRepository.BackupImportResult.SUCCESS -> Log.w(TAG, "Successful backup import should not be handled in this function.", IllegalStateException()) } } @@ -143,7 +143,7 @@ class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_loc ViewUtil.hideKeyboard(requireContext(), prompt) val passphrase = prompt.getText().toString() - restoreLocalBackupViewModel.confirmPassphrase(requireContext(), passphrase) + restoreLocalBackupViewModel.confirmPassphraseAndBeginRestore(requireContext(), passphrase) } .setNegativeButton(android.R.string.cancel, null) .show() diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/RestoreLocalBackupViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/RestoreLocalBackupViewModel.kt index f8c2fee5d3..20c9f3f9b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/RestoreLocalBackupViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/restore/restorelocalbackup/RestoreLocalBackupViewModel.kt @@ -24,7 +24,7 @@ class RestoreLocalBackupViewModel(fileBackupUri: Uri) : ViewModel() { private val store = MutableStateFlow(RestoreLocalBackupState(fileBackupUri)) val uiState = store.asLiveData() - fun startRestore(context: Context) { + fun prepareRestore(context: Context) { val backupFileUri = store.value.uri viewModelScope.launch { val backupInfo = RestoreRepository.getLocalBackupFromUri(context, backupFileUri) @@ -48,7 +48,7 @@ class RestoreLocalBackupViewModel(fileBackupUri: Uri) : ViewModel() { } } - fun confirmPassphrase(context: Context, passphrase: String) { + fun confirmPassphraseAndBeginRestore(context: Context, passphrase: String) { store.update { it.copy( backupPassphrase = passphrase, diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index b555f7a8e3..151766491e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -130,6 +130,7 @@ public final class FeatureFlags { private static final String CAMERAX_CUSTOM_CONTROLLER = "android.cameraXCustomController"; private static final String REGISTRATION_V2 = "android.registration.v2"; private static final String LIBSIGNAL_WEB_SOCKET_ENABLED = "android.libsignalWebSocketEnabled"; + private static final String RESTORE_POST_REGISTRATION = "android.registration.restorePostRegistration"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -214,7 +215,7 @@ public final class FeatureFlags { ); @VisibleForTesting - static final Set NOT_REMOTE_CAPABLE = SetUtil.newHashSet(MESSAGE_BACKUPS, REGISTRATION_V2); + static final Set NOT_REMOTE_CAPABLE = SetUtil.newHashSet(MESSAGE_BACKUPS, REGISTRATION_V2, RESTORE_POST_REGISTRATION); /** * Values in this map will take precedence over any value. This should only be used for local @@ -759,6 +760,11 @@ public final class FeatureFlags { /** Whether unauthenticated chat web socket is backed by libsignal-net */ public static boolean libSignalWebSocketEnabled() { return getBoolean(LIBSIGNAL_WEB_SOCKET_ENABLED, false); } + /** Whether or not to launch the restore activity after registration is complete, rather than before. */ + public static boolean restoreAfterRegistration() { + return getBoolean(RESTORE_POST_REGISTRATION, false); + } + /** Only for rendering debug info. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); diff --git a/app/src/main/res/layout/fragment_registration_pin_restore_entry_v2.xml b/app/src/main/res/layout/fragment_registration_pin_restore_entry_v2.xml new file mode 100644 index 0000000000..9f947c0d81 --- /dev/null +++ b/app/src/main/res/layout/fragment_registration_pin_restore_entry_v2.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_registration_welcome_v2.xml b/app/src/main/res/layout/fragment_registration_welcome_v2.xml index e4914c5f42..5f5a350be4 100644 --- a/app/src/main/res/layout/fragment_registration_welcome_v2.xml +++ b/app/src/main/res/layout/fragment_registration_welcome_v2.xml @@ -55,7 +55,6 @@ app:layout_goneMarginBottom="@dimen/registration_button_bottom_margin" app:materialThemeOverlay="@style/ThemeOverlay.Signal.CircularProgressIndicator.Tonal" /> - - - - - - - - - - - - { } } + /** + * Returns the [Throwable] associated with the result, or null if the result is successful. + */ + fun getCause(): Throwable? { + return when (this) { + is Success -> null + is NetworkError -> exception + is StatusCodeError -> exception + is ApplicationError -> throwable + } + } + /** * Takes the output of one [NetworkResult] and transforms it into another if the operation is successful. * If it's a failure, the original failure will be propagated. Useful for changing the type of a result. diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt index 80dce4bc2e..1eea96734c 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt @@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api.registration import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.account.AccountAttributes import org.whispersystems.signalservice.api.account.PreKeyCollection +import org.whispersystems.signalservice.internal.push.BackupAuthCheckResponse import org.whispersystems.signalservice.internal.push.PushServiceSocket import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse import org.whispersystems.signalservice.internal.push.VerifyAccountResponse @@ -67,4 +68,10 @@ class RegistrationApi( pushServiceSocket.submitRegistrationRequest(sessionId, recoveryPassword, attributes, aciPreKeys, pniPreKeys, fcmToken, skipDeviceTransfer) } } + + fun getSvrAuthCredential(e164: String, usernamePasswords: List): NetworkResult { + return NetworkResult.fromFetch { + pushServiceSocket.checkBackupAuthCredentials(e164, usernamePasswords) + } + } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index b32df31fac..af6602b5d3 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -1137,6 +1137,11 @@ public class PushServiceSocket { .onErrorReturn(ServiceResponse::forUnknownError); } + public BackupAuthCheckResponse checkBackupAuthCredentials(@Nullable String number, @Nonnull List passwords) throws IOException { + String response = makeServiceRequest(BACKUP_AUTH_CHECK, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty()); + return JsonUtil.fromJson(response, BackupAuthCheckResponse.class); + } + private Single> createBackupAuthCheckSingle(@Nonnull String path, @Nonnull BackupAuthCheckRequest request, @Nonnull ResponseMapper responseMapper) @@ -3048,7 +3053,6 @@ public class PushServiceSocket { } } - private static RegistrationSessionMetadataResponse parseSessionMetadataResponse(Response response) throws IOException { long serverDeliveredTimestamp = 0; try {