4254 lines
182 KiB
Swift
4254 lines
182 KiB
Swift
//
|
||
// Copyright 2023 Signal Messenger, LLC
|
||
// SPDX-License-Identifier: AGPL-3.0-only
|
||
//
|
||
|
||
import Contacts
|
||
import Foundation
|
||
import LibSignalClient
|
||
public import SignalServiceKit
|
||
|
||
public protocol RegistrationCoordinatorLoaderDelegate: AnyObject {
|
||
func clearPersistedMode(transaction: DBWriteTransaction)
|
||
|
||
func savePendingChangeNumber(
|
||
oldState: RegistrationCoordinatorLoaderImpl.Mode.ChangeNumberState,
|
||
pniState: RegistrationCoordinatorLoaderImpl.Mode.ChangeNumberState.PendingPniState?,
|
||
transaction: DBWriteTransaction
|
||
) throws -> RegistrationCoordinatorLoaderImpl.Mode.ChangeNumberState
|
||
}
|
||
|
||
public class RegistrationCoordinatorImpl: RegistrationCoordinator {
|
||
|
||
/// Only `RegistrationCoordinatorLoaderImpl` can create a nested `Mode` instance,
|
||
/// so only it can create this class. If you want an instance, use `RegistrationCoordinatorLoaderImpl`.
|
||
public init(
|
||
mode: RegistrationCoordinatorLoaderImpl.Mode,
|
||
loader: RegistrationCoordinatorLoaderDelegate,
|
||
dependencies: RegistrationCoordinatorDependencies
|
||
) {
|
||
self._unsafeToModify_mode = mode
|
||
self.kvStore = KeyValueStore(collection: "RegistrationCoordinator")
|
||
self.loader = loader
|
||
self.deps = dependencies
|
||
}
|
||
|
||
// MARK: - Public API
|
||
|
||
public func switchToSecondaryDeviceLinking() -> Bool {
|
||
Logger.info("")
|
||
|
||
switch mode {
|
||
case .registering:
|
||
if persistedState.hasShownSplash {
|
||
// Once we are past the splash, no going back.
|
||
return false
|
||
} else {
|
||
self.db.write { tx in
|
||
self.wipePersistedState(tx)
|
||
}
|
||
return true
|
||
}
|
||
case .reRegistering, .changingNumber:
|
||
return false
|
||
}
|
||
}
|
||
|
||
public func exitRegistration() -> Bool {
|
||
Logger.info("")
|
||
|
||
switch canExitRegistrationFlow() {
|
||
case .notAllowed:
|
||
Logger.warn("User can't exit registration now")
|
||
return false
|
||
case .allowed(let shouldWipeState):
|
||
if shouldWipeState {
|
||
// Wipe in progress state; presumably the user decided not
|
||
// to proceed and should
|
||
// a) not be sent here by default next app launch
|
||
// b) start again from scratch if they do opt to return
|
||
self.db.write { tx in
|
||
self.wipePersistedState(tx)
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
}
|
||
|
||
public func nextStep() -> Guarantee<RegistrationStep> {
|
||
AssertIsOnMainThread()
|
||
|
||
if deps.appExpiry.isExpired {
|
||
return .value(.appUpdateBanner)
|
||
}
|
||
|
||
// Always start by restoring state.
|
||
return restoreStateIfNeeded().then(on: schedulers.main) { [weak self] () -> Guarantee<RegistrationStep> in
|
||
guard let self = self else {
|
||
owsFailBeta("Unretained self lost")
|
||
return .value(.registrationSplash)
|
||
}
|
||
return self.nextStep(pathway: self.getPathway())
|
||
}
|
||
}
|
||
|
||
public func continueFromSplash() -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
|
||
db.write { tx in
|
||
self.updatePersistedState(tx) {
|
||
$0.hasShownSplash = true
|
||
}
|
||
}
|
||
return nextStep()
|
||
}
|
||
|
||
public func requestPermissions() -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
|
||
// Notifications first, then contacts if needed.
|
||
return deps.pushRegistrationManager.registerUserNotificationSettings()
|
||
.then(on: schedulers.main) { [weak self] in
|
||
guard let self else {
|
||
owsFailBeta("Unretained self lost")
|
||
return .value(())
|
||
}
|
||
return self.deps.contactsStore.requestContactsAuthorization()
|
||
}
|
||
.then(on: schedulers.main) { [weak self] in
|
||
guard let self else {
|
||
owsFailBeta("Unretained self lost")
|
||
return .value(.registrationSplash)
|
||
}
|
||
self.inMemoryState.needsSomePermissions = false
|
||
return self.nextStep()
|
||
}
|
||
}
|
||
|
||
public func submitProspectiveChangeNumberE164(_ e164: E164) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
self.inMemoryState.changeNumberProspectiveE164 = e164
|
||
return nextStep()
|
||
}
|
||
|
||
public func submitE164(_ e164: E164) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
|
||
var e164 = e164
|
||
switch mode {
|
||
case .reRegistering(let reregState):
|
||
if e164 != reregState.e164 {
|
||
Logger.debug("Tried to submit a changed e164 during rereg; ignoring and submitting the fixed e164 instead.")
|
||
e164 = reregState.e164
|
||
}
|
||
case .registering, .changingNumber:
|
||
break
|
||
}
|
||
|
||
let pathway = getPathway()
|
||
db.write { tx in
|
||
updatePersistedState(tx) {
|
||
$0.e164 = e164
|
||
}
|
||
switch pathway {
|
||
case .session(let session):
|
||
guard session.e164 == e164 else {
|
||
resetSession(tx)
|
||
return
|
||
}
|
||
if
|
||
let sessionState = self.persistedState.sessionState,
|
||
sessionState.sessionId == session.id
|
||
{
|
||
switch sessionState.initialCodeRequestState {
|
||
case
|
||
.smsTransportFailed,
|
||
.transientProviderFailure,
|
||
.permanentProviderFailure,
|
||
.failedToRequest,
|
||
.exhaustedCodeAttempts:
|
||
// Reset state so we try again.
|
||
self.updatePersistedSessionState(session: session, tx) {
|
||
$0.initialCodeRequestState = .neverRequested
|
||
}
|
||
case .requested, .neverRequested:
|
||
break
|
||
}
|
||
}
|
||
case
|
||
.opening,
|
||
.svrAuthCredential,
|
||
.svrAuthCredentialCandidates,
|
||
.registrationRecoveryPassword,
|
||
.profileSetup:
|
||
break
|
||
}
|
||
}
|
||
inMemoryState.hasEnteredE164 = true
|
||
|
||
return nextStep()
|
||
}
|
||
|
||
public func requestChangeE164() -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
db.write { tx in
|
||
updatePersistedState(tx) {
|
||
$0.e164 = nil
|
||
}
|
||
// Reset the session; it is e164 dependent.
|
||
resetSession(tx)
|
||
// Reload auth credential candidates; we might not have
|
||
// had a credential for the old e164 but might have one for
|
||
// the new e164!
|
||
loadSVRAuthCredentialCandidates(tx)
|
||
}
|
||
inMemoryState.hasEnteredE164 = false
|
||
inMemoryState.changeNumberProspectiveE164 = nil
|
||
return nextStep()
|
||
}
|
||
|
||
public func requestSMSCode() -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
switch getPathway() {
|
||
case
|
||
.opening,
|
||
.registrationRecoveryPassword,
|
||
.svrAuthCredential,
|
||
.svrAuthCredentialCandidates,
|
||
.profileSetup:
|
||
owsFailBeta("Shouldn't be resending SMS from non session paths.")
|
||
return nextStep()
|
||
case .session:
|
||
inMemoryState.pendingCodeTransport = .sms
|
||
return nextStep()
|
||
}
|
||
}
|
||
|
||
public func requestVoiceCode() -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
switch getPathway() {
|
||
case
|
||
.opening,
|
||
.registrationRecoveryPassword,
|
||
.svrAuthCredential,
|
||
.svrAuthCredentialCandidates,
|
||
.profileSetup:
|
||
owsFailBeta("Shouldn't be sending voice code from non session paths.")
|
||
return nextStep()
|
||
case .session:
|
||
inMemoryState.pendingCodeTransport = .voice
|
||
return nextStep()
|
||
}
|
||
}
|
||
|
||
public func submitVerificationCode(_ code: String) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
switch getPathway() {
|
||
case
|
||
.opening,
|
||
.registrationRecoveryPassword,
|
||
.svrAuthCredential,
|
||
.svrAuthCredentialCandidates,
|
||
.profileSetup:
|
||
owsFailBeta("Shouldn't be submitting verification code from non session paths.")
|
||
return nextStep()
|
||
case .session(let session):
|
||
return submitSessionCode(session: session, code: code)
|
||
}
|
||
}
|
||
|
||
public func restoreFromMessageBackup(type: RegistrationMessageBackupRestoreType) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
switch getPathway() {
|
||
case
|
||
.opening,
|
||
.registrationRecoveryPassword,
|
||
.svrAuthCredential,
|
||
.svrAuthCredentialCandidates,
|
||
.session:
|
||
owsFailBeta("Shouldn't be restoring from non-profile paths.")
|
||
return nextStep()
|
||
case .profileSetup(let identity):
|
||
return restoreFromMessageBackup(type: type, identity: identity)
|
||
}
|
||
}
|
||
|
||
public func submitCaptcha(_ token: String) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
switch getPathway() {
|
||
case
|
||
.opening,
|
||
.registrationRecoveryPassword,
|
||
.svrAuthCredential,
|
||
.svrAuthCredentialCandidates,
|
||
.profileSetup:
|
||
owsFailBeta("Shouldn't be submitting captcha from non session paths.")
|
||
return nextStep()
|
||
case .session(let session):
|
||
return submit(challengeFulfillment: .captcha(token), for: session)
|
||
}
|
||
}
|
||
|
||
public func setPINCodeForConfirmation(_ blob: RegistrationPinConfirmationBlob) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
inMemoryState.unconfirmedPinBlob = blob
|
||
return nextStep()
|
||
}
|
||
|
||
public func resetUnconfirmedPINCode() -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
inMemoryState.unconfirmedPinBlob = nil
|
||
return nextStep()
|
||
}
|
||
|
||
public func submitPINCode(_ code: String) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
switch getPathway() {
|
||
case .registrationRecoveryPassword:
|
||
if
|
||
let pinFromDisk = inMemoryState.pinFromDisk,
|
||
pinFromDisk != code
|
||
{
|
||
let numberOfWrongGuesses = persistedState.numLocalPinGuesses + 1
|
||
db.write { tx in
|
||
updatePersistedState(tx) {
|
||
$0.numLocalPinGuesses = numberOfWrongGuesses
|
||
}
|
||
}
|
||
if numberOfWrongGuesses >= Constants.maxLocalPINGuesses {
|
||
// "Skip" PIN entry, which will make us stop trying to register via registration
|
||
// recovery password.
|
||
db.write { tx in
|
||
updatePersistedState(tx) {
|
||
$0.hasSkippedPinEntry = true
|
||
}
|
||
switch self.mode {
|
||
case .changingNumber:
|
||
break
|
||
case .registering, .reRegistering:
|
||
deps.svr.clearKeys(transaction: tx)
|
||
deps.ows2FAManager.clearLocalPinCode(tx)
|
||
}
|
||
}
|
||
inMemoryState.pinFromUser = nil
|
||
inMemoryState.pinFromDisk = nil
|
||
self.wipeInMemoryStateToPreventSVRPathAttempts()
|
||
return .value(.pinAttemptsExhaustedWithoutReglock(
|
||
.init(mode: .restoringRegistrationRecoveryPassword)
|
||
))
|
||
} else {
|
||
let remainingAttempts = Constants.maxLocalPINGuesses - numberOfWrongGuesses
|
||
return .value(.pinEntry(RegistrationPinState(
|
||
operation: .enteringExistingPin(
|
||
skippability: .canSkip,
|
||
remainingAttempts: remainingAttempts
|
||
),
|
||
error: .wrongPin(wrongPin: code),
|
||
contactSupportMode: contactSupportRegistrationPINMode(),
|
||
exitConfiguration: pinCodeEntryExitConfiguration()
|
||
)))
|
||
}
|
||
}
|
||
case .opening, .svrAuthCredential, .svrAuthCredentialCandidates, .profileSetup, .session:
|
||
// We aren't checking against any local state, rely on the request.
|
||
break
|
||
}
|
||
self.inMemoryState.pinFromUser = code
|
||
// Individual pathway's steps should handle whatever needs to be done with the pin,
|
||
// depending on the current pathway.
|
||
return nextStep()
|
||
}
|
||
|
||
public func skipPINCode() -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
let shouldGiveUpTryingToRestoreWithSVR: Bool = {
|
||
switch getPathway() {
|
||
case
|
||
.opening,
|
||
.registrationRecoveryPassword,
|
||
.svrAuthCredential,
|
||
.svrAuthCredentialCandidates,
|
||
.session:
|
||
return false
|
||
case .profileSetup:
|
||
return true
|
||
}
|
||
}()
|
||
db.write { tx in
|
||
updatePersistedState(tx) {
|
||
$0.hasSkippedPinEntry = true
|
||
if shouldGiveUpTryingToRestoreWithSVR {
|
||
$0.hasGivenUpTryingToRestoreWithSVR = true
|
||
}
|
||
}
|
||
switch self.mode {
|
||
case .changingNumber:
|
||
break
|
||
case .registering, .reRegistering:
|
||
// Whenever we do this, wipe the keys we've got.
|
||
// We don't want to have them and use then implicitly later.
|
||
deps.svr.clearKeys(transaction: tx)
|
||
deps.ows2FAManager.clearLocalPinCode(tx)
|
||
}
|
||
}
|
||
inMemoryState.pinFromUser = nil
|
||
self.wipeInMemoryStateToPreventSVRPathAttempts()
|
||
return nextStep()
|
||
}
|
||
|
||
public func skipAndCreateNewPINCode() -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
switch getPathway() {
|
||
case
|
||
.opening,
|
||
.registrationRecoveryPassword,
|
||
.svrAuthCredential,
|
||
.svrAuthCredentialCandidates,
|
||
.session:
|
||
Logger.error("Invalid state from which to skip!")
|
||
return nextStep()
|
||
case .profileSetup:
|
||
break
|
||
}
|
||
db.write { tx in
|
||
updatePersistedState(tx) {
|
||
// We are NOT skipping PIN entry; just restoring, which
|
||
// means we will create a new PIN.
|
||
$0.hasSkippedPinEntry = false
|
||
$0.hasGivenUpTryingToRestoreWithSVR = true
|
||
}
|
||
switch self.mode {
|
||
case .changingNumber:
|
||
break
|
||
case .registering, .reRegistering:
|
||
// Whenever we do this, wipe the keys we've got.
|
||
// We don't want to have them and use them implicitly later.
|
||
deps.svr.clearKeys(transaction: tx)
|
||
deps.ows2FAManager.clearLocalPinCode(tx)
|
||
}
|
||
}
|
||
inMemoryState.pinFromUser = nil
|
||
self.wipeInMemoryStateToPreventSVRPathAttempts()
|
||
return nextStep()
|
||
}
|
||
|
||
public func skipDeviceTransfer() -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
db.write { tx in
|
||
updatePersistedState(tx) {
|
||
$0.hasDeclinedTransfer = true
|
||
}
|
||
}
|
||
return self.nextStep()
|
||
}
|
||
|
||
public func skipRestoreFromBackup() -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
inMemoryState.hasSkippedRestoreFromMessageBackup = true
|
||
return self.nextStep()
|
||
}
|
||
|
||
private func restoreFromMessageBackup(
|
||
type: RegistrationMessageBackupRestoreType,
|
||
identity: AccountIdentity
|
||
) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
return Promise.wrapAsync {
|
||
let fileUrl: URL
|
||
switch type {
|
||
case .local(let localFileUrl):
|
||
fileUrl = localFileUrl
|
||
case .remote:
|
||
fileUrl = try await self.deps.messageBackupManager.downloadEncryptedBackup(
|
||
localIdentifiers: identity.localIdentifiers,
|
||
auth: identity.chatServiceAuth
|
||
)
|
||
}
|
||
// Get Backup Key
|
||
let backupKey = try self.deps.db.read { tx in
|
||
return try self.deps.messageBackupKeyMaterial.backupKey(type: .messages, tx: tx)
|
||
}
|
||
try await self.deps.messageBackupManager.importEncryptedBackup(
|
||
fileUrl: fileUrl,
|
||
localIdentifiers: identity.localIdentifiers,
|
||
backupKey: backupKey,
|
||
progress: nil
|
||
)
|
||
self.inMemoryState.hasRestoredFromLocalMessageBackup = true
|
||
Logger.info("Finished restore")
|
||
}.recover(on: schedulers.main) { error in
|
||
let (guarantee, future) = Guarantee<Void>.pending()
|
||
self.deps.messageBackupErrorPresenter.presentOverTopmostViewController {
|
||
future.resolve()
|
||
}
|
||
return guarantee
|
||
}.then { [weak self] () -> Guarantee<RegistrationStep> in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
return self.nextStep()
|
||
}
|
||
}
|
||
|
||
public func setPhoneNumberDiscoverability(_ phoneNumberDiscoverability: PhoneNumberDiscoverability) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
guard let accountIdentity = persistedState.accountIdentity else {
|
||
owsFailBeta("Shouldn't be setting phone number discoverability prior to registration.")
|
||
return .value(.showErrorSheet(.genericError))
|
||
}
|
||
|
||
updatePhoneNumberDiscoverability(
|
||
accountIdentity: accountIdentity,
|
||
phoneNumberDiscoverability: phoneNumberDiscoverability
|
||
)
|
||
|
||
return self.nextStep()
|
||
}
|
||
|
||
public func setProfileInfo(
|
||
givenName: OWSUserProfile.NameComponent,
|
||
familyName: OWSUserProfile.NameComponent?,
|
||
avatarData: Data?,
|
||
phoneNumberDiscoverability: PhoneNumberDiscoverability
|
||
) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
|
||
guard let accountIdentity = persistedState.accountIdentity else {
|
||
owsFailBeta("Shouldn't be setting phone number discoverability prior to registration.")
|
||
return .value(.showErrorSheet(.genericError))
|
||
}
|
||
|
||
inMemoryState.pendingProfileInfo = (givenName: givenName, familyName: familyName, avatarData: avatarData)
|
||
|
||
updatePhoneNumberDiscoverability(
|
||
accountIdentity: accountIdentity,
|
||
phoneNumberDiscoverability: phoneNumberDiscoverability
|
||
)
|
||
|
||
return self.nextStep()
|
||
}
|
||
|
||
public func acknowledgeReglockTimeout() -> AcknowledgeReglockResult {
|
||
Logger.info("")
|
||
|
||
switch reglockTimeoutAcknowledgeAction {
|
||
case .resetPhoneNumber:
|
||
db.write { transaction in
|
||
self.resetSession(transaction)
|
||
self.updatePersistedState(transaction) { $0.e164 = nil }
|
||
}
|
||
return .restartRegistration(nextStep())
|
||
case .close:
|
||
guard exitRegistration() else {
|
||
return .cannotExit
|
||
}
|
||
return .exitRegistration
|
||
case .none:
|
||
return .cannotExit
|
||
}
|
||
}
|
||
|
||
// MARK: - Internal
|
||
|
||
typealias Mode = RegistrationCoordinatorLoaderImpl.Mode
|
||
|
||
/// Does not change from one mode to another in the course of registration; you must finish a registration for a mode
|
||
/// before registering in a different mode. (The metadata within a mode may change, e.g. changingNumber has state
|
||
/// that changes as operations are completed. These updates go through RegistrationCoordinatorLoader.)
|
||
/// Persisted on RegistrationCoordinatorLoader.
|
||
private var mode: Mode { return _unsafeToModify_mode }
|
||
|
||
private var _unsafeToModify_mode: Mode
|
||
|
||
private let loader: RegistrationCoordinatorLoaderDelegate
|
||
private let deps: RegistrationCoordinatorDependencies
|
||
private let kvStore: KeyValueStore
|
||
|
||
// Shortcuts for the commonly used ones.
|
||
private var db: any DB { deps.db }
|
||
private var schedulers: Schedulers { deps.schedulers }
|
||
|
||
// MARK: - In Memory State
|
||
|
||
/// This is state that only exists for an in-memory registration attempt;
|
||
/// it is wiped if the app is evicted from memory or registration is completed.
|
||
private struct InMemoryState {
|
||
var hasRestoredState = false
|
||
|
||
var tsRegistrationState: TSRegistrationState?
|
||
|
||
// Whether some system permissions (contacts, APNS) are needed.
|
||
var needsSomePermissions = false
|
||
|
||
// We persist the entered e164. But in addition we need to
|
||
// know whether its been entered during this app launch; if it
|
||
// hasn't we want to explicitly ask the user for it before
|
||
// sending an SMS. But if we have (e.g. we asked for it to try
|
||
// some SVR recovery that failed) we should auto-send an SMS if
|
||
// we get to that step without asking again.
|
||
var hasEnteredE164 = false
|
||
|
||
// When changing number, we ask the user to confirm old number and
|
||
// enter the new number before confirming the new number.
|
||
// This tracks that first check before the confirm.
|
||
var changeNumberProspectiveE164: E164?
|
||
|
||
var shouldRestoreSVRMasterKeyAfterRegistration = false
|
||
// base64 encoded data
|
||
var regRecoveryPw: String?
|
||
// hexadecimal encoded data
|
||
var reglockToken: String?
|
||
|
||
// candidate credentials, which may not
|
||
// be valid, or may not correspond with the current e164.
|
||
var svr2AuthCredentialCandidates: [SVR2AuthCredential]?
|
||
var svrAuthCredential: SVRAuthCredential?
|
||
// If we had SVR backups before registration even began.
|
||
var didHaveSVRBackupsPriorToReg = false
|
||
|
||
// We always require the user to enter the PIN
|
||
// during the in memory app session even if we
|
||
// have it on disk.
|
||
// This is a way to double check they know the PIN.
|
||
var pinFromUser: String?
|
||
var pinFromDisk: String?
|
||
// A really old user might be on v1 2fa; they have a PIN,
|
||
// but no SVR backups. We will encourage backing up
|
||
// to SVR but the user may skip it.
|
||
var isV12faUser: Bool = false
|
||
var unconfirmedPinBlob: RegistrationPinConfirmationBlob?
|
||
|
||
// When we try to register, if we get a response from the server
|
||
// telling us device transfer is possible, we set this to true
|
||
// so the user can explicitly opt out if desired and we retry.
|
||
var needsToAskForDeviceTransfer = false
|
||
|
||
var session: RegistrationSession?
|
||
|
||
// If we try and resend a code (NOT the original SMS code automatically sent
|
||
// at the start of every session), but hit a challenge, we write this var
|
||
// so that when we complete the challenge we send the code right away.
|
||
var pendingCodeTransport: Registration.CodeTransport?
|
||
|
||
// Every time we go through registration, we should back up our SVR master
|
||
// secret's random bytes to SVR. Its safer to do this more than it is to do
|
||
// it less, so keeping this state in memory.
|
||
var hasBackedUpToSVR = false
|
||
var didSkipSVRBackup = false
|
||
var shouldBackUpToSVR: Bool {
|
||
return hasBackedUpToSVR.negated && didSkipSVRBackup.negated
|
||
}
|
||
|
||
// OWS2FAManager state
|
||
// If we are re-registering or changing number and
|
||
// reglock was enabled, we should enable it again when done.
|
||
var wasReglockEnabledBeforeStarting = false
|
||
var hasSetReglock = false
|
||
|
||
var pendingProfileInfo: (givenName: OWSUserProfile.NameComponent, familyName: OWSUserProfile.NameComponent?, avatarData: Data?)?
|
||
|
||
// TSAccountManager state
|
||
var registrationId: UInt32!
|
||
var pniRegistrationId: UInt32!
|
||
var isManualMessageFetchEnabled = false
|
||
var phoneNumberDiscoverability: PhoneNumberDiscoverability?
|
||
|
||
// OWSProfileManager state
|
||
var profileKey: Aes256Key!
|
||
var udAccessKey: SMKUDAccessKey!
|
||
var allowUnrestrictedUD = false
|
||
var hasProfileName = false
|
||
|
||
// Message Backup state
|
||
var hasRestoredFromLocalMessageBackup = false
|
||
var hasSkippedRestoreFromMessageBackup = false
|
||
|
||
// Once we have our SVR master key locally,
|
||
// we can restore profile info from storage service.
|
||
var hasRestoredFromStorageService = false
|
||
var hasSkippedRestoreFromStorageService = false
|
||
|
||
/// Tracks the state of "username reclamation" following Storage Service
|
||
/// restore during registration. See ``attemptToReclaimUsername()`` for
|
||
/// more details.
|
||
enum UsernameReclamationState {
|
||
case localUsernameStateNotLoaded
|
||
case localUsernameStateLoaded(Usernames.LocalUsernameState)
|
||
case reclamationAttempted
|
||
}
|
||
var usernameReclamationState: UsernameReclamationState = .localUsernameStateNotLoaded
|
||
}
|
||
|
||
private var inMemoryState = InMemoryState()
|
||
|
||
// MARK: - Persisted State
|
||
|
||
/// This state is kept across launches of registration. Whatever is set
|
||
/// here must be explicitly wiped between sessions if desired.
|
||
/// Note: We don't persist RegistrationSession because RegistrationSessionManager
|
||
/// handles that; we restore it to InMemoryState instead.
|
||
/// Note: `mode` is kept separate; it has a different lifecycle than the rest
|
||
/// of PersistedState even though it is also persisted to disk.
|
||
internal struct PersistedState: Codable {
|
||
/// We only ever want to show the splash once.
|
||
var hasShownSplash = false
|
||
var shouldSkipRegistrationSplash = false
|
||
|
||
/// When re-registering, just before completing the actual create account
|
||
/// request, we wipe our local state for re-registration. We only do this once,
|
||
/// and once we do, there is no turning back, because we will have wiped
|
||
/// state thats needed to use the app outside of registration.
|
||
var hasResetForReRegistration = false
|
||
|
||
/// The e164 the user has entered for this attempt at registration.
|
||
/// Initially the e164 in the UI may be pre-populated (e.g. in re-reg)
|
||
/// but this value is not set until the user accepts it or enters their own value.
|
||
var e164: E164?
|
||
|
||
/// If we ever get a response from a server where we failed reglock,
|
||
/// we know the e164 the request was for has reglock enabled.
|
||
/// Note that so we always include the reglock token in requests.
|
||
/// (Note that we can't blindly include it because if it wasn't enabled
|
||
/// and we sent it up, that would enable reglock.)
|
||
var e164WithKnownReglockEnabled: E164?
|
||
|
||
/// How many times the user has tried making guesses against the PIN
|
||
/// we have locally? This happens when we have a local SVR master key
|
||
/// and want to confirm the user knows their PIN before using it to register.
|
||
var numLocalPinGuesses: UInt = 0
|
||
|
||
/// There are a few times we ask for the PIN that are skippable:
|
||
///
|
||
/// * Registration recovery password path: we have your SVR master key locally, ask for PIN,
|
||
/// user skips, we stop trying to use the local master key and fall back to session-based
|
||
/// registration.
|
||
///
|
||
/// * SVR Auth Credential path(s): we try and recover the SVR master secret from backups,
|
||
/// ask for PIN, user skips, we stop trying to recover the backup and fall back to
|
||
/// session-based registration.
|
||
///
|
||
/// * Post-registration, if reglock was not enabled but there are SVR backups, we try and
|
||
/// recover them. If the user skips, we don't bother recovering.
|
||
///
|
||
/// In a single flow, the user might hit more than one of these cases (and probably will;
|
||
/// if they have SVR backups and skip in favor of session-based reg, we will see that
|
||
/// they have backups post-registration). This skip applies to _all_ of these; if they
|
||
/// skipped the PIN early on, we won't ask for it again for recovery purposes later.
|
||
var hasSkippedPinEntry = false
|
||
|
||
/// Have we given up trying to restore with SVR? This can happen if you blow through your
|
||
/// PIN guesses or decide to give up before exhausting them.
|
||
var hasGivenUpTryingToRestoreWithSVR = false
|
||
|
||
struct SessionState: Codable {
|
||
let sessionId: String
|
||
|
||
enum InitialCodeRequestState: Codable {
|
||
/// We have never requested a code and should request one when we can.
|
||
case neverRequested
|
||
/// We have already requested a code at least once; further requests
|
||
/// are user driven and not automatic
|
||
case requested
|
||
/// We asked for a code but got some generic failure. User action needed.
|
||
case failedToRequest
|
||
/// We sent a code, but submission attempts were exhausted so we should
|
||
/// send a new code on user input.
|
||
case exhaustedCodeAttempts
|
||
|
||
/// We requested an sms code, but transport failed.
|
||
/// User action needed, by selecting another transport.
|
||
case smsTransportFailed
|
||
// A 3p provider failed to send a message,
|
||
// either permanently or transiently.
|
||
case permanentProviderFailure
|
||
case transientProviderFailure
|
||
}
|
||
|
||
var initialCodeRequestState: InitialCodeRequestState = .neverRequested
|
||
|
||
enum ReglockState: Codable, Equatable {
|
||
/// No reglock known of preventing registration.
|
||
case none
|
||
|
||
/// We tried to register and got reglocked; we have to
|
||
/// recover from SVR2 with the credential given.
|
||
case reglocked(credential: SVRAuthCredential, expirationDate: Date)
|
||
|
||
struct SVRAuthCredential: Codable, Equatable {
|
||
/// In a prior life, this object could contain either a KBS(SVR1) credential or an SVR2 credential.
|
||
/// For backwards compatibility, therefore, the SVR2 credential might be nil.
|
||
let svr2: SVR2AuthCredential?
|
||
|
||
private init(svr2: SVR2AuthCredential?) {
|
||
self.svr2 = svr2
|
||
}
|
||
|
||
init(svr2: SVR2AuthCredential) {
|
||
self.svr2 = svr2
|
||
}
|
||
|
||
#if TESTABLE_BUILD
|
||
static func testOnly(svr2: SVR2AuthCredential?) -> Self {
|
||
return .init(svr2: svr2)
|
||
}
|
||
#endif
|
||
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
self.svr2 = try container.decodeIfPresent(SVR2AuthCredential.self, forKey: .svr2)
|
||
}
|
||
}
|
||
|
||
/// We couldn't recover credentials from SVR (probably
|
||
/// because PIN guesses were exhausted) and so waiting
|
||
/// out the reglock is the only option.
|
||
case waitingTimeout(expirationDate: Date)
|
||
}
|
||
|
||
var reglockState: ReglockState = .none
|
||
|
||
enum PushChallengeState: Codable, Equatable {
|
||
/// We've never requested a push challenge token.
|
||
case notRequested
|
||
/// We don't expect to receive a push challenge token, likely because the user disabled
|
||
/// push notifications.
|
||
case ineligible
|
||
/// We are waiting to receive a push challenge token. Make sure to check the associated
|
||
/// `requestedAt` date to see if it's been too long.
|
||
case waitingForPush(requestedAt: Date)
|
||
/// We've received a push challenge token that we haven't fulfilled.
|
||
case unfulfilledPush(challengeToken: String)
|
||
/// We've sucessfully submitted a push challenge token.
|
||
case fulfilled
|
||
case rejected
|
||
}
|
||
|
||
var pushChallengeState: PushChallengeState = .notRequested
|
||
|
||
/// The number of times we have attempted to submit a verification code.
|
||
var numVerificationCodeSubmissions: UInt = 0
|
||
|
||
/// If non-nil, we created an account with the session but got rate limited
|
||
/// and can retry at the provided time.
|
||
var createAccountTimeout: Date?
|
||
}
|
||
|
||
var sessionState: SessionState?
|
||
|
||
/// Once we get an account identity response from the server
|
||
/// for registering, re-registering, or changing phone number,
|
||
/// we remember it so we don't re-register when we quit the app
|
||
/// before finishing post-registration steps.
|
||
var accountIdentity: AccountIdentity?
|
||
|
||
/// After registration is complete, we generate and sync
|
||
/// one time prekeys (signed prekeys are included in the registration
|
||
/// request). We do not proceed until this succeeds.
|
||
var didRefreshOneTimePreKeys: Bool = false
|
||
|
||
/// When we try and register, the server gives us an error if its possible
|
||
/// to execute a device-to-device transfer. The user can decline; if they
|
||
/// do, this will get set so we try force a re-register.
|
||
/// Note if we are re-registering on the same primary device (based on mode),
|
||
/// we ignore this field and always skip asking for device transfer.
|
||
var hasDeclinedTransfer: Bool = false
|
||
|
||
init() {}
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case hasShownSplash
|
||
case shouldSkipRegistrationSplash
|
||
case hasResetForReRegistration
|
||
case e164
|
||
case e164WithKnownReglockEnabled
|
||
case numLocalPinGuesses
|
||
case hasSkippedPinEntry
|
||
// Legacy naming
|
||
case hasGivenUpTryingToRestoreWithSVR = "hasGivenUpTryingToRestoreWithKBS"
|
||
case sessionState
|
||
case accountIdentity
|
||
case didRefreshOneTimePreKeys
|
||
case hasDeclinedTransfer
|
||
}
|
||
}
|
||
|
||
private var _persistedState: PersistedState?
|
||
private var persistedState: PersistedState { return _persistedState ?? PersistedState() }
|
||
|
||
private func updatePersistedState(_ transaction: DBWriteTransaction, _ update: (inout PersistedState) -> Void) {
|
||
var state: PersistedState = persistedState
|
||
update(&state)
|
||
self._persistedState = state
|
||
try? self.kvStore.setCodable(state, key: Constants.persistedStateKey, transaction: transaction)
|
||
}
|
||
|
||
private func updatePersistedSessionState(
|
||
session: RegistrationSession,
|
||
_ transaction: DBWriteTransaction,
|
||
_ update: (inout PersistedState.SessionState) -> Void
|
||
) {
|
||
updatePersistedState(transaction) {
|
||
var sessionState = $0.sessionState ?? .init(sessionId: session.id)
|
||
if sessionState.sessionId != session.id {
|
||
self.resetSession(transaction)
|
||
sessionState = .init(sessionId: session.id)
|
||
}
|
||
update(&sessionState)
|
||
$0.sessionState = sessionState
|
||
}
|
||
}
|
||
|
||
/// Once per in memory instantiation of this class, we need to do a few things:
|
||
///
|
||
/// * Reload any persisted state from the key value store (from then on we can just use our
|
||
/// in memory copy because its internal to this class and therefore can't change on disk any other way)
|
||
///
|
||
/// * Pull in any "in memory" state so we get a one-time snapshot of this state at the start of registration.
|
||
/// e.g. we ask KeyBackupService for any SVR data so we know whether to attempt registration
|
||
/// via registration recovery password (if present) or via SMS (if not).
|
||
/// We don't want to check this on the fly because if we went down the SMS path we'd eventually
|
||
/// recover our SVR data, but we'd want to stick to the SMS registration path and NOT revert to
|
||
/// the registration recovery password path, which would cause us to repeat work. So we only
|
||
/// grab a snapshot at the start and use that exclusively for state determination.
|
||
private func restoreStateIfNeeded() -> Guarantee<Void> {
|
||
if inMemoryState.hasRestoredState {
|
||
return .value(())
|
||
}
|
||
|
||
// This is best effort; if we fail to parse the consequences will be a restarted
|
||
// registration, which is recoverable by the user (but annoying because they have
|
||
// to repeat some steps).
|
||
_persistedState = db.read {
|
||
try? self.kvStore.getCodableValue(forKey: Constants.persistedStateKey, transaction: $0)
|
||
}
|
||
|
||
// Ideally this would be in the below transaction, but OWSProfileManager
|
||
// isn't set up to do that and its a mess to untangle.
|
||
self.loadProfileState()
|
||
|
||
db.write { tx in
|
||
self.loadLocalMasterKeyAndUpdateState(tx)
|
||
inMemoryState.tsRegistrationState = deps.tsAccountManager.registrationState(tx: tx)
|
||
inMemoryState.pinFromDisk = deps.ows2FAManager.pinCode(tx)
|
||
if
|
||
inMemoryState.pinFromDisk != nil,
|
||
deps.svr.hasBackedUpMasterKey(transaction: tx).negated
|
||
{
|
||
// If we had a pin but no SVR backups, we must be a v1 2fa user.
|
||
inMemoryState.isV12faUser = true
|
||
}
|
||
|
||
loadSVRAuthCredentialCandidates(tx)
|
||
inMemoryState.isManualMessageFetchEnabled = deps.tsAccountManager.isManualMessageFetchEnabled(tx: tx)
|
||
inMemoryState.registrationId = deps.tsAccountManager.getOrGenerateAciRegistrationId(tx: tx)
|
||
inMemoryState.pniRegistrationId = deps.tsAccountManager.getOrGeneratePniRegistrationId(tx: tx)
|
||
|
||
inMemoryState.allowUnrestrictedUD = deps.udManager.shouldAllowUnrestrictedAccessLocal(transaction: tx)
|
||
|
||
inMemoryState.wasReglockEnabledBeforeStarting = deps.ows2FAManager.isReglockEnabled(tx)
|
||
}
|
||
|
||
switch mode {
|
||
case .reRegistering(let reregState):
|
||
if let persistedE164 = persistedState.e164, reregState.e164 != persistedE164 {
|
||
// This exists to catch a bug released in version 6.19, where
|
||
// the phone number view controller would incorrectly inject a
|
||
// leading 0 into phone numbers from certain national codes.
|
||
// That new number would then be written to persisted state.
|
||
// To recover these users, we wipe their entire persisted state
|
||
// and restart registration from scratch with fresh state.
|
||
db.write { tx in
|
||
self.resetSession(tx)
|
||
self.wipePersistedState(tx)
|
||
}
|
||
return self.restoreStateIfNeeded()
|
||
}
|
||
case .registering, .changingNumber:
|
||
break
|
||
}
|
||
|
||
let sessionGuarantee: Guarantee<Void> = deps.sessionManager.restoreSession()
|
||
.map(on: schedulers.main) { [weak self] session in
|
||
self?.db.write { self?.processSession(session, $0) }
|
||
}
|
||
|
||
let permissionsGuarantee: Guarantee<Void> = requiresSystemPermissions()
|
||
.map(on: schedulers.main) { [weak self] needsPermissions in
|
||
self?.inMemoryState.needsSomePermissions = needsPermissions
|
||
}
|
||
|
||
return Guarantee.when(resolved: sessionGuarantee, permissionsGuarantee).asVoid()
|
||
.done(on: schedulers.main) { [weak self] in
|
||
self?.inMemoryState.hasRestoredState = true
|
||
}
|
||
}
|
||
|
||
/// Once registration is complete, we need to take our internal state and write it out to
|
||
/// external classes so that the rest of the app has all our updated information.
|
||
/// Once this is done, we can wipe the internal state of this class so that we get a fresh
|
||
/// registration if we ever re-register while in the same app session.
|
||
private func exportAndWipeState(accountIdentity: AccountIdentity) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
|
||
func finalizeRegistration(_ tx: DBWriteTransaction) {
|
||
if
|
||
inMemoryState.hasBackedUpToSVR
|
||
|| inMemoryState.didHaveSVRBackupsPriorToReg
|
||
|| inMemoryState.hasRestoredFromLocalMessageBackup
|
||
{
|
||
// No need to show the experience if we made the pin
|
||
// and backed up.
|
||
deps.experienceManager.clearIntroducingPinsExperience(tx)
|
||
}
|
||
|
||
if !deps.svr.hasMasterKey(transaction: tx) {
|
||
// If we don't have a master key at this point, use a local master key.
|
||
deps.svr.useDeviceLocalMasterKey(
|
||
authedAccount: accountIdentity.authedAccount,
|
||
transaction: tx
|
||
)
|
||
}
|
||
|
||
deps.registrationStateChangeManager.didRegisterPrimary(
|
||
e164: accountIdentity.e164,
|
||
aci: accountIdentity.aci,
|
||
pni: accountIdentity.pni,
|
||
authToken: accountIdentity.authPassword,
|
||
tx: tx
|
||
)
|
||
deps.tsAccountManager.setIsManualMessageFetchEnabled(inMemoryState.isManualMessageFetchEnabled, tx: tx)
|
||
}
|
||
|
||
func setupContactsAndFinish() -> Guarantee<RegistrationStep> {
|
||
// Start syncing system contacts now that we have set up tsAccountManager.
|
||
deps.contactsManager.fetchSystemContactsOnceIfAlreadyAuthorized()
|
||
|
||
// Update the account attributes once, now, at the end.
|
||
return updateAccountAttributesAndFinish(accountIdentity: accountIdentity)
|
||
}
|
||
|
||
switch mode {
|
||
case .registering:
|
||
db.write { tx in
|
||
/// For new registrations, we want to force-set some state.
|
||
|
||
/// Read receipts should be on by default.
|
||
deps.receiptManager.setAreReadReceiptsEnabled(true, tx)
|
||
deps.receiptManager.setAreStoryViewedReceiptsEnabled(true, tx)
|
||
|
||
/// Enable the onboarding banner cards.
|
||
deps.experienceManager.enableAllGetStartedCards(tx)
|
||
|
||
/// Disable PNI Hello World operations – these aren't necessary
|
||
/// since we are the only device and know that our
|
||
/// just-generated our PNI identity key is correct.
|
||
deps.pniHelloWorldManager.markHelloWorldAsUnnecessary(tx: tx)
|
||
|
||
finalizeRegistration(tx)
|
||
}
|
||
return setupContactsAndFinish()
|
||
|
||
case .reRegistering:
|
||
db.write(block: finalizeRegistration)
|
||
return setupContactsAndFinish()
|
||
|
||
case .changingNumber(let changeNumberState):
|
||
if let pniState = changeNumberState.pniState {
|
||
return finalizeChangeNumberPniState(
|
||
changeNumberState: changeNumberState,
|
||
pniState: pniState,
|
||
accountIdentity: accountIdentity
|
||
).then(on: schedulers.main) { [weak self] result in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
switch result {
|
||
case .success:
|
||
return self.updateAccountAttributesAndFinish(accountIdentity: accountIdentity)
|
||
case .unretainedSelf:
|
||
return unretainedSelfError()
|
||
case .genericError:
|
||
return .value(.showErrorSheet(.genericError))
|
||
}
|
||
}
|
||
} else {
|
||
return updateAccountAttributesAndFinish(accountIdentity: accountIdentity)
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
private func updateAccountAttributesAndFinish(
|
||
accountIdentity: AccountIdentity,
|
||
retriesLeft: Int = Constants.networkErrorRetries
|
||
) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
|
||
return updateAccountAttributes(accountIdentity)
|
||
.then(on: schedulers.main) { [weak self] error -> Guarantee<RegistrationStep> in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
if
|
||
let error,
|
||
error.isNetworkFailureOrTimeout,
|
||
retriesLeft > 0
|
||
{
|
||
return self.updateAccountAttributesAndFinish(
|
||
accountIdentity: accountIdentity,
|
||
retriesLeft: retriesLeft - 1
|
||
)
|
||
}
|
||
// If we have a deregistration erorr, it doesn't matter. we are finished
|
||
// and cleaning up anyway, the main app will discover the issue.
|
||
if let error {
|
||
Logger.warn("Failed account attributes update, finishing registration anyway: \(error)")
|
||
}
|
||
// We are done! Wipe everything
|
||
self.inMemoryState = InMemoryState()
|
||
self.db.write { tx in
|
||
self.wipePersistedState(tx)
|
||
}
|
||
// Do any storage service backups we have pending.
|
||
self.deps.storageServiceManager.backupPendingChanges(
|
||
authedDevice: accountIdentity.authedDevice
|
||
)
|
||
return .value(.done)
|
||
}
|
||
}
|
||
|
||
private func wipePersistedState(_ tx: DBWriteTransaction) {
|
||
Logger.info("")
|
||
|
||
self.kvStore.removeValue(forKey: Constants.persistedStateKey, transaction: tx)
|
||
self.loader.clearPersistedMode(transaction: tx)
|
||
}
|
||
|
||
// MARK: - Pathway
|
||
|
||
/// A pathway is a (internal to this class) way of splitting up the distinct sections
|
||
/// of registration to make this class a little more modular. Different pathways
|
||
/// still share state and interact with each other in subtle ways, but for the most
|
||
/// part are independent sequences.
|
||
private enum Pathway {
|
||
/// The first few screens before we try and register.
|
||
/// (basically, the splash and systems permissions screens)
|
||
case opening
|
||
/// Attempting to register using the reg recovery password
|
||
/// derived from the SVR master key.
|
||
case registrationRecoveryPassword(password: String)
|
||
/// Attempting to recover from SVR auth credentials
|
||
/// which let us talk to SVR server, recover the master key,
|
||
/// and swap to the registrationRecoveryPassword path.
|
||
case svrAuthCredential(SVRAuthCredential)
|
||
/// We might have un-verified SVR auth credentials
|
||
/// synced from another device; first we need to check them
|
||
/// with the server and then potentially go to the svrAuthCredential path.
|
||
case svrAuthCredentialCandidates([SVR2AuthCredential])
|
||
/// Verifying via SMS code using a `RegistrationSession`.
|
||
/// Used as a fallback if the above paths are unavailable or fail.
|
||
case session(RegistrationSession)
|
||
/// After registration is done, all the steps involving setting up
|
||
/// profile state (which may not be needed). Profile name,
|
||
/// setting up a PIN, etc.
|
||
case profileSetup(AccountIdentity)
|
||
|
||
var logSafeString: String {
|
||
switch self {
|
||
case .opening: return "opening"
|
||
case .registrationRecoveryPassword: return "registrationRecoveryPassword"
|
||
case .svrAuthCredential: return "svrAuthCredential"
|
||
case .svrAuthCredentialCandidates: return "svrAuthCredentialCandidates"
|
||
case .session: return "session"
|
||
case .profileSetup: return "profileSetup"
|
||
}
|
||
}
|
||
}
|
||
|
||
private func getPathway() -> Pathway {
|
||
if splashStepToShow() != nil || inMemoryState.needsSomePermissions {
|
||
return .opening
|
||
}
|
||
if let session = inMemoryState.session {
|
||
// If we have a session, always use that. We might have obtained SVR
|
||
// credentials midway through a session (if we failed reglock when
|
||
// trying to create the account with the session) so we don't want
|
||
// their presence to override the session path.
|
||
|
||
// Conversely, to get off the session path and keep going
|
||
// to e.g. the profile setup, we _must_ clear out the session.
|
||
return .session(session)
|
||
}
|
||
if let accountIdentity = persistedState.accountIdentity {
|
||
// If we have an account identity, that means we already registered
|
||
// or changed number, and we may need to do profile setup.
|
||
// That path may finish right away if we have nothing to set up.
|
||
return .profileSetup(accountIdentity)
|
||
}
|
||
// These paths are only available if the user knows their PIN.
|
||
// If they skipped because they don't know it (or exhausted their guesses),
|
||
// don't bother with them.
|
||
if !persistedState.hasSkippedPinEntry {
|
||
if let password = inMemoryState.regRecoveryPw {
|
||
// If we have a reg recover password (but no session), try using that
|
||
// to register.
|
||
// Once again, to get off this path and fall back to session (if it fails)
|
||
// or proceed to profile setup (if it succeeds) we must wipe this state.
|
||
return .registrationRecoveryPassword(password: password)
|
||
}
|
||
if let credential = inMemoryState.svrAuthCredential {
|
||
// If we have a validated SVR auth credential, try using that
|
||
// to recover the SVR master key to register.
|
||
// Once again, to get off this path and fall back to session (if it fails)
|
||
// or proceed to reg recovery pw (if it succeeds) we must wipe this state.
|
||
return .svrAuthCredential(credential)
|
||
}
|
||
if
|
||
let svr2AuthCredentialCandidates = inMemoryState.svr2AuthCredentialCandidates,
|
||
!svr2AuthCredentialCandidates.isEmpty
|
||
{
|
||
// If we have un-vetted candidates, try checking those first
|
||
// and then going to the svrAuthCredential path if one is valid.
|
||
return .svrAuthCredentialCandidates(
|
||
svr2AuthCredentialCandidates
|
||
)
|
||
}
|
||
}
|
||
|
||
// If we have no state to pull from whatsoever, go to the opening.
|
||
return .opening
|
||
|
||
}
|
||
|
||
private func nextStep(pathway: Pathway) -> Guarantee<RegistrationStep> {
|
||
Logger.info("Going to next step for \(pathway.logSafeString) pathway")
|
||
|
||
switch pathway {
|
||
case .opening:
|
||
return nextStepForOpeningPath()
|
||
case .registrationRecoveryPassword(let password):
|
||
return nextStepForRegRecoveryPasswordPath(regRecoveryPw: password)
|
||
case .svrAuthCredential(let credential):
|
||
return nextStepForSVRAuthCredentialPath(svrAuthCredential: credential)
|
||
case .svrAuthCredentialCandidates(let svr2Candidates):
|
||
return nextStepForSVRAuthCredentialCandidatesPath(
|
||
svr2AuthCredentialCandidates: svr2Candidates
|
||
)
|
||
case .session(let session):
|
||
return nextStepForSessionPath(session)
|
||
case .profileSetup(let accountIdentity):
|
||
return nextStepForProfileSetup(accountIdentity)
|
||
}
|
||
}
|
||
|
||
// MARK: - Opening Pathway
|
||
|
||
private func nextStepForOpeningPath() -> Guarantee<RegistrationStep> {
|
||
if let splashStep = splashStepToShow() {
|
||
return .value(splashStep)
|
||
}
|
||
if inMemoryState.needsSomePermissions {
|
||
// This class is only used for primary device registration
|
||
// which always needs contacts permissions.
|
||
return .value(.permissions)
|
||
}
|
||
if inMemoryState.hasEnteredE164, let e164 = persistedState.e164 {
|
||
return self.startSession(e164: e164)
|
||
}
|
||
return .value(.phoneNumberEntry(phoneNumberEntryState()))
|
||
}
|
||
|
||
private func splashStepToShow() -> RegistrationStep? {
|
||
if persistedState.hasShownSplash {
|
||
return nil
|
||
}
|
||
switch mode {
|
||
case .registering:
|
||
if persistedState.shouldSkipRegistrationSplash {
|
||
return nil
|
||
}
|
||
return .registrationSplash
|
||
case .changingNumber:
|
||
return .changeNumberSplash
|
||
case .reRegistering:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
// MARK: - Registration Recovery Password Pathway
|
||
|
||
/// If we have the SVR master key saved locally (e.g. this is re-registration), we can generate the
|
||
/// "Registration Recovery Password" from it, which we can use as an alternative to a verified SMS code session
|
||
/// to register. This path returns the steps to complete that flow.
|
||
private func nextStepForRegRecoveryPasswordPath(regRecoveryPw: String) -> Guarantee<RegistrationStep> {
|
||
// We need a phone number to proceed; ask the user if unavailable.
|
||
guard let e164 = persistedState.e164 else {
|
||
return .value(.phoneNumberEntry(phoneNumberEntryState()))
|
||
}
|
||
|
||
guard let pinFromUser = inMemoryState.pinFromUser else {
|
||
// We need the user to confirm their pin.
|
||
return .value(.pinEntry(RegistrationPinState(
|
||
// We can skip which will stop trying to use reg recovery.
|
||
operation: .enteringExistingPin(skippability: .canSkip, remainingAttempts: nil),
|
||
error: nil,
|
||
contactSupportMode: self.contactSupportRegistrationPINMode(),
|
||
exitConfiguration: pinCodeEntryExitConfiguration()
|
||
)))
|
||
}
|
||
|
||
if
|
||
let pinFromDisk = inMemoryState.pinFromDisk,
|
||
pinFromDisk != pinFromUser
|
||
{
|
||
Logger.warn("PIN mismatch; should be prevented at submission time.")
|
||
return .value(.pinEntry(RegistrationPinState(
|
||
operation: .enteringExistingPin(skippability: .canSkip, remainingAttempts: nil),
|
||
error: .wrongPin(wrongPin: pinFromUser),
|
||
contactSupportMode: self.contactSupportRegistrationPINMode(),
|
||
exitConfiguration: pinCodeEntryExitConfiguration()
|
||
)))
|
||
}
|
||
|
||
if inMemoryState.needsToAskForDeviceTransfer && !persistedState.hasDeclinedTransfer {
|
||
return .value(.transferSelection)
|
||
}
|
||
|
||
// Attempt to register right away with the password.
|
||
return registerForRegRecoveryPwPath(
|
||
regRecoveryPw: regRecoveryPw,
|
||
e164: e164,
|
||
pinFromUser: pinFromUser
|
||
)
|
||
}
|
||
|
||
private func registerForRegRecoveryPwPath(
|
||
regRecoveryPw: String,
|
||
e164: E164,
|
||
pinFromUser: String,
|
||
retriesLeft: Int = Constants.networkErrorRetries
|
||
) -> Guarantee<RegistrationStep> {
|
||
let twoFAMode = self.attributes2FAMode(e164: e164)
|
||
return self.makeRegisterOrChangeNumberRequest(
|
||
.recoveryPassword(regRecoveryPw),
|
||
e164: e164,
|
||
twoFAMode: twoFAMode,
|
||
responseHandler: { [weak self] accountResponse in
|
||
return self?.handleCreateAccountResponseFromRegRecoveryPassword(
|
||
accountResponse,
|
||
regRecoveryPw: regRecoveryPw,
|
||
e164: e164,
|
||
pinFromUser: pinFromUser,
|
||
twoFaModeUsedInRequest: twoFAMode,
|
||
retriesLeft: retriesLeft
|
||
) ?? unretainedSelfError()
|
||
}
|
||
)
|
||
}
|
||
|
||
private func handleCreateAccountResponseFromRegRecoveryPassword(
|
||
_ response: AccountResponse,
|
||
regRecoveryPw: String,
|
||
e164: E164,
|
||
pinFromUser: String,
|
||
twoFaModeUsedInRequest: AccountAttributes.TwoFactorAuthMode,
|
||
retriesLeft: Int
|
||
) -> Guarantee<RegistrationStep> {
|
||
// NOTE: it is not possible for our e164 to be rejected here; the entire request
|
||
// may be rejected for being malformed, but if the e164 is invalidly formatted
|
||
// that will just look to the server like our reg recovery password is incorrect.
|
||
// This shouldn't be possible in practice; we get here either by having had an
|
||
// e164 from a previously registered account on this device, or by getting
|
||
// confirmation from the auth credential check endpoint that the e164 was good.
|
||
switch response {
|
||
case .success(let identityResponse):
|
||
// We have succeeded! Set the account identity response
|
||
// so nextStep() will take us to the profile setup path.
|
||
db.write { tx in
|
||
updatePersistedState(tx) {
|
||
$0.accountIdentity = identityResponse
|
||
}
|
||
}
|
||
return nextStep()
|
||
|
||
case .reglockFailure:
|
||
switch twoFaModeUsedInRequest {
|
||
case .none, .v1:
|
||
// We failed reglock because we didn't even try it!
|
||
// Try again with reglock included this time.
|
||
db.write { tx in
|
||
self.updatePersistedState(tx) {
|
||
$0.e164WithKnownReglockEnabled = e164
|
||
}
|
||
}
|
||
return nextStep()
|
||
case .v2:
|
||
// We tried our reglock token and it failed.
|
||
switch self.mode {
|
||
case .registering, .reRegistering:
|
||
// Both the reglock and the reg recovery password are derived from the SVR master key.
|
||
// Its weird that we'd get this response implying the recovery password is right
|
||
// but the reglock token is wrong, but lets assume our SVR master secret is just
|
||
// wrong entirely and reset _all_ SVR state so we go through sms verification.
|
||
db.write { tx in
|
||
// We want to wipe credentials on disk too; we don't want to retry it on next app launch.
|
||
// Its possible we tried svr2 and kbs has the right info, or vice versa, but this is all
|
||
// best effort anyway; just fall back to session-based registration.
|
||
deps.svrAuthCredentialStore.removeSVR2CredentialsForCurrentUser(tx)
|
||
// Clear the SVR master key locally; we failed reglock so we know its wrong
|
||
// and useless anyway.
|
||
deps.svr.clearKeys(transaction: tx)
|
||
deps.ows2FAManager.clearLocalPinCode(tx)
|
||
self.updatePersistedState(tx) {
|
||
$0.e164WithKnownReglockEnabled = e164
|
||
}
|
||
}
|
||
case .changingNumber:
|
||
db.write { tx in
|
||
// If changing number we don't wanna wipe our SVR data;
|
||
// its still good for the previous number. just note the reglock
|
||
// and keep going.
|
||
self.updatePersistedState(tx) {
|
||
$0.e164WithKnownReglockEnabled = e164
|
||
}
|
||
}
|
||
}
|
||
// If changing number, we never want to wipe local our SVR secret.
|
||
// Just pretend we don't have it by wiping
|
||
|
||
wipeInMemoryStateToPreventSVRPathAttempts()
|
||
|
||
// Start a session so we go down that path to recovery, challenging
|
||
// the reglock we just failed so we can eventually get in.
|
||
return startSession(e164: e164)
|
||
}
|
||
|
||
case .rejectedVerificationMethod:
|
||
// The reg recovery password was wrong. This can happen for two reasons:
|
||
// 1) We have the wrong SVR master key
|
||
// 2) We have been reglock challenged, forcing us to re-register via session
|
||
// If it were just the former case, we'd wipe our known-wrong SVR master key.
|
||
// But the latter case means we want to go through session path registration,
|
||
// and re-upload our local SVR master secret, so we don't want to wipe it.
|
||
// (If we wiped it and our SVR server guesses were consumed by the reglock-challenger,
|
||
// we'd be outta luck and have no way to recover).
|
||
db.write { tx in
|
||
// We do want to clear out any credentials permanently; we know we
|
||
// have to use the session path so credentials aren't helpful.
|
||
if let svr2Credential = inMemoryState.svrAuthCredential {
|
||
deps.svrAuthCredentialStore.deleteInvalidCredentials([svr2Credential], tx)
|
||
}
|
||
}
|
||
// Wipe our in memory SVR state; its now useless.
|
||
wipeInMemoryStateToPreventSVRPathAttempts()
|
||
|
||
// Now we have to start a session; its the only way to recover.
|
||
return self.startSession(e164: e164)
|
||
|
||
case .retryAfter(let timeInterval):
|
||
if timeInterval < Constants.autoRetryInterval {
|
||
return Guarantee
|
||
.after(on: self.schedulers.global(), seconds: timeInterval)
|
||
.then(on: self.schedulers.sync) { [weak self] in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
return self.registerForRegRecoveryPwPath(
|
||
regRecoveryPw: regRecoveryPw,
|
||
e164: e164,
|
||
pinFromUser: pinFromUser
|
||
)
|
||
}
|
||
}
|
||
// If we get a long timeout, just give up and fall back to the session
|
||
// path. Reg recovery password based recovery is best effort anyway.
|
||
// Besides since this is always our first attempt at registering,
|
||
// this lockout should never happen.
|
||
Logger.error("Rate limited when registering via recovery password; falling back to session.")
|
||
wipeInMemoryStateToPreventSVRPathAttempts()
|
||
return self.startSession(e164: e164)
|
||
|
||
case .deviceTransferPossible:
|
||
// Device transfer can happen, let the user pick.
|
||
inMemoryState.needsToAskForDeviceTransfer = true
|
||
return nextStep()
|
||
|
||
case .networkError:
|
||
if retriesLeft > 0 {
|
||
return registerForRegRecoveryPwPath(
|
||
regRecoveryPw: regRecoveryPw,
|
||
e164: e164,
|
||
pinFromUser: pinFromUser,
|
||
retriesLeft: retriesLeft - 1
|
||
)
|
||
}
|
||
return .value(.showErrorSheet(.networkError))
|
||
|
||
case .genericError:
|
||
return .value(.showErrorSheet(.genericError))
|
||
}
|
||
}
|
||
|
||
private func loadSVRAuthCredentialCandidates(_ tx: DBReadTransaction) {
|
||
let svr2AuthCredentialCandidates: [SVR2AuthCredential] = deps.svrAuthCredentialStore.getAuthCredentials(tx)
|
||
if svr2AuthCredentialCandidates.isEmpty.negated {
|
||
inMemoryState.svr2AuthCredentialCandidates = svr2AuthCredentialCandidates
|
||
}
|
||
}
|
||
|
||
private func wipeInMemoryStateToPreventSVRPathAttempts() {
|
||
inMemoryState.regRecoveryPw = nil
|
||
inMemoryState.shouldRestoreSVRMasterKeyAfterRegistration = true
|
||
// Wiping auth credential state too; if we failed with the local
|
||
// SVR master key we don't expect the backed up master key to work
|
||
// either so we shouldn't bother trying.
|
||
inMemoryState.svrAuthCredential = nil
|
||
inMemoryState.svr2AuthCredentialCandidates = nil
|
||
}
|
||
|
||
// MARK: - SVR Auth Credential Pathway
|
||
|
||
/// If we don't have the SVR master key saved locally but we do have a SVR auth credential,
|
||
/// we can use it to talk to the SVR server and, together with the user-entered PIN, recover the
|
||
/// full SVR master key. Then we use the Registration Recovery Password registration flow.
|
||
/// (If we had the SVR master key saved locally to begin with, we would have just used it right away.)
|
||
private func nextStepForSVRAuthCredentialPath(
|
||
svrAuthCredential: SVRAuthCredential
|
||
) -> Guarantee<RegistrationStep> {
|
||
guard let pin = inMemoryState.pinFromUser else {
|
||
// We don't have a pin at all, ask the user for it.
|
||
return .value(.pinEntry(RegistrationPinState(
|
||
operation: .enteringExistingPin(skippability: .canSkip, remainingAttempts: nil),
|
||
error: nil,
|
||
contactSupportMode: self.contactSupportRegistrationPINMode(),
|
||
exitConfiguration: pinCodeEntryExitConfiguration()
|
||
)))
|
||
}
|
||
|
||
return restoreSVRMasterSecretForAuthCredentialPath(
|
||
pin: pin,
|
||
credential: svrAuthCredential
|
||
)
|
||
}
|
||
|
||
private func restoreSVRMasterSecretForAuthCredentialPath(
|
||
pin: String,
|
||
credential: SVRAuthCredential,
|
||
retriesLeft: Int = Constants.networkErrorRetries
|
||
) -> Guarantee<RegistrationStep> {
|
||
deps.svr.restoreKeys(pin: pin, authMethod: .svrAuth(credential, backup: nil))
|
||
.then(on: schedulers.main) { [weak self] result -> Guarantee<RegistrationStep> in
|
||
guard let self = self else {
|
||
return unretainedSelfError()
|
||
}
|
||
switch result {
|
||
case .success:
|
||
self.db.write { self.loadLocalMasterKeyAndUpdateState($0) }
|
||
return self.nextStep()
|
||
case let .invalidPin(remainingAttempts):
|
||
return .value(.pinEntry(RegistrationPinState(
|
||
operation: .enteringExistingPin(
|
||
skippability: .canSkip,
|
||
remainingAttempts: UInt(remainingAttempts)
|
||
),
|
||
error: .wrongPin(wrongPin: pin),
|
||
contactSupportMode: self.contactSupportRegistrationPINMode(),
|
||
exitConfiguration: self.pinCodeEntryExitConfiguration()
|
||
)))
|
||
case .backupMissing:
|
||
// If we are unable to talk to SVR, it got wiped and we can't
|
||
// recover. Give it all up and wipe our SVR info.
|
||
self.wipeInMemoryStateToPreventSVRPathAttempts()
|
||
self.inMemoryState.pinFromUser = nil
|
||
self.db.write { tx in
|
||
self.updatePersistedState(tx) {
|
||
$0.hasGivenUpTryingToRestoreWithSVR = true
|
||
}
|
||
}
|
||
return .value(.pinAttemptsExhaustedWithoutReglock(
|
||
.init(mode: .restoringRegistrationRecoveryPassword)
|
||
))
|
||
|
||
case .networkError:
|
||
if retriesLeft > 0 {
|
||
return self.restoreSVRMasterSecretForAuthCredentialPath(
|
||
pin: pin,
|
||
credential: credential,
|
||
retriesLeft: retriesLeft - 1
|
||
)
|
||
}
|
||
return .value(.showErrorSheet(.networkError))
|
||
case .genericError:
|
||
if retriesLeft > 0 {
|
||
return self.restoreSVRMasterSecretForAuthCredentialPath(
|
||
pin: pin,
|
||
credential: credential,
|
||
retriesLeft: retriesLeft - 1
|
||
)
|
||
} else {
|
||
self.inMemoryState.pinFromUser = nil
|
||
return .value(.pinEntry(RegistrationPinState(
|
||
operation: .enteringExistingPin(skippability: .canSkip, remainingAttempts: nil),
|
||
error: .serverError,
|
||
contactSupportMode: self.contactSupportRegistrationPINMode(),
|
||
exitConfiguration: pinCodeEntryExitConfiguration()
|
||
)))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private func loadLocalMasterKeyAndUpdateState(_ tx: DBWriteTransaction) {
|
||
let regRecoveryPw = deps.svrKeyDeriver.data(
|
||
for: .registrationRecoveryPassword,
|
||
tx: tx
|
||
)?.canonicalStringRepresentation
|
||
inMemoryState.regRecoveryPw = regRecoveryPw
|
||
if regRecoveryPw != nil {
|
||
updatePersistedState(tx) { $0.shouldSkipRegistrationSplash = true }
|
||
}
|
||
inMemoryState.reglockToken = deps.svrKeyDeriver.data(
|
||
for: .registrationLock,
|
||
tx: tx
|
||
)?.canonicalStringRepresentation
|
||
// If we have a local master key, theres no need to restore after registration.
|
||
// (we will still back up though)
|
||
inMemoryState.shouldRestoreSVRMasterKeyAfterRegistration = !deps.svr.hasMasterKey(transaction: tx)
|
||
inMemoryState.didHaveSVRBackupsPriorToReg = deps.svr.hasBackedUpMasterKey(transaction: tx)
|
||
}
|
||
|
||
// MARK: - SVR Auth Credential Candidates Pathway
|
||
|
||
private func nextStepForSVRAuthCredentialCandidatesPath(
|
||
svr2AuthCredentialCandidates: [SVR2AuthCredential]
|
||
) -> Guarantee<RegistrationStep> {
|
||
guard let e164 = persistedState.e164 else {
|
||
// If we haven't entered a phone number but we have auth
|
||
// credential candidates to check, enter it now.
|
||
return .value(.phoneNumberEntry(phoneNumberEntryState()))
|
||
}
|
||
return makeSVR2AuthCredentialCheckRequest(
|
||
svr2AuthCredentialCandidates: svr2AuthCredentialCandidates,
|
||
e164: e164
|
||
)
|
||
}
|
||
|
||
private func makeSVR2AuthCredentialCheckRequest(
|
||
svr2AuthCredentialCandidates: [SVR2AuthCredential],
|
||
e164: E164,
|
||
retriesLeft: Int = Constants.networkErrorRetries
|
||
) -> Guarantee<RegistrationStep> {
|
||
return Service.makeSVR2AuthCheckRequest(
|
||
e164: e164,
|
||
candidateCredentials: svr2AuthCredentialCandidates,
|
||
signalService: deps.signalService,
|
||
schedulers: schedulers
|
||
).then(on: schedulers.main) { [weak self] response in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
return self.handleSVR2AuthCredentialCheckResponse(
|
||
response,
|
||
svr2AuthCredentialCandidates: svr2AuthCredentialCandidates,
|
||
e164: e164,
|
||
retriesLeft: retriesLeft
|
||
)
|
||
}
|
||
}
|
||
|
||
private func handleSVR2AuthCredentialCheckResponse(
|
||
_ response: Service.SVR2AuthCheckResponse,
|
||
svr2AuthCredentialCandidates: [SVR2AuthCredential],
|
||
e164: E164,
|
||
retriesLeft: Int
|
||
) -> Guarantee<RegistrationStep> {
|
||
var matchedCredential: SVR2AuthCredential?
|
||
var credentialsToDelete = [SVR2AuthCredential]()
|
||
switch response {
|
||
case .networkError:
|
||
if retriesLeft > 0 {
|
||
return makeSVR2AuthCredentialCheckRequest(
|
||
svr2AuthCredentialCandidates: svr2AuthCredentialCandidates,
|
||
e164: e164,
|
||
retriesLeft: retriesLeft - 1
|
||
)
|
||
}
|
||
self.inMemoryState.svr2AuthCredentialCandidates = nil
|
||
return self.nextStep()
|
||
case .genericError:
|
||
// If we failed to verify, wipe the candidates so we don't try again
|
||
// and keep going.
|
||
self.inMemoryState.svr2AuthCredentialCandidates = nil
|
||
return self.nextStep()
|
||
case .success(let response):
|
||
for candidate in svr2AuthCredentialCandidates {
|
||
let result: RegistrationServiceResponses.SVR2AuthCheckResponse.Result? = response.result(for: candidate)
|
||
switch result {
|
||
case .match:
|
||
matchedCredential = candidate
|
||
case .notMatch:
|
||
// Still valid, keep it around but don't use it.
|
||
continue
|
||
case .invalid, .none:
|
||
credentialsToDelete.append(candidate)
|
||
}
|
||
}
|
||
}
|
||
// Wipe the candidates so we don't re-check them.
|
||
self.inMemoryState.svr2AuthCredentialCandidates = nil
|
||
// If this is nil, the next time we call `nextStepForSVRAuthCredentialPath`
|
||
// will just return an empty promise.
|
||
|
||
self.inMemoryState.svrAuthCredential = matchedCredential
|
||
self.db.write { tx in
|
||
self.deps.svrAuthCredentialStore.deleteInvalidCredentials(credentialsToDelete, tx)
|
||
}
|
||
return self.nextStep()
|
||
}
|
||
|
||
// MARK: - RegistrationSession Pathway
|
||
|
||
private func nextStepForSessionPath(_ session: RegistrationSession) -> Guarantee<RegistrationStep> {
|
||
switch persistedState.sessionState?.reglockState ?? .none {
|
||
case .none:
|
||
break
|
||
case let .reglocked(svrAuthCredential, reglockExpirationDate):
|
||
guard let svrAuthCredential = svrAuthCredential.svr2 else {
|
||
// If we don't have a useable credential, we are stuck.
|
||
db.write { tx in
|
||
self.updatePersistedSessionState(session: session, tx) {
|
||
$0.reglockState = .waitingTimeout(expirationDate: reglockExpirationDate)
|
||
}
|
||
}
|
||
return self.nextStep()
|
||
}
|
||
if let pinFromUser = inMemoryState.pinFromUser {
|
||
return restoreSVRMasterSecretForSessionPathReglock(
|
||
session: session,
|
||
pin: pinFromUser,
|
||
svrAuthCredential: svrAuthCredential,
|
||
reglockExpirationDate: reglockExpirationDate
|
||
)
|
||
} else {
|
||
return .value(.pinEntry(RegistrationPinState(
|
||
operation: .enteringExistingPin(
|
||
skippability: .unskippable,
|
||
remainingAttempts: nil
|
||
),
|
||
error: .none,
|
||
contactSupportMode: self.contactSupportRegistrationPINMode(),
|
||
exitConfiguration: pinCodeEntryExitConfiguration()
|
||
)))
|
||
}
|
||
case .waitingTimeout(let reglockExpirationDate):
|
||
if deps.dateProvider() >= reglockExpirationDate {
|
||
// We've passed the time needed and reglock should be expired.
|
||
// Wipe our state and proceed.
|
||
db.write { tx in
|
||
self.updatePersistedSessionState(session: session, tx) {
|
||
$0.reglockState = .none
|
||
}
|
||
}
|
||
return self.nextStep()
|
||
}
|
||
return .value(.reglockTimeout(RegistrationReglockTimeoutState(
|
||
reglockExpirationDate: reglockExpirationDate,
|
||
acknowledgeAction: self.reglockTimeoutAcknowledgeAction
|
||
)))
|
||
}
|
||
|
||
if inMemoryState.needsToAskForDeviceTransfer && !persistedState.hasDeclinedTransfer {
|
||
return .value(.transferSelection)
|
||
}
|
||
|
||
if session.verified {
|
||
// We have to complete registration.
|
||
return self.makeRegisterOrChangeNumberRequestFromSession(session)
|
||
}
|
||
|
||
// We show the code entry screen if we've ever tried sending
|
||
// a verification code, even if that send failed.
|
||
// Note we will re-emit validation errors on every `nextStep()` call,
|
||
// and it is up to the view controller to ignore duplicates.
|
||
let shouldShowCodeEntryStep: Bool
|
||
let codeEntryValidationError: RegistrationVerificationValidationError?
|
||
var pendingCodeTransport = inMemoryState.pendingCodeTransport
|
||
|
||
switch persistedState.sessionState?.initialCodeRequestState {
|
||
case .none:
|
||
shouldShowCodeEntryStep = false
|
||
codeEntryValidationError = nil
|
||
|
||
case .neverRequested:
|
||
shouldShowCodeEntryStep = false
|
||
codeEntryValidationError = nil
|
||
if pendingCodeTransport == nil {
|
||
// If we've never requested a code before, and aren't about to,
|
||
// we should automatically request an sms code.
|
||
pendingCodeTransport = .sms
|
||
}
|
||
|
||
case .requested:
|
||
shouldShowCodeEntryStep = true
|
||
codeEntryValidationError = nil
|
||
|
||
case .smsTransportFailed:
|
||
shouldShowCodeEntryStep = true
|
||
codeEntryValidationError = .failedInitialTransport(failedTransport: .sms)
|
||
case .transientProviderFailure:
|
||
shouldShowCodeEntryStep = true
|
||
codeEntryValidationError = .providerFailure(isPermanent: false)
|
||
case .permanentProviderFailure:
|
||
shouldShowCodeEntryStep = true
|
||
codeEntryValidationError = .providerFailure(isPermanent: true)
|
||
case .exhaustedCodeAttempts:
|
||
shouldShowCodeEntryStep = true
|
||
codeEntryValidationError = .submitCodeTimeout
|
||
case .failedToRequest:
|
||
shouldShowCodeEntryStep = true
|
||
codeEntryValidationError = .genericCodeRequestError(isNetworkError: false)
|
||
}
|
||
|
||
// If we have a pending transport to which we want to send a code,
|
||
// try and do that, regardless of other state.
|
||
if let pendingCodeTransport {
|
||
guard session.allowedToRequestCode else {
|
||
return attemptToFulfillAvailableChallengesWaitingIfNeeded(for: session)
|
||
}
|
||
|
||
// If we have pending transport and can send, send.
|
||
switch pendingCodeTransport {
|
||
case .sms:
|
||
if let nextSMSDate = session.nextSMSDate, nextSMSDate <= deps.dateProvider() {
|
||
return requestSessionCode(session: session, transport: pendingCodeTransport)
|
||
} else {
|
||
// Inability to send puts on the verification entry screen, so the
|
||
// user can try the alternate transport manually.
|
||
return .value(.verificationCodeEntry(self.verificationCodeEntryState(
|
||
session: session,
|
||
validationError: .smsResendTimeout
|
||
)))
|
||
}
|
||
case .voice:
|
||
if let nextCallDate = session.nextCallDate, nextCallDate <= deps.dateProvider() {
|
||
return requestSessionCode(session: session, transport: pendingCodeTransport)
|
||
} else {
|
||
// Inability to send puts on the verification entry screen, so the
|
||
// user can try the alternate transport manually.
|
||
return .value(.verificationCodeEntry(self.verificationCodeEntryState(
|
||
session: session,
|
||
validationError: .voiceResendTimeout
|
||
)))
|
||
}
|
||
}
|
||
}
|
||
|
||
if shouldShowCodeEntryStep {
|
||
return .value(.verificationCodeEntry(self.verificationCodeEntryState(
|
||
session: session,
|
||
validationError: codeEntryValidationError
|
||
)))
|
||
}
|
||
|
||
// Otherwise we have no code awaiting submission and aren't
|
||
// trying to send one yet, so just go to phone number entry.
|
||
return .value(.phoneNumberEntry(phoneNumberEntryState()))
|
||
}
|
||
|
||
private func processSession(
|
||
_ session: RegistrationSession?,
|
||
initialCodeRequestState: PersistedState.SessionState.InitialCodeRequestState? = nil,
|
||
_ transaction: DBWriteTransaction
|
||
) {
|
||
if session == nil || persistedState.sessionState?.sessionId != session?.id {
|
||
self.updatePersistedState(transaction) {
|
||
$0.sessionState = session.map { .init(sessionId: $0.id) }
|
||
}
|
||
}
|
||
var newInitialCodeRequestState = initialCodeRequestState
|
||
if session?.nextVerificationAttempt != nil {
|
||
// If we can submit a code, we must have requested
|
||
// at least once.
|
||
newInitialCodeRequestState = .requested
|
||
}
|
||
let oldInitialCodeRequestState = persistedState.sessionState?.initialCodeRequestState
|
||
switch (oldInitialCodeRequestState, newInitialCodeRequestState) {
|
||
case
|
||
(.none, _),
|
||
(.smsTransportFailed, _),
|
||
(.transientProviderFailure, _),
|
||
(.permanentProviderFailure, _),
|
||
(.failedToRequest, _),
|
||
(.neverRequested, _),
|
||
(.exhaustedCodeAttempts, _),
|
||
(.requested, .exhaustedCodeAttempts):
|
||
if let newInitialCodeRequestState, newInitialCodeRequestState != persistedState.sessionState?.initialCodeRequestState {
|
||
self.updatePersistedState(transaction) {
|
||
var sessionState = $0.sessionState
|
||
sessionState?.initialCodeRequestState = newInitialCodeRequestState
|
||
$0.sessionState = sessionState
|
||
}
|
||
}
|
||
case (.requested, _):
|
||
// Don't overwrite already requested state under any circumstances.
|
||
break
|
||
}
|
||
|
||
if session?.verified == true {
|
||
// Any verified session is good and we should keep it.
|
||
inMemoryState.session = session
|
||
return
|
||
}
|
||
|
||
if
|
||
let session,
|
||
// If we can't submit a code...
|
||
session.nextVerificationAttempt == nil,
|
||
// Can't request a code (and can't do any challenges to move on)...
|
||
(!session.allowedToRequestCode && session.requestedInformation.isEmpty),
|
||
// And have exhausted our ability to request codes...
|
||
session.nextSMS == nil,
|
||
session.nextCall == nil
|
||
{
|
||
// Then this session is incapable of being verified, and we should
|
||
// discard it.
|
||
|
||
// UNLESS it has an unknown challenge type on it.
|
||
// In this case, the session might still be good, and we want to
|
||
// alert the user instead of discarding.
|
||
if session.hasUnknownChallengeRequiringAppUpdate {
|
||
inMemoryState.session = session
|
||
return
|
||
} else {
|
||
self.resetSession(transaction)
|
||
return
|
||
}
|
||
}
|
||
inMemoryState.session = session
|
||
}
|
||
|
||
private func resetSession(_ transaction: DBWriteTransaction) {
|
||
inMemoryState.session = nil
|
||
inMemoryState.pendingCodeTransport = nil
|
||
// Force the user to enter an e164 again
|
||
// when making a new session.
|
||
inMemoryState.hasEnteredE164 = false
|
||
self.updatePersistedState(transaction) {
|
||
$0.sessionState = nil
|
||
}
|
||
self.deps.sessionManager.clearPersistedSession(transaction)
|
||
}
|
||
|
||
private func makeRegisterOrChangeNumberRequestFromSession(
|
||
_ session: RegistrationSession,
|
||
retriesLeft: Int = Constants.networkErrorRetries
|
||
) -> Guarantee<RegistrationStep> {
|
||
if
|
||
let timeoutDate = persistedState.sessionState?.createAccountTimeout,
|
||
deps.dateProvider() < timeoutDate
|
||
{
|
||
return .value(.phoneNumberEntry(phoneNumberEntryState(
|
||
validationError: .rateLimited(.init(
|
||
expiration: timeoutDate,
|
||
e164: session.e164
|
||
))
|
||
)))
|
||
}
|
||
let twoFAMode = self.attributes2FAMode(e164: session.e164)
|
||
return self.makeRegisterOrChangeNumberRequest(
|
||
.sessionId(session.id),
|
||
e164: session.e164,
|
||
twoFAMode: twoFAMode,
|
||
responseHandler: { [weak self] accountResponse in
|
||
return self?.handleCreateAccountResponseFromSession(
|
||
accountResponse,
|
||
sessionFromBeforeRequest: session,
|
||
twoFAModeUsedInRequest: twoFAMode,
|
||
retriesLeft: retriesLeft
|
||
) ?? unretainedSelfError()
|
||
}
|
||
)
|
||
}
|
||
|
||
private func handleCreateAccountResponseFromSession(
|
||
_ response: AccountResponse,
|
||
sessionFromBeforeRequest: RegistrationSession,
|
||
twoFAModeUsedInRequest: AccountAttributes.TwoFactorAuthMode,
|
||
retriesLeft: Int
|
||
) -> Guarantee<RegistrationStep> {
|
||
switch response {
|
||
case .success(let identityResponse):
|
||
inMemoryState.session = nil
|
||
db.write { tx in
|
||
// We can clear the session now!
|
||
deps.sessionManager.clearPersistedSession(tx)
|
||
updatePersistedState(tx) {
|
||
$0.accountIdentity = identityResponse
|
||
$0.sessionState = nil
|
||
}
|
||
}
|
||
// Should take us to the profile setup flow since
|
||
// the identity response is set.
|
||
return nextStep()
|
||
case .reglockFailure(let reglockFailure):
|
||
let reglockExpirationDate = self.deps.dateProvider().addingTimeInterval(TimeInterval(reglockFailure.timeRemainingMs / 1000))
|
||
guard persistedState.hasGivenUpTryingToRestoreWithSVR.negated else {
|
||
// If we have already exhausted our SVR backup attempts, we are stuck.
|
||
db.write { tx in
|
||
// May as well store credentials, anyway.
|
||
deps.svrAuthCredentialStore.storeAuthCredentialForCurrentUsername(
|
||
reglockFailure.svr2AuthCredential,
|
||
tx
|
||
)
|
||
self.updatePersistedSessionState(session: sessionFromBeforeRequest, tx) {
|
||
$0.reglockState = .waitingTimeout(expirationDate: reglockExpirationDate)
|
||
}
|
||
self.updatePersistedState(tx) {
|
||
$0.e164WithKnownReglockEnabled = sessionFromBeforeRequest.e164
|
||
}
|
||
}
|
||
return nextStep()
|
||
}
|
||
// We need the user to enter their PIN so we can get through reglock.
|
||
// So we set up the state we need (the SVR credential)
|
||
// and go to the next step which should look at the state and take us to the right place.
|
||
switch twoFAModeUsedInRequest {
|
||
case .v2:
|
||
// We were already trying reglock, and the token was wrong.
|
||
// that means the whole thing is stuck. wait out the reglock.
|
||
db.write { tx in
|
||
// May as well store credentials, anyway.
|
||
deps.svrAuthCredentialStore.storeAuthCredentialForCurrentUsername(
|
||
reglockFailure.svr2AuthCredential,
|
||
tx
|
||
)
|
||
self.updatePersistedSessionState(session: sessionFromBeforeRequest, tx) {
|
||
$0.reglockState = .waitingTimeout(expirationDate: reglockExpirationDate)
|
||
}
|
||
self.updatePersistedState(tx) {
|
||
$0.e164WithKnownReglockEnabled = sessionFromBeforeRequest.e164
|
||
}
|
||
}
|
||
return nextStep()
|
||
|
||
case .none, .v1:
|
||
let persistedCredential = PersistedState.SessionState.ReglockState.SVRAuthCredential(
|
||
svr2: reglockFailure.svr2AuthCredential
|
||
)
|
||
db.write { tx in
|
||
deps.svrAuthCredentialStore.storeAuthCredentialForCurrentUsername(reglockFailure.svr2AuthCredential, tx)
|
||
self.updatePersistedSessionState(session: sessionFromBeforeRequest, tx) {
|
||
$0.reglockState = .reglocked(
|
||
credential: persistedCredential,
|
||
expirationDate: reglockExpirationDate
|
||
)
|
||
}
|
||
self.updatePersistedState(tx) {
|
||
$0.e164WithKnownReglockEnabled = sessionFromBeforeRequest.e164
|
||
// If we skipped for reg recovery, unskip now.
|
||
$0.hasSkippedPinEntry = false
|
||
}
|
||
}
|
||
return nextStep()
|
||
}
|
||
|
||
case .rejectedVerificationMethod:
|
||
// The session is invalid; we have to wipe it and potentially start again.
|
||
db.write { self.resetSession($0) }
|
||
return nextStep()
|
||
|
||
case .retryAfter(let timeInterval):
|
||
if timeInterval < Constants.autoRetryInterval {
|
||
return Guarantee
|
||
.after(on: schedulers.global(), seconds: timeInterval)
|
||
.then(on: schedulers.sync) { [weak self] in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
return self.makeRegisterOrChangeNumberRequestFromSession(
|
||
sessionFromBeforeRequest
|
||
)
|
||
}
|
||
}
|
||
let timeoutDate = self.deps.dateProvider().addingTimeInterval(timeInterval)
|
||
self.db.write { tx in
|
||
self.updatePersistedSessionState(session: sessionFromBeforeRequest, tx) {
|
||
$0.createAccountTimeout = timeoutDate
|
||
}
|
||
}
|
||
return nextStep()
|
||
case .deviceTransferPossible:
|
||
inMemoryState.needsToAskForDeviceTransfer = true
|
||
return .value(.transferSelection)
|
||
case .networkError:
|
||
if retriesLeft > 0 {
|
||
return makeRegisterOrChangeNumberRequestFromSession(
|
||
sessionFromBeforeRequest,
|
||
retriesLeft: retriesLeft - 1
|
||
)
|
||
}
|
||
return .value(.showErrorSheet(.networkError))
|
||
case .genericError:
|
||
return .value(.showErrorSheet(.genericError))
|
||
}
|
||
}
|
||
|
||
private func startSession(
|
||
e164: E164,
|
||
retriesLeft: Int = Constants.networkErrorRetries
|
||
) -> Guarantee<RegistrationStep> {
|
||
return deps.pushRegistrationManager.requestPushToken()
|
||
.then(on: schedulers.global()) { [weak self] tokenResult -> Guarantee<RegistrationStep> in
|
||
guard let strongSelf = self else {
|
||
return unretainedSelfError()
|
||
}
|
||
let apnsToken: String?
|
||
switch tokenResult {
|
||
case .success(let tokens):
|
||
apnsToken = tokens.apnsToken
|
||
case .pushUnsupported, .timeout, .genericError:
|
||
apnsToken = nil
|
||
}
|
||
return strongSelf.deps.sessionManager.beginOrRestoreSession(
|
||
e164: e164,
|
||
apnsToken: apnsToken
|
||
).then(on: strongSelf.schedulers.main) { [weak self] response -> Guarantee<RegistrationStep> in
|
||
guard let strongSelf = self else {
|
||
return unretainedSelfError()
|
||
}
|
||
switch response {
|
||
case .success(let session):
|
||
strongSelf.db.write { transaction in
|
||
strongSelf.processSession(session, transaction)
|
||
|
||
if apnsToken == nil {
|
||
strongSelf.noPreAuthChallengeTokenWillArrive(
|
||
session: session,
|
||
transaction: transaction
|
||
)
|
||
} else {
|
||
strongSelf.prepareToReceivePreAuthChallengeToken(
|
||
session: session,
|
||
transaction: transaction
|
||
)
|
||
}
|
||
}
|
||
|
||
return strongSelf.nextStep()
|
||
case .invalidArgument:
|
||
return .value(.phoneNumberEntry(strongSelf.phoneNumberEntryState(
|
||
validationError: .invalidE164(.init(invalidE164: e164))
|
||
)))
|
||
case .retryAfter(let timeInterval):
|
||
if timeInterval < Constants.autoRetryInterval {
|
||
return Guarantee
|
||
.after(on: strongSelf.schedulers.global(), seconds: timeInterval)
|
||
.then(on: strongSelf.schedulers.sync) { [weak self] in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
return self.startSession(
|
||
e164: e164
|
||
)
|
||
}
|
||
}
|
||
return .value(.phoneNumberEntry(strongSelf.phoneNumberEntryState(
|
||
validationError: .rateLimited(.init(
|
||
expiration: strongSelf.deps.dateProvider().addingTimeInterval(timeInterval),
|
||
e164: e164
|
||
))
|
||
)))
|
||
case .networkFailure:
|
||
if retriesLeft > 0 {
|
||
return strongSelf.startSession(
|
||
e164: e164,
|
||
retriesLeft: retriesLeft - 1
|
||
)
|
||
}
|
||
return .value(.showErrorSheet(.networkError))
|
||
case .genericError:
|
||
return .value(.showErrorSheet(.genericError))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private func requestSessionCode(
|
||
session: RegistrationSession,
|
||
transport: Registration.CodeTransport,
|
||
retriesLeft: Int = Constants.networkErrorRetries
|
||
) -> Guarantee<RegistrationStep> {
|
||
return deps.sessionManager.requestVerificationCode(
|
||
for: session,
|
||
transport: transport
|
||
).then(on: schedulers.main) { [weak self] (result: Registration.UpdateSessionResponse) -> Guarantee<RegistrationStep> in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
switch result {
|
||
case .success(let session):
|
||
self.inMemoryState.pendingCodeTransport = nil
|
||
self.db.write {
|
||
self.processSession(session, initialCodeRequestState: .requested, $0)
|
||
}
|
||
return self.nextStep()
|
||
case .rejectedArgument(let session):
|
||
Logger.error("Should never get rejected argument error from requesting code. E164 already set on session.")
|
||
// Wipe the pending code request, so we don't retry.
|
||
self.inMemoryState.pendingCodeTransport = nil
|
||
self.db.write {
|
||
self.processSession(session, initialCodeRequestState: .failedToRequest, $0)
|
||
}
|
||
return self.nextStep()
|
||
case .disallowed(let session):
|
||
// Whatever caused this should be represented on the session itself,
|
||
// and once we unblock we should retry sending so don't clear the pending
|
||
// code transport.
|
||
self.db.write { self.processSession(session, $0) }
|
||
return self.nextStep()
|
||
case .transportError(let session):
|
||
// We failed with the current transport, but another transport
|
||
// might work.
|
||
self.db.write { self.processSession(session, initialCodeRequestState: .smsTransportFailed, $0) }
|
||
// Wipe the pending code request, so we don't auto-retry.
|
||
self.inMemoryState.pendingCodeTransport = nil
|
||
return self.nextStep()
|
||
case .invalidSession:
|
||
self.inMemoryState.pendingCodeTransport = nil
|
||
self.db.write { self.resetSession($0) }
|
||
return .value(.showErrorSheet(.sessionInvalidated))
|
||
case .serverFailure(let failureResponse):
|
||
self.db.write { tx in
|
||
self.processSession(
|
||
session,
|
||
initialCodeRequestState: failureResponse.isPermanent
|
||
? .permanentProviderFailure
|
||
: .transientProviderFailure,
|
||
tx
|
||
)
|
||
}
|
||
// Wipe the pending code request, so we don't auto-retry.
|
||
self.inMemoryState.pendingCodeTransport = nil
|
||
return self.nextStep()
|
||
case .retryAfterTimeout(let session):
|
||
let timeInterval: TimeInterval?
|
||
switch transport {
|
||
case .sms:
|
||
timeInterval = session.nextSMS
|
||
case .voice:
|
||
timeInterval = session.nextCall
|
||
}
|
||
if let timeInterval, timeInterval < Constants.autoRetryInterval {
|
||
self.db.write { self.processSession(session, $0) }
|
||
return Guarantee
|
||
.after(on: self.schedulers.global(), seconds: timeInterval)
|
||
.then(on: self.schedulers.sync) { [weak self] in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
return self.requestSessionCode(
|
||
session: session,
|
||
transport: transport
|
||
)
|
||
}
|
||
} else {
|
||
self.inMemoryState.pendingCodeTransport = nil
|
||
if session.nextVerificationAttemptDate != nil {
|
||
self.db.write {
|
||
self.processSession(session, initialCodeRequestState: .requested, $0)
|
||
}
|
||
// Show an error on the verification code entry screen.
|
||
return .value(.verificationCodeEntry(self.verificationCodeEntryState(
|
||
session: session,
|
||
validationError: {
|
||
switch transport {
|
||
case .sms: return .smsResendTimeout
|
||
case .voice: return .voiceResendTimeout
|
||
}
|
||
}()
|
||
)))
|
||
} else if let timeInterval {
|
||
self.db.write {
|
||
self.processSession(session, initialCodeRequestState: .failedToRequest, $0)
|
||
}
|
||
// We were trying to resend from the phone number screen.
|
||
return .value(.phoneNumberEntry(self.phoneNumberEntryState(
|
||
validationError: .rateLimited(.init(
|
||
expiration: self.deps.dateProvider().addingTimeInterval(timeInterval),
|
||
e164: session.e164
|
||
)
|
||
))))
|
||
} else {
|
||
// Can't send a code, session is useless.
|
||
self.db.write { self.resetSession($0) }
|
||
return .value(.showErrorSheet(.sessionInvalidated))
|
||
}
|
||
}
|
||
case .networkFailure:
|
||
if retriesLeft > 0 {
|
||
return self.requestSessionCode(
|
||
session: session,
|
||
transport: transport,
|
||
retriesLeft: retriesLeft - 1
|
||
)
|
||
}
|
||
self.inMemoryState.pendingCodeTransport = nil
|
||
self.db.write {
|
||
self.processSession(session, initialCodeRequestState: .failedToRequest, $0)
|
||
}
|
||
return .value(.showErrorSheet(.networkError))
|
||
case .genericError:
|
||
self.inMemoryState.pendingCodeTransport = nil
|
||
self.db.write {
|
||
self.processSession(session, initialCodeRequestState: .failedToRequest, $0)
|
||
}
|
||
return .value(.showErrorSheet(.genericError))
|
||
}
|
||
}
|
||
}
|
||
|
||
private func noPreAuthChallengeTokenWillArrive(
|
||
session: RegistrationSession,
|
||
transaction: DBWriteTransaction
|
||
) {
|
||
switch persistedState.sessionState?.pushChallengeState {
|
||
case nil, .notRequested, .waitingForPush, .rejected:
|
||
Logger.info("No pre-auth challenge token will arrive. Noting that")
|
||
updatePersistedSessionState(session: session, transaction) {
|
||
$0.pushChallengeState = .ineligible
|
||
}
|
||
case .ineligible, .unfulfilledPush, .fulfilled:
|
||
Logger.info("No pre-auth challenge token will arrive, but we don't need to update our state")
|
||
}
|
||
}
|
||
|
||
private func prepareToReceivePreAuthChallengeToken(
|
||
session: RegistrationSession,
|
||
transaction: DBWriteTransaction
|
||
) {
|
||
switch persistedState.sessionState?.pushChallengeState {
|
||
case nil, .notRequested, .ineligible, .rejected:
|
||
// It's unlikely but possible to go from ineligible -> waiting if the user denied
|
||
// notification permissions, closed the app, re-enabled them in settings, and then
|
||
// relaunched. It's much more likely that we'd be in the "not requested" state.
|
||
Logger.info("Started waiting for a pre-auth challenge token")
|
||
self.updatePersistedSessionState(session: session, transaction) {
|
||
$0.pushChallengeState = .waitingForPush(requestedAt: deps.dateProvider())
|
||
}
|
||
case .waitingForPush, .unfulfilledPush, .fulfilled:
|
||
Logger.info("Already waiting for a pre-auth challenge token, presumably from a prior launch")
|
||
}
|
||
|
||
// There is no timeout on this promise. That's deliberate. If we get a push challenge token
|
||
// at some point, we'd like to hold onto it, even if it took awhile to arrive. Other spots
|
||
// in the code may handle a timeout.
|
||
deps.pushRegistrationManager.receivePreAuthChallengeToken().done(on: schedulers.main) { [weak self] token in
|
||
guard let self else { return }
|
||
self.db.write { transaction in
|
||
self.didReceive(pushChallengeToken: token, for: session, transaction: transaction)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func didReceive(
|
||
pushChallengeToken: String,
|
||
for session: RegistrationSession,
|
||
transaction: DBWriteTransaction
|
||
) {
|
||
deps.pushRegistrationManager.clearPreAuthChallengeToken()
|
||
Logger.info("Received a push challenge token")
|
||
updatePersistedSessionState(session: session, transaction) {
|
||
$0.pushChallengeState = .unfulfilledPush(challengeToken: pushChallengeToken)
|
||
}
|
||
}
|
||
|
||
private func attemptToFulfillAvailableChallengesWaitingIfNeeded(
|
||
for session: RegistrationSession
|
||
) -> Guarantee<RegistrationStep> {
|
||
Logger.info("Found \(session.requestedInformation.count) challenge(s)")
|
||
|
||
var requestsPushChallenge = false
|
||
var requestsCaptchaChallenge = false
|
||
for challenge in session.requestedInformation {
|
||
switch challenge {
|
||
case .pushChallenge: requestsPushChallenge = true
|
||
case .captcha: requestsCaptchaChallenge = true
|
||
}
|
||
}
|
||
|
||
// Our first choice: a push challenge for which we already have the challenge token.
|
||
let unfulfilledPushChallengeToken: String? = {
|
||
switch persistedState.sessionState?.pushChallengeState {
|
||
case nil, .notRequested, .ineligible, .waitingForPush, .fulfilled, .rejected:
|
||
return nil
|
||
case let .unfulfilledPush(challengeToken):
|
||
return challengeToken
|
||
}
|
||
}()
|
||
if requestsPushChallenge, let unfulfilledPushChallengeToken {
|
||
Logger.info("Attempting to fulfill push challenge with a token we already have")
|
||
return submit(
|
||
challengeFulfillment: .pushChallenge(unfulfilledPushChallengeToken),
|
||
for: session
|
||
)
|
||
}
|
||
|
||
func waitForPushTokenChallenge(
|
||
timeout: TimeInterval,
|
||
failChallengeIfTimedOut: Bool
|
||
) -> Guarantee<RegistrationStep> {
|
||
Logger.info("Attempting to fulfill push challenge with a token we don't have yet")
|
||
return deps.pushRegistrationManager
|
||
.receivePreAuthChallengeToken()
|
||
.map { $0 }
|
||
.nilTimeout(on: schedulers.global(), seconds: timeout)
|
||
.then(on: schedulers.global()) { [weak self] (challengeToken: String?) -> Guarantee<RegistrationStep> in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
|
||
if let challengeToken {
|
||
self.db.write { transaction in
|
||
self.didReceive(
|
||
pushChallengeToken: challengeToken,
|
||
for: session,
|
||
transaction: transaction
|
||
)
|
||
}
|
||
return self.submit(
|
||
challengeFulfillment: .pushChallenge(challengeToken),
|
||
for: session
|
||
)
|
||
} else if failChallengeIfTimedOut {
|
||
Logger.warn("No challenge token received in time. Resetting")
|
||
self.db.write { self.resetSession($0) }
|
||
return .value(.showErrorSheet(.sessionInvalidated))
|
||
} else {
|
||
Logger.warn("No challenge token received in time, falling back to next challenge")
|
||
return tryNonImmediatePushChallenge()
|
||
}
|
||
}
|
||
}
|
||
|
||
func tryNonImmediatePushChallenge() -> Guarantee<RegistrationStep> {
|
||
// Our third choice: a captcha challenge
|
||
if requestsCaptchaChallenge {
|
||
Logger.info("Showing the CAPTCHA challenge to the user")
|
||
return .value(.captchaChallenge)
|
||
}
|
||
|
||
// Our fourth choice: a push challenge where we're still waiting for the challenge token.
|
||
if
|
||
requestsPushChallenge,
|
||
let timeToWaitUntil = pushChallengeRequestDate?.addingTimeInterval(Constants.pushTokenTimeout),
|
||
deps.dateProvider() < timeToWaitUntil
|
||
{
|
||
let timeout = timeToWaitUntil.timeIntervalSince(deps.dateProvider())
|
||
return waitForPushTokenChallenge(
|
||
timeout: timeout,
|
||
failChallengeIfTimedOut: true
|
||
)
|
||
}
|
||
|
||
// We're out of luck.
|
||
if session.hasUnknownChallengeRequiringAppUpdate {
|
||
Logger.warn("An unknown challenge was found")
|
||
inMemoryState.pendingCodeTransport = nil
|
||
db.write { tx in
|
||
self.processSession(session, initialCodeRequestState: .failedToRequest, tx)
|
||
}
|
||
return .value(.appUpdateBanner)
|
||
} else {
|
||
Logger.warn("Couldn't fulfill any challenges. Resetting the session")
|
||
db.write { resetSession($0) }
|
||
return nextStep()
|
||
}
|
||
}
|
||
|
||
// Our second choice: a very recent push challenge.
|
||
let pushChallengeRequestDate: Date? = {
|
||
switch persistedState.sessionState?.pushChallengeState {
|
||
case nil, .notRequested, .ineligible, .unfulfilledPush, .fulfilled, .rejected:
|
||
return nil
|
||
case let .waitingForPush(requestedAt):
|
||
return requestedAt
|
||
}
|
||
}()
|
||
if
|
||
requestsPushChallenge,
|
||
let timeToWaitUntil = pushChallengeRequestDate?.addingTimeInterval(Constants.pushTokenMinWaitTime),
|
||
deps.dateProvider() < timeToWaitUntil
|
||
{
|
||
let timeout = timeToWaitUntil.timeIntervalSince(deps.dateProvider())
|
||
return waitForPushTokenChallenge(timeout: timeout, failChallengeIfTimedOut: false)
|
||
}
|
||
|
||
// Try the next choices.
|
||
return tryNonImmediatePushChallenge()
|
||
}
|
||
|
||
private func submit(
|
||
challengeFulfillment fulfillment: Registration.ChallengeFulfillment,
|
||
for session: RegistrationSession,
|
||
retriesLeft: Int = Constants.networkErrorRetries
|
||
) -> Guarantee<RegistrationStep> {
|
||
switch fulfillment {
|
||
case .captcha:
|
||
Logger.info("Submitting CAPTCHA challenge fulfillment")
|
||
case .pushChallenge:
|
||
Logger.info("Submitting push challenge fulfillment")
|
||
}
|
||
|
||
return deps.sessionManager.fulfillChallenge(
|
||
for: session,
|
||
fulfillment: fulfillment
|
||
).then(on: schedulers.main) { [weak self] (result: Registration.UpdateSessionResponse) -> Guarantee<RegistrationStep> in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
switch result {
|
||
case .success(let session):
|
||
self.db.write { tx in
|
||
self.processSession(session, tx)
|
||
switch fulfillment {
|
||
case .captcha: break
|
||
case .pushChallenge:
|
||
self.updatePersistedSessionState(session: session, tx) {
|
||
$0.pushChallengeState = .fulfilled
|
||
}
|
||
}
|
||
}
|
||
return self.nextStep()
|
||
case .rejectedArgument(let session):
|
||
self.db.write { tx in
|
||
self.processSession(session, tx)
|
||
self.updatePersistedSessionState(session: session, tx) {
|
||
$0.pushChallengeState = .rejected
|
||
}
|
||
}
|
||
return .value(.showErrorSheet(.genericError))
|
||
case .disallowed(let session):
|
||
Logger.warn("Disallowed to complete a challenge which should be impossible.")
|
||
// Don't keep trying to send a code.
|
||
self.inMemoryState.pendingCodeTransport = nil
|
||
self.db.write { self.processSession(session, initialCodeRequestState: .failedToRequest, $0) }
|
||
return .value(.showErrorSheet(.genericError))
|
||
case .invalidSession:
|
||
self.db.write { self.resetSession($0) }
|
||
return .value(.showErrorSheet(.sessionInvalidated))
|
||
case .serverFailure(let failureResponse):
|
||
if failureResponse.isPermanent {
|
||
return .value(.showErrorSheet(.genericError))
|
||
} else {
|
||
return .value(.showErrorSheet(.networkError))
|
||
}
|
||
case .retryAfterTimeout(let session):
|
||
Logger.error("Should not have to retry a captcha challenge request")
|
||
// Clear the pending code; we want the user to press again
|
||
// once the timeout expires.
|
||
self.inMemoryState.pendingCodeTransport = nil
|
||
self.db.write { self.processSession(session, initialCodeRequestState: .failedToRequest, $0) }
|
||
self.db.write { self.processSession(session, $0) }
|
||
return self.nextStep()
|
||
case .networkFailure:
|
||
if retriesLeft > 0 {
|
||
return self.submit(
|
||
challengeFulfillment: fulfillment,
|
||
for: session,
|
||
retriesLeft: retriesLeft - 1
|
||
)
|
||
}
|
||
return .value(.showErrorSheet(.networkError))
|
||
case .transportError(let session):
|
||
Logger.error("Should not get a transport error for a challenge request")
|
||
// Clear the pending code; we want the user to press again
|
||
// once the timeout expires.
|
||
self.inMemoryState.pendingCodeTransport = nil
|
||
self.db.write { self.processSession(session, initialCodeRequestState: .failedToRequest, $0) }
|
||
return self.nextStep()
|
||
case .genericError:
|
||
return .value(.showErrorSheet(.genericError))
|
||
}
|
||
}
|
||
}
|
||
|
||
private func submitSessionCode(
|
||
session: RegistrationSession,
|
||
code: String,
|
||
retriesLeft: Int = Constants.networkErrorRetries
|
||
) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
|
||
db.write { tx in
|
||
self.updatePersistedSessionState(session: session, tx) {
|
||
$0.numVerificationCodeSubmissions += 1
|
||
}
|
||
}
|
||
|
||
return deps.sessionManager.submitVerificationCode(
|
||
for: session,
|
||
code: code
|
||
).then(on: schedulers.main) { [weak self] (result: Registration.UpdateSessionResponse) -> Guarantee<RegistrationStep> in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
switch result {
|
||
case .success(let session):
|
||
if !session.verified {
|
||
// The code must have been wrong.
|
||
fallthrough
|
||
}
|
||
self.db.write { self.processSession(session, $0) }
|
||
return self.nextStep()
|
||
case .rejectedArgument(let session):
|
||
if session.nextVerificationAttemptDate != nil {
|
||
self.db.write { self.processSession(session, $0) }
|
||
return .value(.verificationCodeEntry(self.verificationCodeEntryState(
|
||
session: session,
|
||
validationError: .invalidVerificationCode(invalidCode: code)
|
||
)))
|
||
} else {
|
||
// Something went wrong, we can't submit again.
|
||
self.db.write { self.processSession(session, initialCodeRequestState: .exhaustedCodeAttempts, $0) }
|
||
return .value(self.verificationCodeSubmissionRejectedError)
|
||
}
|
||
case .disallowed(let session):
|
||
// This state means the session state is updated
|
||
// such that what comes next has changed, e.g. we can't send a verification
|
||
// code and will kick the user back to sending an sms code.
|
||
self.db.write { self.processSession(session, $0) }
|
||
return .value(self.verificationCodeSubmissionRejectedError)
|
||
case .invalidSession:
|
||
self.db.write { self.resetSession($0) }
|
||
return .value(.showErrorSheet(.sessionInvalidated))
|
||
case .serverFailure(let failureResponse):
|
||
if failureResponse.isPermanent {
|
||
return .value(.showErrorSheet(.genericError))
|
||
} else {
|
||
return .value(.showErrorSheet(.networkError))
|
||
}
|
||
case .retryAfterTimeout(let session):
|
||
self.db.write { self.processSession(session, $0) }
|
||
if let timeInterval = session.nextVerificationAttempt, timeInterval < Constants.autoRetryInterval {
|
||
return Guarantee
|
||
.after(on: self.schedulers.global(), seconds: timeInterval)
|
||
.then(on: self.schedulers.sync) { [weak self] in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
return self.submitSessionCode(
|
||
session: session,
|
||
code: code
|
||
)
|
||
}
|
||
}
|
||
if session.nextVerificationAttemptDate != nil {
|
||
return .value(.verificationCodeEntry(self.verificationCodeEntryState(
|
||
session: session,
|
||
validationError: .submitCodeTimeout
|
||
)))
|
||
} else {
|
||
// Something went wrong, we can't submit again.
|
||
return .value(self.verificationCodeSubmissionRejectedError)
|
||
}
|
||
case .networkFailure:
|
||
if retriesLeft > 0 {
|
||
return self.submitSessionCode(
|
||
session: session,
|
||
code: code,
|
||
retriesLeft: retriesLeft - 1
|
||
)
|
||
}
|
||
return .value(.showErrorSheet(.networkError))
|
||
case .transportError(let session):
|
||
Logger.error("Should not get transport error when submitting verification code")
|
||
self.db.write { self.processSession(session, $0) }
|
||
return .value(.showErrorSheet(.genericError))
|
||
case .genericError:
|
||
return .value(.showErrorSheet(.genericError))
|
||
}
|
||
}
|
||
}
|
||
|
||
private func restoreSVRMasterSecretForSessionPathReglock(
|
||
session: RegistrationSession,
|
||
pin: String,
|
||
svrAuthCredential: SVRAuthCredential,
|
||
reglockExpirationDate: Date,
|
||
retriesLeft: Int = Constants.networkErrorRetries
|
||
) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
|
||
return deps.svr.restoreKeys(
|
||
pin: pin,
|
||
authMethod: .svrAuth(svrAuthCredential, backup: nil)
|
||
)
|
||
.then(on: schedulers.main) { [weak self] result -> Guarantee<RegistrationStep> in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
switch result {
|
||
case .success:
|
||
self.db.write { tx in
|
||
self.loadLocalMasterKeyAndUpdateState(tx)
|
||
self.updatePersistedSessionState(session: session, tx) {
|
||
// Now we have the state we need to get past reglock.
|
||
$0.reglockState = .none
|
||
}
|
||
}
|
||
return self.nextStep()
|
||
case let .invalidPin(remainingAttempts):
|
||
return .value(.pinEntry(RegistrationPinState(
|
||
operation: .enteringExistingPin(
|
||
skippability: .unskippable,
|
||
remainingAttempts: UInt(remainingAttempts)
|
||
),
|
||
error: .wrongPin(wrongPin: pin),
|
||
contactSupportMode: self.contactSupportRegistrationPINMode(),
|
||
exitConfiguration: self.pinCodeEntryExitConfiguration()
|
||
)))
|
||
case .backupMissing:
|
||
// If we are unable to talk to SVR, it got wiped, probably
|
||
// because we used up our guesses. We can't get past reglock.
|
||
self.inMemoryState.pinFromUser = nil
|
||
self.inMemoryState.shouldRestoreSVRMasterKeyAfterRegistration = false
|
||
self.db.write { tx in
|
||
self.updatePersistedState(tx) {
|
||
$0.hasGivenUpTryingToRestoreWithSVR = true
|
||
}
|
||
self.updatePersistedSessionState(session: session, tx) {
|
||
$0.reglockState = .waitingTimeout(expirationDate: reglockExpirationDate)
|
||
}
|
||
}
|
||
return self.nextStep()
|
||
case .networkError:
|
||
if retriesLeft > 0 {
|
||
return self.restoreSVRMasterSecretForSessionPathReglock(
|
||
session: session,
|
||
pin: pin,
|
||
svrAuthCredential: svrAuthCredential,
|
||
reglockExpirationDate: reglockExpirationDate,
|
||
retriesLeft: retriesLeft - 1
|
||
)
|
||
}
|
||
return .value(.showErrorSheet(.networkError))
|
||
case .genericError:
|
||
return .value(.showErrorSheet(.genericError))
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Profile Setup Pathway
|
||
|
||
/// Returns the next step the user needs to go through _after_ the actual account
|
||
/// registration or change number is complete (e.g. profile setup).
|
||
private func nextStepForProfileSetup(
|
||
_ accountIdentity: AccountIdentity
|
||
) -> Guarantee<RegistrationStep> {
|
||
switch mode {
|
||
case .registering, .reRegistering:
|
||
break
|
||
case .changingNumber:
|
||
// Change number is different; we do a limited number of operations and then finalize.
|
||
if let stepGuarantee = performSVRBackupStepsIfNeeded(accountIdentity: accountIdentity) {
|
||
return stepGuarantee
|
||
}
|
||
|
||
return exportAndWipeState(accountIdentity: accountIdentity)
|
||
}
|
||
|
||
// We _must_ do these steps first.
|
||
if shouldRefreshOneTimePreKeys() {
|
||
// After atomic account creation, our account is ready to go from the start.
|
||
// But we should still upload one-time prekeys, as that is not part
|
||
// of account creation.
|
||
return self.deps.preKeyManager.rotateOneTimePreKeysForRegistration(auth: accountIdentity.chatServiceAuth)
|
||
.then(on: schedulers.main) { [weak self] () -> Guarantee<RegistrationStep> in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
self.db.write { tx in
|
||
self.updatePersistedState(tx) {
|
||
// No harm marking both down as done even though
|
||
// we only did one or the other.
|
||
$0.didRefreshOneTimePreKeys = true
|
||
}
|
||
}
|
||
return self.nextStep()
|
||
}
|
||
.recover(on: schedulers.main) { [weak self] error -> Guarantee<RegistrationStep> in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
if error.isPostRegDeregisteredError {
|
||
return self.becameDeregisteredBeforeCompleting(accountIdentity: accountIdentity)
|
||
}
|
||
Logger.error("Failed to create prekeys: \(error)")
|
||
// Note this is undismissable; the user will be on whatever
|
||
// screen they were on but with the error sheet atop which retries
|
||
// via `nextStep()` when tapped.
|
||
return .value(.showErrorSheet(.genericError))
|
||
}
|
||
}
|
||
|
||
if let stepGuarantee = performSVRBackupStepsIfNeeded(accountIdentity: accountIdentity) {
|
||
return stepGuarantee
|
||
}
|
||
|
||
if shouldRestoreFromMessageBackup() {
|
||
return chooseLocalMessageBackupToRestore()
|
||
}
|
||
|
||
if shouldRestoreFromStorageService() {
|
||
return restoreFromStorageService(accountIdentity: accountIdentity)
|
||
}
|
||
|
||
if let localUsernameState = shouldAttemptToReclaimUsername() {
|
||
return attemptToReclaimUsername(
|
||
accountIdentity: accountIdentity,
|
||
localUsernameState: localUsernameState
|
||
)
|
||
}
|
||
|
||
if !inMemoryState.hasProfileName {
|
||
if let profileInfo = inMemoryState.pendingProfileInfo {
|
||
let profileManager = deps.profileManager
|
||
return deps.db.writePromise { tx in
|
||
profileManager.updateLocalProfile(
|
||
givenName: profileInfo.givenName,
|
||
familyName: profileInfo.familyName,
|
||
avatarData: profileInfo.avatarData,
|
||
authedAccount: accountIdentity.authedAccount,
|
||
tx: tx
|
||
)
|
||
}
|
||
.then(on: SyncScheduler()) { updatePromise in
|
||
// Run the Promise returned from databaseStorage.write(...).
|
||
updatePromise
|
||
}
|
||
.map(on: schedulers.sync) { return nil }
|
||
.recover(on: schedulers.sync) { (error) -> Guarantee<Error?> in
|
||
return .value(error)
|
||
}
|
||
.then(on: schedulers.main) { [weak self] (error) -> Guarantee<RegistrationStep> in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
if let error {
|
||
if error.isPostRegDeregisteredError {
|
||
return self.becameDeregisteredBeforeCompleting(accountIdentity: accountIdentity)
|
||
}
|
||
return .value(.showErrorSheet(
|
||
error.isNetworkFailureOrTimeout ? .networkError : .genericError
|
||
))
|
||
}
|
||
self.inMemoryState.hasProfileName = true
|
||
self.inMemoryState.pendingProfileInfo = nil
|
||
return self.nextStep()
|
||
}
|
||
}
|
||
|
||
return .value(.setupProfile(RegistrationProfileState(
|
||
e164: accountIdentity.e164,
|
||
phoneNumberDiscoverability: inMemoryState.phoneNumberDiscoverability.orDefault
|
||
)))
|
||
}
|
||
|
||
if inMemoryState.phoneNumberDiscoverability == nil {
|
||
return .value(.phoneNumberDiscoverability(RegistrationPhoneNumberDiscoverabilityState(
|
||
e164: accountIdentity.e164,
|
||
phoneNumberDiscoverability: inMemoryState.phoneNumberDiscoverability.orDefault
|
||
)))
|
||
}
|
||
|
||
// We are ready to finish! Export all state and wipe things
|
||
// so we can re-register later if desired.
|
||
return exportAndWipeState(accountIdentity: accountIdentity)
|
||
}
|
||
|
||
// returns nil if no steps needed.
|
||
private func performSVRBackupStepsIfNeeded(
|
||
accountIdentity: AccountIdentity
|
||
) -> Guarantee<RegistrationStep>? {
|
||
Logger.info("")
|
||
|
||
let isRestoringPinBackup: Bool = (
|
||
accountIdentity.hasPreviouslyUsedSVR &&
|
||
!persistedState.hasGivenUpTryingToRestoreWithSVR
|
||
)
|
||
|
||
if !persistedState.hasSkippedPinEntry {
|
||
guard let pin = inMemoryState.pinFromUser ?? inMemoryState.pinFromDisk else {
|
||
if isRestoringPinBackup {
|
||
return .value(.pinEntry(RegistrationPinState(
|
||
operation: .enteringExistingPin(
|
||
skippability: .canSkipAndCreateNew,
|
||
remainingAttempts: nil
|
||
),
|
||
error: nil,
|
||
contactSupportMode: self.contactSupportRegistrationPINMode(),
|
||
exitConfiguration: pinCodeEntryExitConfiguration()
|
||
)))
|
||
} else if let blob = inMemoryState.unconfirmedPinBlob {
|
||
return .value(.pinEntry(RegistrationPinState(
|
||
operation: .confirmingNewPin(blob),
|
||
error: nil,
|
||
contactSupportMode: self.contactSupportRegistrationPINMode(),
|
||
exitConfiguration: pinCodeEntryExitConfiguration()
|
||
)))
|
||
} else {
|
||
return .value(.pinEntry(RegistrationPinState(
|
||
operation: .creatingNewPin,
|
||
error: nil,
|
||
contactSupportMode: self.contactSupportRegistrationPINMode(),
|
||
exitConfiguration: pinCodeEntryExitConfiguration()
|
||
)))
|
||
}
|
||
}
|
||
if inMemoryState.shouldBackUpToSVR {
|
||
// If we have no SVR data, fetch it.
|
||
if isRestoringPinBackup, inMemoryState.shouldRestoreSVRMasterKeyAfterRegistration {
|
||
return restoreSVRBackupPostRegistration(pin: pin, accountIdentity: accountIdentity)
|
||
} else {
|
||
// If we haven't backed up, do so now.
|
||
return backupToSVR(pin: pin, accountIdentity: accountIdentity)
|
||
}
|
||
}
|
||
|
||
switch attributes2FAMode(e164: accountIdentity.e164) {
|
||
case .none, .v1:
|
||
Logger.info("Not enabling reglock because it wasn't enabled to begin with")
|
||
case .v2(let reglockToken):
|
||
guard inMemoryState.hasSetReglock.negated else {
|
||
break
|
||
}
|
||
return enableReglock(accountIdentity: accountIdentity, reglockToken: reglockToken)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
private func restoreSVRBackupPostRegistration(
|
||
pin: String,
|
||
accountIdentity: AccountIdentity,
|
||
retriesLeft: Int = Constants.networkErrorRetries
|
||
) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
|
||
let backupAuthMethod = SVR.AuthMethod.chatServerAuth(accountIdentity.authedAccount)
|
||
let authMethod: SVR.AuthMethod
|
||
if let svrAuthCredential = inMemoryState.svrAuthCredential {
|
||
authMethod = .svrAuth(svrAuthCredential, backup: backupAuthMethod)
|
||
} else {
|
||
authMethod = backupAuthMethod
|
||
}
|
||
return deps.svr
|
||
.restoreKeysAndBackup(
|
||
pin: pin,
|
||
authMethod: authMethod
|
||
)
|
||
.then(on: schedulers.main) { [weak self] result -> Guarantee<RegistrationStep> in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
switch result {
|
||
case .success:
|
||
self.inMemoryState.shouldRestoreSVRMasterKeyAfterRegistration = false
|
||
self.inMemoryState.hasBackedUpToSVR = true
|
||
return self.nextStep()
|
||
case let .invalidPin(remainingAttempts):
|
||
return .value(.pinEntry(RegistrationPinState(
|
||
operation: .enteringExistingPin(
|
||
skippability: .canSkipAndCreateNew,
|
||
remainingAttempts: UInt(remainingAttempts)
|
||
),
|
||
error: .wrongPin(wrongPin: pin),
|
||
contactSupportMode: self.contactSupportRegistrationPINMode(),
|
||
exitConfiguration: self.pinCodeEntryExitConfiguration()
|
||
)))
|
||
case .backupMissing:
|
||
// If we are unable to talk to SVR, it got wiped and we can't
|
||
// recover. Keep going like if nothing happened.
|
||
self.inMemoryState.pinFromUser = nil
|
||
self.inMemoryState.shouldRestoreSVRMasterKeyAfterRegistration = false
|
||
self.db.write { tx in
|
||
self.updatePersistedState(tx) { $0.hasGivenUpTryingToRestoreWithSVR = true }
|
||
}
|
||
return .value(.pinAttemptsExhaustedWithoutReglock(
|
||
.init(mode: .restoringBackup)
|
||
))
|
||
case .networkError:
|
||
if retriesLeft > 0 {
|
||
return self.restoreSVRBackupPostRegistration(
|
||
pin: pin,
|
||
accountIdentity: accountIdentity,
|
||
retriesLeft: retriesLeft - 1
|
||
)
|
||
}
|
||
return .value(.showErrorSheet(.networkError))
|
||
case .genericError(let error):
|
||
if error.isPostRegDeregisteredError {
|
||
return self.becameDeregisteredBeforeCompleting(accountIdentity: accountIdentity)
|
||
} else if retriesLeft > 0 {
|
||
return self.restoreSVRBackupPostRegistration(
|
||
pin: pin,
|
||
accountIdentity: accountIdentity,
|
||
retriesLeft: retriesLeft - 1
|
||
)
|
||
} else {
|
||
self.inMemoryState.pinFromUser = nil
|
||
return .value(.pinEntry(RegistrationPinState(
|
||
operation: .enteringExistingPin(
|
||
skippability: .canSkipAndCreateNew,
|
||
remainingAttempts: nil
|
||
),
|
||
error: .serverError,
|
||
contactSupportMode: self.contactSupportRegistrationPINMode(),
|
||
exitConfiguration: self.pinCodeEntryExitConfiguration()
|
||
)))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private func backupToSVR(
|
||
pin: String,
|
||
accountIdentity: AccountIdentity,
|
||
retriesLeft: Int = Constants.networkErrorRetries
|
||
) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
|
||
let authMethod: SVR.AuthMethod
|
||
let backupAuthMethod = SVR.AuthMethod.chatServerAuth(accountIdentity.authedAccount)
|
||
if let svrAuthCredential = inMemoryState.svrAuthCredential {
|
||
authMethod = .svrAuth(svrAuthCredential, backup: backupAuthMethod)
|
||
} else {
|
||
authMethod = backupAuthMethod
|
||
}
|
||
return deps.svr
|
||
.generateAndBackupKeys(
|
||
pin: pin,
|
||
authMethod: authMethod
|
||
)
|
||
.then(on: schedulers.main) { [weak self] () -> Guarantee<RegistrationStep> in
|
||
guard let strongSelf = self else {
|
||
return unretainedSelfError()
|
||
}
|
||
strongSelf.inMemoryState.hasBackedUpToSVR = true
|
||
strongSelf.db.write { tx in
|
||
Logger.info("Setting pin code after SVR backup")
|
||
strongSelf.deps.ows2FAManager.markPinEnabled(pin, tx)
|
||
}
|
||
return strongSelf.nextStep()
|
||
}
|
||
.recover(on: schedulers.main) { [weak self] error -> Guarantee<RegistrationStep> in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
if error.isNetworkFailureOrTimeout {
|
||
if retriesLeft > 0 {
|
||
return self.backupToSVR(
|
||
pin: pin,
|
||
accountIdentity: accountIdentity,
|
||
retriesLeft: retriesLeft - 1
|
||
)
|
||
}
|
||
return .value(.showErrorSheet(.networkError))
|
||
}
|
||
Logger.error("Failed to back up to SVR with error: \(error)")
|
||
// We want to let people get through registration even if backups
|
||
// go wrong. Show an error but let the user continue when they try the next step.
|
||
self.inMemoryState.didSkipSVRBackup = true
|
||
return .value(.showErrorSheet(.genericError))
|
||
}
|
||
}
|
||
|
||
private func chooseLocalMessageBackupToRestore() -> Guarantee<RegistrationStep> {
|
||
return .value(.restoreFromLocalMessageBackup)
|
||
}
|
||
|
||
private func restoreFromStorageService(
|
||
accountIdentity: AccountIdentity
|
||
) -> Guarantee<RegistrationStep> {
|
||
if FeatureFlags.storageServiceRecordIkmMigration {
|
||
db.write { tx in
|
||
switch mode {
|
||
case .registering, .reRegistering:
|
||
break
|
||
case .changingNumber:
|
||
owsFailDebug("Unexpectedly restoring from Storage Service while changing number, rather than during (re)registration! Bailing.")
|
||
return
|
||
}
|
||
|
||
/// We are (re-)registering, which means we have no devices.
|
||
/// Consequently, we can hardcode this capability to `true`.
|
||
///
|
||
/// This is important because the `restoreOrCreateManifest` call
|
||
/// below may end up creating a brand-new Storage Service manifest,
|
||
/// and we want to ensure it's created with a `recordIkm`.
|
||
///
|
||
/// - SeeAlso `StorageServiceRecordIkmCapabilityStore`
|
||
deps.storageServiceRecordIkmCapabilityStore.setIsRecordIkmCapable(tx: tx)
|
||
}
|
||
}
|
||
|
||
return deps
|
||
.storageServiceManager.restoreOrCreateManifestIfNecessary(
|
||
authedDevice: accountIdentity.authedDevice
|
||
)
|
||
.timeout(seconds: 120)
|
||
.then(on: schedulers.sync) { [weak self] in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
self.loadProfileState()
|
||
if self.inMemoryState.hasProfileName {
|
||
self.scheduleReuploadProfileStateAsync(accountIdentity: accountIdentity)
|
||
}
|
||
self.inMemoryState.hasRestoredFromStorageService = true
|
||
return self.nextStep()
|
||
}
|
||
.recover(on: schedulers.main) { [weak self] error in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
if error.isPostRegDeregisteredError {
|
||
return self.becameDeregisteredBeforeCompleting(accountIdentity: accountIdentity)
|
||
}
|
||
self.inMemoryState.hasSkippedRestoreFromStorageService = true
|
||
return self.nextStep()
|
||
}
|
||
}
|
||
|
||
/// If we have a username/username link during registration – which we would
|
||
/// have restored from Storage Service – attempts to "reclaim" it.
|
||
///
|
||
/// When we call `POST /v1/registration` and an account already exists with
|
||
/// our phone number, and the account has a username, the server will move
|
||
/// the username to a "reserved" state. That gives us an opportunity to
|
||
/// reclaim that username and have it re-added to our account, which we do
|
||
/// by sending a "confirm username" request.
|
||
///
|
||
/// In making that request we use the username we have locally (which we
|
||
/// expect to be reserved), and the same username-link-entropy we had
|
||
/// locally. The server will notice that we're attempting to confirm a
|
||
/// username it moved from confirmed -> reserved, and will not rotate the
|
||
/// username-link-handle. The end result should therefore be that we get our
|
||
/// username back, and our username link is unaffected.
|
||
///
|
||
/// - Note
|
||
/// This method will automatically retry the "confirm username" request on
|
||
/// network errors.
|
||
///
|
||
/// - Note
|
||
/// If the reclamation attempt fails for a non-network reason, or exhausts
|
||
/// network retries, we will simply move on. Any further recovery will
|
||
/// happen via the username validation job and interactive recovery flows.
|
||
private func attemptToReclaimUsername(
|
||
accountIdentity: AccountIdentity,
|
||
localUsernameState: Usernames.LocalUsernameState,
|
||
remainingNetworkErrorRetries: UInt = 2
|
||
) -> Guarantee<RegistrationStep> {
|
||
func attemptComplete() -> Guarantee<RegistrationStep> {
|
||
AssertIsOnMainThread()
|
||
inMemoryState.usernameReclamationState = .reclamationAttempted
|
||
return nextStep()
|
||
}
|
||
|
||
let logger = PrefixedLogger(prefix: "UsernameReclamation")
|
||
|
||
let localUsername: String
|
||
let localUsernameLink: Usernames.UsernameLink
|
||
|
||
switch localUsernameState {
|
||
case .unset, .linkCorrupted, .usernameAndLinkCorrupted:
|
||
return attemptComplete()
|
||
case .available(let username, let usernameLink):
|
||
localUsername = username
|
||
localUsernameLink = usernameLink
|
||
}
|
||
|
||
let hashedLocalUsername: Usernames.HashedUsername
|
||
let encryptedUsernameForLink: Data
|
||
|
||
do {
|
||
hashedLocalUsername = try Usernames.HashedUsername(forUsername: localUsername)
|
||
(_, encryptedUsernameForLink) = try deps.usernameLinkManager.generateEncryptedUsername(
|
||
username: localUsername,
|
||
existingEntropy: localUsernameLink.entropy
|
||
)
|
||
} catch let error {
|
||
logger.error("Failed to reclaim username: error while generating params! \(error)")
|
||
return attemptComplete()
|
||
}
|
||
|
||
return firstly(on: schedulers.sync) { () -> Promise<Usernames.ApiClientConfirmationResult> in
|
||
return self.deps.usernameApiClient.confirmReservedUsername(
|
||
reservedUsername: hashedLocalUsername,
|
||
encryptedUsernameForLink: encryptedUsernameForLink,
|
||
chatServiceAuth: accountIdentity.chatServiceAuth
|
||
)
|
||
}
|
||
.then(on: schedulers.main) { confirmationResult -> Guarantee<RegistrationStep> in
|
||
switch confirmationResult {
|
||
case .success(let usernameLinkHandle):
|
||
if localUsernameLink.handle != usernameLinkHandle {
|
||
logger.error("Username link handle rotated during reclamation! Our local username link is now broken.")
|
||
} else {
|
||
logger.info("Successfully reclaimed username during registration.")
|
||
}
|
||
case .rejected, .rateLimited:
|
||
logger.error("Unexpectedly failed to confirm .username! \(confirmationResult)")
|
||
}
|
||
|
||
return attemptComplete()
|
||
}
|
||
.recover(on: schedulers.main) { error -> Guarantee<RegistrationStep> in
|
||
if error.isNetworkFailureOrTimeout, remainingNetworkErrorRetries > 0 {
|
||
return self.attemptToReclaimUsername(
|
||
accountIdentity: accountIdentity,
|
||
localUsernameState: localUsernameState,
|
||
remainingNetworkErrorRetries: remainingNetworkErrorRetries - 1
|
||
)
|
||
} else if error.isNetworkFailureOrTimeout {
|
||
logger.error("Failed to reclaim username: network error!")
|
||
} else {
|
||
logger.error("Failed to reclaim username: unknown error!")
|
||
}
|
||
|
||
return attemptComplete()
|
||
}
|
||
}
|
||
|
||
private func enableReglock(
|
||
accountIdentity: AccountIdentity,
|
||
reglockToken: String
|
||
) -> Guarantee<RegistrationStep> {
|
||
Logger.info("Attempting to enable reglock")
|
||
|
||
return Service.makeEnableReglockRequest(
|
||
reglockToken: reglockToken,
|
||
auth: accountIdentity.chatServiceAuth,
|
||
signalService: deps.signalService,
|
||
schedulers: schedulers
|
||
).recover(on: schedulers.sync) { _ -> Guarantee<Void> in
|
||
// This isn't immediately catastrophic; this user already had reglock
|
||
// enabled, so while it may now be out of date, its still there and
|
||
// preventing others from getting in. We defer updating this until
|
||
// later (when we update account attributes).
|
||
// This matches legacy registration behavior.
|
||
Logger.error("Unable to set reglock, so old reglock password will remain enforced.")
|
||
return .value(())
|
||
}.then(on: schedulers.main) { [weak self] () -> Guarantee<RegistrationStep> in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
self.inMemoryState.hasSetReglock = true
|
||
self.inMemoryState.wasReglockEnabledBeforeStarting = true
|
||
self.db.write { tx in
|
||
self.deps.ows2FAManager.markRegistrationLockEnabled(tx)
|
||
}
|
||
return self.nextStep()
|
||
}
|
||
}
|
||
|
||
private func scheduleReuploadProfileStateAsync(accountIdentity: AccountIdentity) {
|
||
Logger.debug("restored local profile name. Uploading...")
|
||
// if we don't have a `localGivenName`, there's nothing to upload, and trying
|
||
// to upload would fail.
|
||
|
||
// Note we *don't* block on the update. There's no need to block registration on
|
||
// it completing, and if there are any errors, it's durable.
|
||
self.deps.profileManager
|
||
.scheduleReuploadLocalProfile(authedAccount: accountIdentity.authedAccount)
|
||
}
|
||
|
||
private func loadProfileState() {
|
||
Logger.info("")
|
||
|
||
let profileKey = deps.profileManager.localProfileKey
|
||
inMemoryState.profileKey = profileKey
|
||
let udAccessKey: SMKUDAccessKey
|
||
do {
|
||
udAccessKey = try SMKUDAccessKey(profileKey: profileKey.keyData)
|
||
if udAccessKey.keyData.count < 1 {
|
||
owsFail("Could not determine UD access key, empty key generated.")
|
||
}
|
||
} catch {
|
||
// Crash app if UD cannot be enabled.
|
||
owsFail("Could not determine UD access key: \(error).")
|
||
}
|
||
inMemoryState.udAccessKey = udAccessKey
|
||
inMemoryState.hasProfileName = deps.profileManager.hasProfileName
|
||
db.read { tx in
|
||
inMemoryState.phoneNumberDiscoverability =
|
||
deps.phoneNumberDiscoverabilityManager.phoneNumberDiscoverability(tx: tx)
|
||
|
||
inMemoryState.usernameReclamationState =
|
||
.localUsernameStateLoaded(deps.localUsernameManager.usernameState(tx: tx))
|
||
}
|
||
}
|
||
|
||
private func updateAccountAttributes(_ accountIdentity: AccountIdentity) -> Guarantee<Error?> {
|
||
Logger.info("")
|
||
return Service
|
||
.makeUpdateAccountAttributesRequest(
|
||
makeAccountAttributes(
|
||
isManualMessageFetchEnabled: inMemoryState.isManualMessageFetchEnabled,
|
||
twoFAMode: self.attributes2FAMode(e164: accountIdentity.e164)
|
||
),
|
||
auth: accountIdentity.chatServiceAuth,
|
||
signalService: deps.signalService,
|
||
schedulers: schedulers
|
||
)
|
||
}
|
||
|
||
private func updatePhoneNumberDiscoverability(accountIdentity: AccountIdentity, phoneNumberDiscoverability: PhoneNumberDiscoverability) {
|
||
Logger.info("")
|
||
|
||
self.inMemoryState.phoneNumberDiscoverability = phoneNumberDiscoverability
|
||
|
||
db.write { tx in
|
||
// We will update attributes & storage service at the end of registration.
|
||
deps.phoneNumberDiscoverabilityManager.setPhoneNumberDiscoverability(
|
||
phoneNumberDiscoverability,
|
||
updateAccountAttributes: false,
|
||
updateStorageService: false,
|
||
authedAccount: accountIdentity.authedAccount,
|
||
tx: tx
|
||
)
|
||
}
|
||
}
|
||
|
||
private enum FinalizeChangeNumberResult {
|
||
case success
|
||
case unretainedSelf
|
||
case genericError
|
||
}
|
||
|
||
private func finalizeChangeNumberPniState(
|
||
changeNumberState: Mode.ChangeNumberState,
|
||
pniState: Mode.ChangeNumberState.PendingPniState,
|
||
accountIdentity: AccountIdentity
|
||
) -> Guarantee<FinalizeChangeNumberResult> {
|
||
Logger.info("")
|
||
|
||
// Creating a high strust signal recipient for oneself
|
||
// must happen in a transaction initiated off the main thread.
|
||
return firstly(on: schedulers.global()) { [weak self] () -> FinalizeChangeNumberResult in
|
||
guard let strongSelf = self else {
|
||
return .unretainedSelf
|
||
}
|
||
do {
|
||
try strongSelf.db.write { tx in
|
||
try strongSelf.deps.changeNumberPniManager.finalizePniIdentity(
|
||
withPendingState: pniState.asPniState(),
|
||
transaction: tx
|
||
)
|
||
strongSelf._unsafeToModify_mode = .changingNumber(try strongSelf.loader.savePendingChangeNumber(
|
||
oldState: changeNumberState,
|
||
pniState: nil,
|
||
transaction: tx
|
||
))
|
||
|
||
Logger.info(
|
||
"""
|
||
Recording new phone number
|
||
localAci: \(changeNumberState.localAci),
|
||
localE164: \(changeNumberState.oldE164.stringValue),
|
||
serviceAci: \(accountIdentity.aci),
|
||
servicePni: \(accountIdentity.pni),
|
||
serviceE164: \(accountIdentity.e164.stringValue)")
|
||
"""
|
||
)
|
||
|
||
// We do these here, and not in export state, so that we don't risk
|
||
// syncing out-of-date state to storage service.
|
||
strongSelf.deps.registrationStateChangeManager.didUpdateLocalPhoneNumber(
|
||
accountIdentity.e164,
|
||
aci: accountIdentity.aci,
|
||
pni: accountIdentity.pni,
|
||
tx: tx
|
||
)
|
||
// Make sure we update our local account.
|
||
strongSelf.deps.storageServiceManager.recordPendingLocalAccountUpdates()
|
||
}
|
||
return .success
|
||
} catch {
|
||
Logger.error("Failed to finalize change number state: \(error)")
|
||
return .genericError
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: Device Transfer
|
||
|
||
private func shouldSkipDeviceTransfer() -> Bool {
|
||
switch mode {
|
||
case .registering:
|
||
return persistedState.hasDeclinedTransfer
|
||
case .reRegistering, .changingNumber:
|
||
// Always skip device transfer in these modes.
|
||
return true
|
||
}
|
||
}
|
||
|
||
// MARK: - Permissions
|
||
|
||
private func requiresSystemPermissions() -> Guarantee<Bool> {
|
||
let contacts = deps.contactsStore.needsContactsAuthorization()
|
||
let notifications = deps.pushRegistrationManager.needsNotificationAuthorization()
|
||
return Guarantee.when(fulfilled: [contacts, notifications])
|
||
.map { results in
|
||
return results.allSatisfy({ $0 })
|
||
}
|
||
.recover { _ in return .value(true) }
|
||
}
|
||
|
||
// MARK: - Register/Change Number Requests
|
||
|
||
private func makeRegisterOrChangeNumberRequest(
|
||
_ method: RegistrationRequestFactory.VerificationMethod,
|
||
e164: E164,
|
||
twoFAMode: AccountAttributes.TwoFactorAuthMode,
|
||
responseHandler: @escaping (AccountResponse) -> Guarantee<RegistrationStep>
|
||
) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
|
||
switch mode {
|
||
case .reRegistering(let state):
|
||
if persistedState.hasResetForReRegistration.negated {
|
||
db.write { tx in
|
||
let isPrimaryDevice = deps.tsAccountManager.registrationState(tx: tx).isPrimaryDevice ?? true
|
||
deps.registrationStateChangeManager.resetForReregistration(
|
||
localPhoneNumber: state.e164,
|
||
localAci: state.aci,
|
||
wasPrimaryDevice: isPrimaryDevice,
|
||
tx: tx
|
||
)
|
||
updatePersistedState(tx) {
|
||
$0.hasResetForReRegistration = true
|
||
}
|
||
}
|
||
}
|
||
fallthrough
|
||
case .registering:
|
||
// The auth token we use going forwards for chat server auth headers
|
||
// is generated by the client. We do that here and put it on the
|
||
// AccountIdentity we generate after success so that we eventually
|
||
// write it to TSAccountManager when all is said and done, and use
|
||
// it for requests we need to make between now and then.
|
||
let authToken = generateServerAuthToken()
|
||
return fetchApnRegistrationId().then(on: schedulers.main) { [weak self] apnResult in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
// Either manual message fetch is true, or apns tokens are set.
|
||
// Otherwise the request will fail.
|
||
let isManualMessageFetchEnabled: Bool
|
||
let apnRegistrationId: RegistrationRequestFactory.ApnRegistrationId?
|
||
switch apnResult {
|
||
case .success(let tokens):
|
||
isManualMessageFetchEnabled = false
|
||
apnRegistrationId = tokens
|
||
case .pushUnsupported:
|
||
Logger.info("Push unsupported; enabling manual message fetch.")
|
||
isManualMessageFetchEnabled = true
|
||
apnRegistrationId = nil
|
||
case .timeout:
|
||
Logger.error("Timed out waiting for apns token")
|
||
return .value(.showErrorSheet(.genericError))
|
||
case .genericError:
|
||
return .value(.showErrorSheet(.genericError))
|
||
}
|
||
self.inMemoryState.isManualMessageFetchEnabled = isManualMessageFetchEnabled
|
||
if isManualMessageFetchEnabled {
|
||
self.db.write { tx in
|
||
self.deps.tsAccountManager.setIsManualMessageFetchEnabled(true, tx: tx)
|
||
}
|
||
}
|
||
let accountAttributes = self.makeAccountAttributes(
|
||
isManualMessageFetchEnabled: isManualMessageFetchEnabled,
|
||
twoFAMode: twoFAMode
|
||
)
|
||
return self.makeCreateAccountRequestAndFinalizePreKeys(
|
||
method: method,
|
||
e164: e164,
|
||
authPassword: authToken,
|
||
accountAttributes: accountAttributes,
|
||
skipDeviceTransfer: self.shouldSkipDeviceTransfer(),
|
||
apnRegistrationId: apnRegistrationId,
|
||
responseHandler: responseHandler
|
||
)
|
||
}
|
||
|
||
case .changingNumber(let changeNumberState):
|
||
if let pniState = changeNumberState.pniState {
|
||
// We had an in flight change number that was interrupted, recover.
|
||
return recoverPendingPniChangeNumberState(
|
||
changeNumberState: changeNumberState,
|
||
pniState: pniState
|
||
)
|
||
}
|
||
return self.generatePniStateAndMakeChangeNumberRequest(
|
||
e164: e164,
|
||
verificationMethod: method,
|
||
twoFAMode: twoFAMode,
|
||
changeNumberState: changeNumberState
|
||
).then(on: schedulers.main) { [weak self] changeNumberResult in
|
||
switch changeNumberResult {
|
||
case .unretainedSelf:
|
||
return unretainedSelfError()
|
||
case .pniStateError:
|
||
return .value(.showErrorSheet(.genericError))
|
||
case .serviceResponse(let accountResponse):
|
||
switch accountResponse {
|
||
case .success:
|
||
// Pni state will get finalized and cleaned up later in
|
||
// the normal course of action.
|
||
break
|
||
case .reglockFailure, .rejectedVerificationMethod, .retryAfter:
|
||
// Explicit rejection by the server, we can safely
|
||
// wipe our local PNI state and regenerate when we retry.
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
do {
|
||
try self.db.write { tx in
|
||
self._unsafeToModify_mode = .changingNumber(try self.loader.savePendingChangeNumber(
|
||
oldState: changeNumberState,
|
||
pniState: nil,
|
||
transaction: tx
|
||
))
|
||
}
|
||
} catch {
|
||
return .value(.showErrorSheet(.genericError))
|
||
}
|
||
case .deviceTransferPossible:
|
||
owsFailBeta("Should't get device transfer response on change number request.")
|
||
case .networkError, .genericError:
|
||
// We don't know what went wrong, so PNI state
|
||
// may be set server side. Don't wipe PNI state
|
||
// so we try and recover.
|
||
Logger.error("Unknown error when changing number; preserving pni state")
|
||
}
|
||
return responseHandler(accountResponse)
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
private func makeCreateAccountRequestAndFinalizePreKeys(
|
||
method: RegistrationRequestFactory.VerificationMethod,
|
||
e164: E164,
|
||
authPassword: String,
|
||
accountAttributes: AccountAttributes,
|
||
skipDeviceTransfer: Bool,
|
||
apnRegistrationId: RegistrationRequestFactory.ApnRegistrationId?,
|
||
responseHandler: @escaping (AccountResponse) -> Guarantee<RegistrationStep>
|
||
) -> Guarantee<RegistrationStep> {
|
||
return self.deps.preKeyManager.createPreKeysForRegistration()
|
||
.map(on: self.schedulers.sync) { (bundles: RegistrationPreKeyUploadBundles) -> RegistrationPreKeyUploadBundles? in
|
||
return bundles
|
||
}.recover(on: self.schedulers.sync) {
|
||
Logger.error("Unable to generate prekeys: \($0)")
|
||
return .value(nil)
|
||
}
|
||
.then(on: self.schedulers.main) { [weak self] (prekeyBundles: RegistrationPreKeyUploadBundles?) in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
guard let prekeyBundles else {
|
||
return .value(.showErrorSheet(.genericError))
|
||
}
|
||
return Service
|
||
.makeCreateAccountRequest(
|
||
method,
|
||
e164: e164,
|
||
authPassword: authPassword,
|
||
accountAttributes: accountAttributes,
|
||
skipDeviceTransfer: self.shouldSkipDeviceTransfer(),
|
||
apnRegistrationId: apnRegistrationId,
|
||
prekeyBundles: prekeyBundles,
|
||
signalService: self.deps.signalService,
|
||
schedulers: self.schedulers
|
||
)
|
||
.then(on: self.schedulers.main) { [weak self] (accountResponse: AccountResponse) -> Guarantee<RegistrationStep> in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
let isPrekeyUploadSuccess: Bool
|
||
switch accountResponse {
|
||
case .success:
|
||
isPrekeyUploadSuccess = true
|
||
case
|
||
.retryAfter,
|
||
.rejectedVerificationMethod,
|
||
.reglockFailure,
|
||
.networkError,
|
||
.genericError,
|
||
.deviceTransferPossible:
|
||
isPrekeyUploadSuccess = false
|
||
}
|
||
return self.deps.preKeyManager
|
||
.finalizeRegistrationPreKeys(
|
||
prekeyBundles,
|
||
uploadDidSucceed: isPrekeyUploadSuccess
|
||
).recover(on: self.schedulers.sync) { error in
|
||
// Finalizing is best effort.
|
||
Logger.error("Unable to finalize prekeys, ignoring and continuing")
|
||
return .value(())
|
||
}
|
||
.then(on: self.schedulers.main) { () -> Guarantee<RegistrationStep> in
|
||
return responseHandler(accountResponse)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private enum ChangeNumberResult {
|
||
case serviceResponse(AccountResponse)
|
||
case pniStateError
|
||
case unretainedSelf
|
||
}
|
||
|
||
private func generatePniStateAndMakeChangeNumberRequest(
|
||
e164: E164,
|
||
verificationMethod: RegistrationRequestFactory.VerificationMethod,
|
||
twoFAMode: AccountAttributes.TwoFactorAuthMode,
|
||
changeNumberState: RegistrationCoordinatorLoaderImpl.Mode.ChangeNumberState
|
||
) -> Guarantee<ChangeNumberResult> {
|
||
Logger.info("")
|
||
|
||
return deps.changeNumberPniManager
|
||
.generatePniIdentity(
|
||
forNewE164: e164,
|
||
localAci: changeNumberState.localAci,
|
||
localRecipientUniqueId: changeNumberState.localAccountId,
|
||
localDeviceId: changeNumberState.localDeviceId,
|
||
localUserAllDeviceIds: changeNumberState.localUserAllDeviceIds
|
||
)
|
||
.then(on: schedulers.global()) { [weak self] pniResult -> Guarantee<ChangeNumberResult> in
|
||
guard let strongSelf = self else {
|
||
return .value(.unretainedSelf)
|
||
}
|
||
switch pniResult {
|
||
case .failure:
|
||
return .value(.pniStateError)
|
||
case .success(let pniParams, let pniPendingState):
|
||
return strongSelf.makeChangeNumberRequest(
|
||
e164: e164,
|
||
verificationMethod: verificationMethod,
|
||
twoFAMode: twoFAMode,
|
||
changeNumberState: changeNumberState,
|
||
pniPendingState: pniPendingState,
|
||
pniParams: pniParams
|
||
)
|
||
|
||
}
|
||
}
|
||
}
|
||
|
||
private func makeChangeNumberRequest(
|
||
e164: E164,
|
||
verificationMethod: RegistrationRequestFactory.VerificationMethod,
|
||
twoFAMode: AccountAttributes.TwoFactorAuthMode,
|
||
changeNumberState: RegistrationCoordinatorLoaderImpl.Mode.ChangeNumberState,
|
||
pniPendingState: ChangePhoneNumberPni.PendingState,
|
||
pniParams: PniDistribution.Parameters
|
||
) -> Guarantee<ChangeNumberResult> {
|
||
Logger.info("")
|
||
|
||
// Process all messages first.
|
||
return deps.messageProcessor.waitForProcessingCompleteAndThenSuspend(for: .pendingChangeNumber)
|
||
.then(on: schedulers.main) { [weak self] in
|
||
guard let strongSelf = self else {
|
||
return .value(.unretainedSelf)
|
||
}
|
||
do {
|
||
try strongSelf.db.write { tx in
|
||
strongSelf._unsafeToModify_mode = .changingNumber(try strongSelf.loader.savePendingChangeNumber(
|
||
oldState: changeNumberState,
|
||
pniState: pniPendingState.asRegPniState(),
|
||
transaction: tx
|
||
))
|
||
}
|
||
} catch {
|
||
return .value(.pniStateError)
|
||
}
|
||
let reglockToken: String?
|
||
switch twoFAMode {
|
||
case .v2(let token):
|
||
reglockToken = token
|
||
case .v1, .none:
|
||
reglockToken = nil
|
||
}
|
||
return Service
|
||
.makeChangeNumberRequest(
|
||
verificationMethod,
|
||
e164: e164,
|
||
reglockToken: reglockToken,
|
||
authPassword: changeNumberState.oldAuthToken,
|
||
pniChangeNumberParameters: pniParams,
|
||
signalService: strongSelf.deps.signalService,
|
||
schedulers: strongSelf.schedulers
|
||
)
|
||
.map(on: strongSelf.schedulers.sync) { .serviceResponse($0) }
|
||
}
|
||
}
|
||
|
||
private func recoverPendingPniChangeNumberState(
|
||
changeNumberState: Mode.ChangeNumberState,
|
||
pniState: Mode.ChangeNumberState.PendingPniState
|
||
) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
|
||
return Service
|
||
.makeWhoAmIRequest(
|
||
auth: ChatServiceAuth.explicit(
|
||
aci: changeNumberState.localAci,
|
||
deviceId: .primary,
|
||
password: changeNumberState.oldAuthToken
|
||
),
|
||
signalService: deps.signalService,
|
||
schedulers: schedulers
|
||
)
|
||
.then(on: schedulers.main) { [weak self] whoAmIResult -> Guarantee<RegistrationStep> in
|
||
guard let strongSelf = self else {
|
||
return unretainedSelfError()
|
||
}
|
||
switch whoAmIResult {
|
||
case .networkError, .genericError:
|
||
return .value(.showErrorSheet(.genericError))
|
||
case .success(let whoAmIResponse):
|
||
if whoAmIResponse.e164 == pniState.newE164 {
|
||
// Success! Fake us getting the success response.
|
||
strongSelf.db.write { tx in
|
||
strongSelf.handleSuccessfulAccountResponse(
|
||
identity: AccountIdentity(
|
||
aci: whoAmIResponse.aci,
|
||
pni: whoAmIResponse.pni,
|
||
e164: whoAmIResponse.e164,
|
||
hasPreviouslyUsedSVR: strongSelf.inMemoryState.didHaveSVRBackupsPriorToReg,
|
||
authPassword: changeNumberState.oldAuthToken
|
||
),
|
||
tx
|
||
)
|
||
}
|
||
return strongSelf.nextStep()
|
||
} else {
|
||
// We had an in progress change number, but we arent on that number now.
|
||
// pretend it never happened.
|
||
do {
|
||
try strongSelf.db.write { tx in
|
||
strongSelf._unsafeToModify_mode = .changingNumber(try strongSelf.loader.savePendingChangeNumber(
|
||
oldState: changeNumberState,
|
||
pniState: nil,
|
||
transaction: tx
|
||
))
|
||
}
|
||
} catch {
|
||
return .value(.showErrorSheet(.genericError))
|
||
}
|
||
return strongSelf.nextStep()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private func handleSuccessfulAccountResponse(
|
||
identity: AccountIdentity,
|
||
_ transaction: DBWriteTransaction
|
||
) {
|
||
inMemoryState.session = nil
|
||
deps.sessionManager.clearPersistedSession(transaction)
|
||
updatePersistedState(transaction) {
|
||
$0.accountIdentity = identity
|
||
$0.sessionState = nil
|
||
}
|
||
}
|
||
|
||
// MARK: - Becoming deregistered while registering
|
||
|
||
private func becameDeregisteredBeforeCompleting(
|
||
accountIdentity: AccountIdentity
|
||
) -> Guarantee<RegistrationStep> {
|
||
Logger.info("")
|
||
|
||
let kickBackToReRegistration: () -> Guarantee<RegistrationStep> = { [weak self] in
|
||
guard let self else {
|
||
return unretainedSelfError()
|
||
}
|
||
Logger.warn("Got deregistered while completing registration; starting over with re-registration.")
|
||
self.db.write { tx in
|
||
self.wipePersistedState(tx)
|
||
}
|
||
return .value(.showErrorSheet(.becameDeregistered(reregParams: .init(
|
||
e164: accountIdentity.e164,
|
||
aci: accountIdentity.aci
|
||
))))
|
||
}
|
||
|
||
switch mode {
|
||
case .registering, .reRegistering:
|
||
return kickBackToReRegistration()
|
||
case .changingNumber(let changeNumberState):
|
||
if let pniState = changeNumberState.pniState {
|
||
return finalizeChangeNumberPniState(
|
||
changeNumberState: changeNumberState,
|
||
pniState: pniState,
|
||
accountIdentity: accountIdentity
|
||
).then(on: schedulers.main) { result in
|
||
return kickBackToReRegistration()
|
||
}
|
||
} else {
|
||
return kickBackToReRegistration()
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Account objects
|
||
|
||
private func attributes2FAMode(e164: E164) -> AccountAttributes.TwoFactorAuthMode {
|
||
if
|
||
(
|
||
inMemoryState.wasReglockEnabledBeforeStarting
|
||
|| persistedState.e164WithKnownReglockEnabled == e164
|
||
),
|
||
let reglockToken = inMemoryState.reglockToken
|
||
{
|
||
return .v2(reglockToken: reglockToken)
|
||
} else if
|
||
let pinCode = inMemoryState.pinFromDisk,
|
||
inMemoryState.isV12faUser
|
||
{
|
||
return .v1(pinCode: pinCode)
|
||
} else {
|
||
return .none
|
||
}
|
||
}
|
||
|
||
private func makeAccountAttributes(
|
||
isManualMessageFetchEnabled: Bool,
|
||
twoFAMode: AccountAttributes.TwoFactorAuthMode
|
||
) -> AccountAttributes {
|
||
let hasSVRBackups: Bool
|
||
switch getPathway() {
|
||
case
|
||
.opening,
|
||
.registrationRecoveryPassword,
|
||
.svrAuthCredential,
|
||
.svrAuthCredentialCandidates,
|
||
.session:
|
||
hasSVRBackups = inMemoryState.didHaveSVRBackupsPriorToReg
|
||
case .profileSetup:
|
||
if inMemoryState.didHaveSVRBackupsPriorToReg && !inMemoryState.didSkipSVRBackup {
|
||
hasSVRBackups = true
|
||
} else if inMemoryState.hasRestoredFromStorageService {
|
||
hasSVRBackups = true
|
||
} else if inMemoryState.hasBackedUpToSVR {
|
||
hasSVRBackups = true
|
||
} else {
|
||
hasSVRBackups = false
|
||
}
|
||
}
|
||
return AccountAttributes(
|
||
isManualMessageFetchEnabled: isManualMessageFetchEnabled,
|
||
registrationId: inMemoryState.registrationId,
|
||
pniRegistrationId: inMemoryState.pniRegistrationId,
|
||
unidentifiedAccessKey: inMemoryState.udAccessKey.keyData.base64EncodedString(),
|
||
unrestrictedUnidentifiedAccess: inMemoryState.allowUnrestrictedUD,
|
||
twofaMode: twoFAMode,
|
||
registrationRecoveryPassword: inMemoryState.regRecoveryPw,
|
||
encryptedDeviceName: nil, // This class only deals in primary devices, which have no name
|
||
discoverableByPhoneNumber: inMemoryState.phoneNumberDiscoverability,
|
||
hasSVRBackups: hasSVRBackups
|
||
)
|
||
}
|
||
|
||
private func fetchApnRegistrationId() -> Guarantee<Registration.RequestPushTokensResult> {
|
||
guard !inMemoryState.isManualMessageFetchEnabled else {
|
||
return .value(.pushUnsupported(description: "Manual fetch pre-enabled"))
|
||
}
|
||
return deps.pushRegistrationManager.requestPushToken()
|
||
}
|
||
|
||
private func generateServerAuthToken() -> String {
|
||
return Randomness.generateRandomBytes(16).hexadecimalString
|
||
}
|
||
|
||
struct AccountIdentity: Codable {
|
||
@AciUuid var aci: Aci
|
||
@PniUuid var pni: Pni
|
||
let e164: E164
|
||
let hasPreviouslyUsedSVR: Bool
|
||
|
||
/// The auth token used to communicate with the server.
|
||
/// We create this locally and include it in the create account request,
|
||
/// then use it to authenticate subsequent requests.
|
||
let authPassword: String
|
||
|
||
var authUsername: String {
|
||
return aci.serviceIdString
|
||
}
|
||
|
||
var authedAccount: AuthedAccount {
|
||
return AuthedAccount.explicit(
|
||
aci: aci,
|
||
pni: pni,
|
||
e164: e164,
|
||
deviceId: .primary,
|
||
authPassword: authPassword
|
||
)
|
||
}
|
||
|
||
var authedDevice: AuthedDevice {
|
||
return .explicit(AuthedDevice.Explicit(
|
||
aci: aci,
|
||
phoneNumber: e164,
|
||
pni: pni,
|
||
deviceId: .primary,
|
||
authPassword: authPassword
|
||
))
|
||
}
|
||
|
||
var chatServiceAuth: ChatServiceAuth {
|
||
return ChatServiceAuth.explicit(
|
||
aci: aci,
|
||
deviceId: .primary,
|
||
password: authPassword
|
||
)
|
||
}
|
||
|
||
var localIdentifiers: LocalIdentifiers {
|
||
return AuthedDevice.Explicit(
|
||
aci: aci,
|
||
phoneNumber: e164,
|
||
pni: pni,
|
||
deviceId: .primary,
|
||
authPassword: authPassword
|
||
).localIdentifiers
|
||
}
|
||
}
|
||
|
||
enum AccountResponse {
|
||
case success(AccountIdentity)
|
||
case reglockFailure(RegistrationServiceResponses.RegistrationLockFailureResponse)
|
||
/// The verification method attempted was rejected.
|
||
/// Either the session was invalid/expired or the registration recovery password was wrong.
|
||
case rejectedVerificationMethod
|
||
case deviceTransferPossible
|
||
case retryAfter(TimeInterval)
|
||
case networkError
|
||
case genericError
|
||
}
|
||
|
||
// MARK: - Step State Generation Helpers
|
||
|
||
private enum RemoteValidationError {
|
||
case invalidE164(RegistrationPhoneNumberViewState.ValidationError.InvalidE164)
|
||
case rateLimited(RegistrationPhoneNumberViewState.ValidationError.RateLimited)
|
||
|
||
func asViewStateError() -> RegistrationPhoneNumberViewState.ValidationError {
|
||
switch self {
|
||
case let .invalidE164(error):
|
||
return .invalidE164(error)
|
||
case let .rateLimited(error):
|
||
return .rateLimited(error)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func phoneNumberEntryState(
|
||
validationError: RemoteValidationError? = nil
|
||
) -> RegistrationPhoneNumberViewState {
|
||
switch mode {
|
||
case .registering:
|
||
return .registration(.initialRegistration(.init(
|
||
previouslyEnteredE164: persistedState.e164,
|
||
validationError: validationError?.asViewStateError(),
|
||
canExitRegistration: canExitRegistrationFlow().canExit
|
||
)))
|
||
case .reRegistering(let state):
|
||
return .registration(.reregistration(.init(
|
||
e164: state.e164,
|
||
validationError: validationError?.asViewStateError(),
|
||
canExitRegistration: canExitRegistrationFlow().canExit
|
||
)))
|
||
case .changingNumber(let state):
|
||
var rateLimitedError: RegistrationPhoneNumberViewState.ValidationError.RateLimited?
|
||
switch validationError {
|
||
case .none:
|
||
break
|
||
case .rateLimited(let error):
|
||
rateLimitedError = error
|
||
case .invalidE164(let invalidE164Error):
|
||
return .changingNumber(.initialEntry(.init(
|
||
oldE164: state.oldE164,
|
||
newE164: inMemoryState.changeNumberProspectiveE164,
|
||
hasConfirmed: inMemoryState.changeNumberProspectiveE164 != nil,
|
||
invalidE164Error: invalidE164Error
|
||
)))
|
||
}
|
||
if let newE164 = inMemoryState.changeNumberProspectiveE164 {
|
||
return .changingNumber(.confirmation(.init(
|
||
oldE164: state.oldE164,
|
||
newE164: newE164,
|
||
rateLimitedError: rateLimitedError
|
||
)))
|
||
} else {
|
||
return .changingNumber(.initialEntry(.init(
|
||
oldE164: state.oldE164,
|
||
newE164: nil,
|
||
hasConfirmed: false,
|
||
invalidE164Error: nil
|
||
)))
|
||
}
|
||
}
|
||
}
|
||
|
||
private func verificationCodeEntryState(
|
||
session: RegistrationSession,
|
||
validationError: RegistrationVerificationValidationError? = nil
|
||
) -> RegistrationVerificationState {
|
||
let exitConfiguration: RegistrationVerificationState.ExitConfiguration
|
||
if canExitRegistrationFlow().canExit {
|
||
switch mode {
|
||
case .registering:
|
||
exitConfiguration = .noExitAllowed
|
||
case .reRegistering:
|
||
exitConfiguration = .exitReRegistration
|
||
case .changingNumber:
|
||
exitConfiguration = .exitChangeNumber
|
||
}
|
||
} else {
|
||
exitConfiguration = .noExitAllowed
|
||
}
|
||
|
||
let canChangeE164: Bool
|
||
switch mode {
|
||
case .reRegistering:
|
||
canChangeE164 = false
|
||
case .registering, .changingNumber:
|
||
canChangeE164 = true
|
||
}
|
||
|
||
return RegistrationVerificationState(
|
||
e164: session.e164,
|
||
nextSMSDate: session.nextSMSDate,
|
||
nextCallDate: session.nextCallDate,
|
||
nextVerificationAttemptDate: session.nextVerificationAttemptDate,
|
||
canChangeE164: canChangeE164,
|
||
// TODO[Registration]: pass up the number directly here, and test for it.
|
||
showHelpText: (persistedState.sessionState?.numVerificationCodeSubmissions ?? 0) >= 3,
|
||
validationError: validationError,
|
||
exitConfiguration: exitConfiguration
|
||
)
|
||
}
|
||
|
||
private func pinCodeEntryExitConfiguration() -> RegistrationPinState.ExitConfiguration {
|
||
guard canExitRegistrationFlow().canExit else {
|
||
return .noExitAllowed
|
||
}
|
||
switch mode {
|
||
case .registering:
|
||
return .noExitAllowed
|
||
case .reRegistering:
|
||
return .exitReRegistration
|
||
case .changingNumber:
|
||
return .exitChangeNumber
|
||
}
|
||
}
|
||
|
||
private func contactSupportRegistrationPINMode() -> ContactSupportRegistrationPINMode {
|
||
switch getPathway() {
|
||
case .opening:
|
||
owsFailBeta("Should not be asking for PIN during opening path.")
|
||
return .v2WithUnknownReglockState
|
||
case .svrAuthCredential, .svrAuthCredentialCandidates, .registrationRecoveryPassword:
|
||
if
|
||
let e164 = persistedState.e164,
|
||
e164 == persistedState.e164WithKnownReglockEnabled
|
||
{
|
||
return .v2WithReglock
|
||
}
|
||
return .v2WithUnknownReglockState
|
||
case .session:
|
||
return .v2WithReglock
|
||
case .profileSetup:
|
||
if inMemoryState.isV12faUser {
|
||
return .v1
|
||
} else {
|
||
// If they are in profile setup that means they
|
||
// would have gotten past reglock already.
|
||
return .v2NoReglock
|
||
}
|
||
}
|
||
}
|
||
|
||
private var reglockTimeoutAcknowledgeAction: RegistrationReglockTimeoutAcknowledgeAction {
|
||
switch mode {
|
||
case .registering: return .resetPhoneNumber
|
||
case .reRegistering, .changingNumber:
|
||
if canExitRegistrationFlow().canExit {
|
||
return .close
|
||
} else {
|
||
return .none
|
||
}
|
||
}
|
||
}
|
||
|
||
private var verificationCodeSubmissionRejectedError: RegistrationStep {
|
||
switch persistedState.sessionState?.initialCodeRequestState {
|
||
case
|
||
.none,
|
||
.neverRequested,
|
||
.failedToRequest,
|
||
.permanentProviderFailure,
|
||
.transientProviderFailure,
|
||
.smsTransportFailed:
|
||
return .showErrorSheet(.submittingVerificationCodeBeforeAnyCodeSent)
|
||
case .exhaustedCodeAttempts, .requested:
|
||
return .showErrorSheet(.verificationCodeSubmissionUnavailable)
|
||
}
|
||
}
|
||
|
||
private func shouldAttemptToReclaimUsername() -> Usernames.LocalUsernameState? {
|
||
switch mode {
|
||
case .registering, .reRegistering:
|
||
switch inMemoryState.usernameReclamationState {
|
||
case .localUsernameStateNotLoaded, .reclamationAttempted:
|
||
return nil
|
||
case .localUsernameStateLoaded(let localUsernameState):
|
||
return localUsernameState
|
||
}
|
||
case .changingNumber:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
private func shouldRestoreFromMessageBackup() -> Bool {
|
||
switch mode {
|
||
case .registering:
|
||
return
|
||
deps.featureFlags.messageBackupFileAlphaRegistrationFlow
|
||
&& inMemoryState.hasBackedUpToSVR
|
||
&& !inMemoryState.hasRestoredFromLocalMessageBackup
|
||
&& !inMemoryState.hasSkippedRestoreFromMessageBackup
|
||
case .changingNumber, .reRegistering:
|
||
return false
|
||
}
|
||
}
|
||
|
||
private func shouldRestoreFromStorageService() -> Bool {
|
||
switch mode {
|
||
case .registering, .reRegistering:
|
||
return !inMemoryState.hasRestoredFromStorageService
|
||
&& !inMemoryState.hasSkippedRestoreFromStorageService
|
||
case .changingNumber:
|
||
return false
|
||
}
|
||
}
|
||
|
||
private func shouldRefreshOneTimePreKeys() -> Bool {
|
||
switch mode {
|
||
case .registering, .reRegistering:
|
||
return !persistedState.didRefreshOneTimePreKeys
|
||
case .changingNumber:
|
||
return false
|
||
}
|
||
}
|
||
|
||
// MARK: - Exit
|
||
|
||
private enum RegExitState {
|
||
case allowed(shouldWipeState: Bool)
|
||
case notAllowed
|
||
|
||
var canExit: Bool {
|
||
switch self {
|
||
case .allowed:
|
||
return true
|
||
case .notAllowed:
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
|
||
private func canExitRegistrationFlow() -> RegExitState {
|
||
switch mode {
|
||
case .registering:
|
||
if persistedState.hasResetForReRegistration {
|
||
// Once you have reset its too late.
|
||
return .notAllowed
|
||
}
|
||
// If we had a bug that puts you into the reg flow despite being registered,
|
||
// we make that bug worse by keeping you in the reg flow forever. So allow
|
||
// exiting only if the reg state was registered. Doing so should wipe your state.
|
||
guard inMemoryState.tsRegistrationState?.isRegistered == true else {
|
||
return .notAllowed
|
||
}
|
||
return .allowed(shouldWipeState: true)
|
||
case .reRegistering:
|
||
if persistedState.hasResetForReRegistration {
|
||
// Once you have reset its too late.
|
||
return .notAllowed
|
||
}
|
||
// Wipe if you were previously registered, so we don't send you here
|
||
// on every app launch. If you were deregistered, we _want_ to send
|
||
// you here by default and save your progress, so don't wipe state.
|
||
return .allowed(shouldWipeState: inMemoryState.tsRegistrationState?.isRegistered == true)
|
||
case .changingNumber(let state):
|
||
return state.pniState == nil ? .allowed(shouldWipeState: true) : .notAllowed
|
||
}
|
||
}
|
||
|
||
// MARK: - Constants
|
||
|
||
enum Constants {
|
||
static let persistedStateKey = "state"
|
||
|
||
// how many times we will retry network errors.
|
||
static let networkErrorRetries = 1
|
||
|
||
// If a request that can be retried has a timeout below this
|
||
// threshold, we will auto-retry it.
|
||
// (e.g. you try sending an sms code and the nextSMS is less than this.)
|
||
static let autoRetryInterval: TimeInterval = 0.5
|
||
|
||
// If we have a PIN and SVR master key locally (only possible for re-registration)
|
||
// then we reuse it to register. We make the user guess the PIN before proceeding,
|
||
// though. This is how many tries they have before we wipe our local state and make
|
||
// them go through re-registration.
|
||
static let maxLocalPINGuesses: UInt = 10
|
||
|
||
/// How long we wait for a push challenge to the exclusion of all else after requesting one.
|
||
/// Even if we have another challenge to fulfill, we will wait this long before proceeding.
|
||
static let pushTokenMinWaitTime: TimeInterval = 3
|
||
/// How long we block waiting for a push challenge after requesting one.
|
||
/// We might still fulfill the challenge after this, but we won't opportunistically block proceeding.
|
||
static let pushTokenTimeout: TimeInterval = 30
|
||
}
|
||
}
|
||
|
||
private func unretainedSelfError() -> Guarantee<RegistrationStep> {
|
||
return .value(unretainedSelfErrorStep())
|
||
}
|
||
|
||
private func unretainedSelfErrorStep() -> RegistrationStep {
|
||
Logger.warn("Registration coordinator reference lost. Showing generic error")
|
||
return .showErrorSheet(.genericError)
|
||
}
|
||
|
||
extension Error {
|
||
|
||
fileprivate var isPostRegDeregisteredError: Bool {
|
||
guard let statusCode = (self as? OWSHTTPError)?.responseStatusCode else {
|
||
return false
|
||
}
|
||
// We only use REST during registration;
|
||
// Websocket deregisters with a 403 but that doesn't matter.
|
||
return statusCode == 401
|
||
}
|
||
}
|