diff --git a/Signal/Registration/RegistrationCoordinator.swift b/Signal/Registration/RegistrationCoordinator.swift index 3086b6f72e..ddbfae2a4f 100644 --- a/Signal/Registration/RegistrationCoordinator.swift +++ b/Signal/Registration/RegistrationCoordinator.swift @@ -46,6 +46,12 @@ public protocol RegistrationCoordinator { /// This gives the user a chance to change it before any automatic steps are taken. func submitE164(_ e164: E164) -> Guarantee + /// Wipes any previously submitted E164 so the user can enter a new one. + /// + /// May also blow away in progress registration steps and require redoing them; + /// a new number is essentially a new registration. + func requestChangeE164() -> Guarantee + /// Request an SMS code be sent, returning the next step to take. /// If requesting a code is disallowed for any reason, the next step will be the same current /// step but with attached metadata giving more info on the reason. diff --git a/Signal/Registration/RegistrationCoordinatorImpl.swift b/Signal/Registration/RegistrationCoordinatorImpl.swift index 376feed2c8..52d6d88c88 100644 --- a/Signal/Registration/RegistrationCoordinatorImpl.swift +++ b/Signal/Registration/RegistrationCoordinatorImpl.swift @@ -102,32 +102,29 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator { } public func submitE164(_ e164: E164) -> Guarantee { - // TODO[Registration] do some validation on the e164 format? - // maybe we trust the view controller to do that. db.write { tx in updatePersistedState(tx) { $0.e164 = e164 } } inMemoryState.hasEnteredE164 = true - switch getPathway() { - case .opening: - // Now we transition to the session path since - // we are submitting an e164. - return self.startSession(e164: e164) - case .registrationRecoveryPassword(let password): - return nextStepForRegRecoveryPasswordPath(regRecoveryPw: password) - case .kbsAuthCredential: - owsFailBeta("Shouldn't be submitting an e164 for a known valid kbs auth credential") - return nextStep() - case .kbsAuthCredentialCandidates: - return nextStep() - case .session: - return self.startSession(e164: e164) - case .profileSetup: - owsFailBeta("Shouldn't be submitting an e164 in profile setup") - return nextStep() + return nextStep() + } + + public func requestChangeE164() -> Guarantee { + 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! + loadkbsAuthCredentialCandidates(tx) } + inMemoryState.hasEnteredE164 = false + return nextStep() } public func requestSMSCode() -> Guarantee { @@ -606,10 +603,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator { inMemoryState.isV12faUser = true } - let kbsAuthCredentialCandidates = deps.kbsAuthCredentialStore.getAuthCredentials(tx) - if kbsAuthCredentialCandidates.isEmpty.negated { - inMemoryState.kbsAuthCredentialCandidates = kbsAuthCredentialCandidates - } + loadkbsAuthCredentialCandidates(tx) inMemoryState.isManualMessageFetchEnabled = deps.tsAccountManager.isManualMessageFetchEnabled(tx) inMemoryState.registrationId = deps.tsAccountManager.getOrGenerateRegistrationId(tx) inMemoryState.pniRegistrationId = deps.tsAccountManager.getOrGeneratePniRegistrationId(tx) @@ -1058,6 +1052,13 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator { } } + private func loadkbsAuthCredentialCandidates(_ tx: DBReadTransaction) { + let kbsAuthCredentialCandidates = deps.kbsAuthCredentialStore.getAuthCredentials(tx) + if kbsAuthCredentialCandidates.isEmpty.negated { + inMemoryState.kbsAuthCredentialCandidates = kbsAuthCredentialCandidates + } + } + private func wipeInMemoryStateToPreventKBSPathAttempts() { inMemoryState.reglockToken = nil inMemoryState.regRecoveryPw = nil diff --git a/Signal/Registration/ViewControllers/RegistrationNavigationController.swift b/Signal/Registration/ViewControllers/RegistrationNavigationController.swift index ba158e130b..9e932f18ac 100644 --- a/Signal/Registration/ViewControllers/RegistrationNavigationController.swift +++ b/Signal/Registration/ViewControllers/RegistrationNavigationController.swift @@ -279,7 +279,7 @@ extension RegistrationNavigationController: RegistrationCaptchaPresenter { extension RegistrationNavigationController: RegistrationVerificationPresenter { func returnToPhoneNumberEntry() { - // TODO[Registration]: figure out our story on going back. + pushNextController(coordinator.requestChangeE164()) } func requestSMSCode() { diff --git a/Signal/test/Registration/RegistrationCoordinatorTest.swift b/Signal/test/Registration/RegistrationCoordinatorTest.swift index eebc8575d9..0b882ac4d6 100644 --- a/Signal/test/Registration/RegistrationCoordinatorTest.swift +++ b/Signal/test/Registration/RegistrationCoordinatorTest.swift @@ -1077,6 +1077,116 @@ public class RegistrationCoordinatorTest: XCTestCase { } } + func testKBSAuthCredentialPath_noMatchingCredentialsThenChangeNumber() { + executeTest { + // Don't care about timing, just start it. + scheduler.start() + + // Set profile info so we skip those steps. + setupDefaultAccountAttributes() + + // Put some auth credentials in storage. + let credentialCandidates: [KBSAuthCredential] = [ + Stubs.kbsAuthCredential + ] + kbsAuthCredentialStore.dict = Dictionary(grouping: credentialCandidates, by: \.username).mapValues { $0.first! } + + // Get past the opening. + goThroughOpeningHappyPath(expectedNextStep: .phoneNumberEntry(Stubs.phoneNumberEntryState(mode: mode))) + + scheduler.stop() + scheduler.adjustTime(to: 0) + + let originalE164 = E164("+17875550100")! + let changedE164 = E164("+17875550101")! + + // Give it a phone number, which should cause it to check the auth credentials. + var nextStep = coordinator.submitE164(originalE164) + + // Don't give back any matches at t=2, which means we will want to create a session as a fallback. + var expectedKBSCheckRequest = RegistrationRequestFactory.kbsAuthCredentialCheckRequest( + e164: originalE164, + credentials: credentialCandidates + ) + mockURLSession.addResponse( + TSRequestOWSURLSessionMock.Response( + urlSuffix: expectedKBSCheckRequest.url!.absoluteString, + statusCode: 200, + bodyJson: RegistrationServiceResponses.KBSAuthCheckResponse(matches: [ + "\(Stubs.kbsAuthCredential.username):\(Stubs.kbsAuthCredential.credential.password)": .notMatch + ]) + ), + atTime: 2, + on: scheduler + ) + + // Once the first request fails, at t=2, it should try an start a session. + scheduler.run(atTime: 1) { + // We'll ask for a push challenge, though we don't need to resolve it in this test. + self.pushRegistrationManagerMock.receivePreAuthChallengeTokenMock = { + return Guarantee.pending().0 + } + + // Resolve with a session at time 3. + self.sessionManager.beginSessionResponse = self.scheduler.guarantee( + resolvingWith: .success(Stubs.session(e164: originalE164, hasSentVerificationCode: false)), + atTime: 3 + ) + } + + // Then when it gets back the session at t=3, it should immediately ask for + // a verification code to be sent. + scheduler.run(atTime: 3) { + // Resolve with an updated session at time 4. + self.sessionManager.requestCodeResponse = self.scheduler.guarantee( + resolvingWith: .success(Stubs.session(hasSentVerificationCode: true)), + atTime: 4 + ) + } + + pushRegistrationManagerMock.requestPushTokenMock = { .value(Stubs.apnsToken)} + + scheduler.runUntilIdle() + XCTAssertEqual(scheduler.currentTime, 4) + + // Now we should expect to be at verification code entry since we already set the phone number. + XCTAssertEqual(nextStep.value, .verificationCodeEntry(Stubs.verificationCodeEntryState())) + + // We should have wiped the invalid and unknown credentials. + let remainingCredentials = kbsAuthCredentialStore.dict + XCTAssertNotNil(remainingCredentials[Stubs.kbsAuthCredential.username]) + + // Now change the phone number; this should take us back to phone number entry. + nextStep = coordinator.requestChangeE164() + scheduler.runUntilIdle() + XCTAssertEqual(nextStep.value, .phoneNumberEntry(Stubs.phoneNumberEntryState(mode: mode))) + + // Give it a phone number, which should cause it to check the auth credentials again. + nextStep = coordinator.submitE164(changedE164) + + // Give a match at t=5, so it registers via kbs auth credential. + expectedKBSCheckRequest = RegistrationRequestFactory.kbsAuthCredentialCheckRequest( + e164: changedE164, + credentials: credentialCandidates + ) + mockURLSession.addResponse( + TSRequestOWSURLSessionMock.Response( + urlSuffix: expectedKBSCheckRequest.url!.absoluteString, + statusCode: 200, + bodyJson: RegistrationServiceResponses.KBSAuthCheckResponse(matches: [ + "\(Stubs.kbsAuthCredential.username):\(Stubs.kbsAuthCredential.credential.password)": .match + ]) + ), + atTime: 5, + on: scheduler + ) + + // Now it should ask for PIN entry; we are on the kbs auth credential path. + scheduler.runUntilIdle() + XCTAssertEqual(nextStep.value, .pinEntry(Stubs.pinEntryStateForKBSAuthCredentialPath())) + } + } + // MARK: - Session Path public func testSessionPath_happyPath() { @@ -1389,6 +1499,135 @@ public class RegistrationCoordinatorTest: XCTestCase { } } + public func testSessionPath_changeE164() { + executeTest { + setUpSessionPath() + + let originalE164 = E164("+17875550100")! + let changedE164 = E164("+17875550101")! + + // Give it a phone number, which should cause it to start a session. + var nextStep = coordinator.submitE164(originalE164) + + // We'll ask for a push challenge, though we won't resolve it in this test. + self.pushRegistrationManagerMock.receivePreAuthChallengeTokenMock = { + return Guarantee.pending().0 + } + + // At t=2, give back a session that's ready to go. + self.sessionManager.beginSessionResponse = self.scheduler.guarantee( + resolvingWith: .success(RegistrationSession( + id: Stubs.sessionId, + e164: originalE164, + receivedDate: date, + nextSMS: 0, + nextCall: 0, + nextVerificationAttempt: nil, + allowedToRequestCode: true, + requestedInformation: [], + hasUnknownChallengeRequiringAppUpdate: false, + verified: false + )), + atTime: 2 + ) + + // Once we get that session at t=2, we should try and send a code. + // Be ready for that starting at t=1 (but not before). + scheduler.run(atTime: 1) { + // Give back a session with a sent code. + self.sessionManager.requestCodeResponse = self.scheduler.guarantee( + resolvingWith: .success(RegistrationSession( + id: Stubs.sessionId, + e164: originalE164, + receivedDate: self.date, + nextSMS: 0, + nextCall: 0, + nextVerificationAttempt: 0, + allowedToRequestCode: true, + requestedInformation: [], + hasUnknownChallengeRequiringAppUpdate: false, + verified: false + )), + atTime: 3 + ) + } + + // We should be on the verification code entry screen. + scheduler.runUntilIdle() + XCTAssertEqual(scheduler.currentTime, 3) + XCTAssertEqual( + nextStep.value, + .verificationCodeEntry( + Stubs.verificationCodeEntryState(e164: originalE164) + ) + ) + + // Ask to change the number; this should put us back on phone number entry. + nextStep = coordinator.requestChangeE164() + scheduler.runUntilIdle() + XCTAssertEqual( + nextStep.value, + .phoneNumberEntry(Stubs.phoneNumberEntryState(mode: mode)) + ) + + // Give it the new phone number, which should cause it to start a session. + nextStep = coordinator.submitE164(changedE164) + + // We'll ask for a push challenge, though we won't resolve it in this test. + self.pushRegistrationManagerMock.receivePreAuthChallengeTokenMock = { + return Guarantee.pending().0 + } + + // At t=5, give back a session that's ready to go. + self.sessionManager.beginSessionResponse = self.scheduler.guarantee( + resolvingWith: .success(RegistrationSession( + id: Stubs.sessionId, + e164: changedE164, + receivedDate: date, + nextSMS: 0, + nextCall: 0, + nextVerificationAttempt: nil, + allowedToRequestCode: true, + requestedInformation: [], + hasUnknownChallengeRequiringAppUpdate: false, + verified: false + )), + atTime: 5 + ) + + // Once we get that session at t=5, we should try and send a code. + // Be ready for that starting at t=4 (but not before). + scheduler.run(atTime: 4) { + // Give back a session with a sent code. + self.sessionManager.requestCodeResponse = self.scheduler.guarantee( + resolvingWith: .success(RegistrationSession( + id: Stubs.sessionId, + e164: changedE164, + receivedDate: self.date, + nextSMS: 0, + nextCall: 0, + nextVerificationAttempt: 0, + allowedToRequestCode: true, + requestedInformation: [], + hasUnknownChallengeRequiringAppUpdate: false, + verified: false + )), + atTime: 6 + ) + } + + // We should be on the verification code entry screen. + scheduler.runUntilIdle() + XCTAssertEqual(scheduler.currentTime, 6) + XCTAssertEqual( + nextStep.value, + .verificationCodeEntry( + Stubs.verificationCodeEntryState(e164: changedE164) + ) + ) + } + } + public func testSessionPath_captchaChallenge() { executeTest { setUpSessionPath() @@ -2582,7 +2821,10 @@ public class RegistrationCoordinatorTest: XCTestCase { ) } - static func session(hasSentVerificationCode: Bool) -> RegistrationSession { + static func session( + e164: E164 = Stubs.e164, + hasSentVerificationCode: Bool + ) -> RegistrationSession { return RegistrationSession( id: UUID().uuidString, e164: e164,