diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 4e5ffa634d..6f2e4d4e74 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -530,6 +530,8 @@ 981FED76220464EF008488D7 /* AutoClearSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981FED75220464EF008488D7 /* AutoClearSettingsModel.swift */; }; 9820EAF522613CD30089094D /* WebProgressWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9820EAF422613CD30089094D /* WebProgressWorker.swift */; }; 9820FF502244FECC008D4782 /* UIScrollViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9820FF4F2244FECC008D4782 /* UIScrollViewExtension.swift */; }; + 9821234E2B6D0A6300F08C57 /* UserAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9821234D2B6D0A6300F08C57 /* UserAuthenticator.swift */; }; + 982123502B6D233E00F08C57 /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9821234F2B6D233E00F08C57 /* UserSession.swift */; }; 9825F9DB293F2E8700F220F2 /* BookmarksTestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9825F9DA293F2E8700F220F2 /* BookmarksTestData.swift */; }; 982686AD2600C0850011A8D6 /* ActionMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 982686AC2600C0850011A8D6 /* ActionMessageView.swift */; }; 982686B92600C0960011A8D6 /* ActionMessageView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 982686B82600C0960011A8D6 /* ActionMessageView.xib */; }; @@ -1656,6 +1658,8 @@ 9820A5D522B1C0B20024E37C /* DDG Trace.tracetemplate */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = "DDG Trace.tracetemplate"; sourceTree = ""; }; 9820EAF422613CD30089094D /* WebProgressWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebProgressWorker.swift; sourceTree = ""; }; 9820FF4F2244FECC008D4782 /* UIScrollViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollViewExtension.swift; sourceTree = ""; }; + 9821234D2B6D0A6300F08C57 /* UserAuthenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAuthenticator.swift; sourceTree = ""; }; + 9821234F2B6D233E00F08C57 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = ""; }; 9825F9D7293F2DE900F220F2 /* PerformanceTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PerformanceTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 9825F9DA293F2E8700F220F2 /* BookmarksTestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksTestData.swift; sourceTree = ""; }; 982686AC2600C0850011A8D6 /* ActionMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionMessageView.swift; sourceTree = ""; }; @@ -5357,6 +5361,8 @@ 983EABB7236198F6003948D1 /* DatabaseMigration.swift */, 853C5F6021C277C7001F7A05 /* global.swift */, 85C8E61C2B0E47380029A6BD /* BookmarksDatabaseSetup.swift */, + 9821234D2B6D0A6300F08C57 /* UserAuthenticator.swift */, + 9821234F2B6D233E00F08C57 /* UserSession.swift */, ); name = Application; sourceTree = ""; @@ -6765,12 +6771,14 @@ C1F341C52A6924000032057B /* EmailAddressPromptView.swift in Sources */, 316931D727BD10BB0095F5ED /* SaveToDownloadsAlert.swift in Sources */, 31C70B5B2804C61000FB6AD1 /* SaveAutofillLoginManager.swift in Sources */, + 982123502B6D233E00F08C57 /* UserSession.swift in Sources */, 85449EFD23FDA71F00512AAF /* KeyboardSettings.swift in Sources */, 980891A222369ADB00313A70 /* FeedbackUserText.swift in Sources */, 4BCD14692B05BDD5000B1E4C /* AppDelegate+Waitlists.swift in Sources */, 988F3DD3237DE8D900AEE34C /* ForgetDataAlert.swift in Sources */, 850ABD012AC3961100A733DF /* MainViewController+Segues.swift in Sources */, 9817C9C321EF594700884F65 /* AutoClear.swift in Sources */, + 9821234E2B6D0A6300F08C57 /* UserAuthenticator.swift in Sources */, 310C4B47281B60E300BA79A9 /* AutofillLoginDetailsViewModel.swift in Sources */, 85EE7F572246685B000FE757 /* WebContainerViewController.swift in Sources */, 1EC458462948932500CB2B13 /* UIHostingControllerExtension.swift in Sources */, @@ -8158,7 +8166,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -8195,7 +8203,7 @@ CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8287,7 +8295,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -8315,7 +8323,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8465,7 +8473,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -8491,7 +8499,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; INFOPLIST_FILE = DuckDuckGo/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -8556,7 +8564,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -8591,7 +8599,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -8625,7 +8633,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -8656,7 +8664,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -8943,7 +8951,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGo.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -8974,7 +8982,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = ShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -9003,7 +9011,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = OpenAction/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -9037,7 +9045,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Widgets/Info.plist; @@ -9068,7 +9076,7 @@ CODE_SIGN_ENTITLEMENTS = PacketTunnelProvider/PacketTunnelProvider.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; @@ -9101,11 +9109,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 3; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9343,7 +9351,7 @@ CODE_SIGN_ENTITLEMENTS = DuckDuckGo/DuckDuckGoAlpha.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9370,7 +9378,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9403,7 +9411,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9441,7 +9449,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; @@ -9477,7 +9485,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HKE973VLUW; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -9512,11 +9520,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 3; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9690,11 +9698,11 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 3; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -9723,10 +9731,10 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 2; + DYLIB_CURRENT_VERSION = 3; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Core/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; diff --git a/DuckDuckGo/AutofillLoginDetailsViewController.swift b/DuckDuckGo/AutofillLoginDetailsViewController.swift index 73903f128f..c44a48f94f 100644 --- a/DuckDuckGo/AutofillLoginDetailsViewController.swift +++ b/DuckDuckGo/AutofillLoginDetailsViewController.swift @@ -37,7 +37,7 @@ class AutofillLoginDetailsViewController: UIViewController { weak var delegate: AutofillLoginDetailsViewControllerDelegate? private let viewModel: AutofillLoginDetailsViewModel private var cancellables: Set = [] - private var authenticator = AutofillLoginListAuthenticator() + private var authenticator = AutofillLoginListAuthenticator(reason: UserText.autofillLoginListAuthenticationReason) private let lockedView = AutofillItemsLockedView() private let noAuthAvailableView = AutofillNoAuthAvailableView() private var contentView: UIView? diff --git a/DuckDuckGo/AutofillLoginListAuthenticator.swift b/DuckDuckGo/AutofillLoginListAuthenticator.swift index ab047227a5..158583661b 100644 --- a/DuckDuckGo/AutofillLoginListAuthenticator.swift +++ b/DuckDuckGo/AutofillLoginListAuthenticator.swift @@ -22,68 +22,16 @@ import Foundation import LocalAuthentication import Core -final class AutofillLoginListAuthenticator { - enum AuthError: Equatable { - case noAuthAvailable - case failedToAuthenticate - } - - enum AuthenticationState { - case loggedIn, loggedOut, notAvailable - } - - public struct Notifications { - public static let invalidateContext = Notification.Name("com.duckduckgo.app.AutofillLoginListAuthenticator.invalidateContext") - } - - private var context = LAContext() - @Published private(set) var state = AuthenticationState.loggedOut - - func logOut() { - state = .loggedOut - } +final class AutofillLoginListAuthenticator: UserAuthenticator { - func canAuthenticate() -> Bool { - var error: NSError? - let canAuthenticate = LAContext().canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) - return canAuthenticate - } + override func authenticate(completion: ((AuthError?) -> Void)? = nil) { - func authenticate(completion: ((AuthError?) -> Void)? = nil) { - - if state == .loggedIn { - completion?(nil) - return - } - - context = LAContext() - context.localizedCancelTitle = UserText.autofillLoginListAuthenticationCancelButton - let reason = UserText.autofillLoginListAuthenticationReason - context.localizedReason = reason - - if canAuthenticate() { - let reason = reason - context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason ) { success, error in - - DispatchQueue.main.async { - if success { - self.state = .loggedIn - completion?(nil) - } else { - os_log("Failed to authenticate: %s", log: .generalLog, type: .debug, error?.localizedDescription ?? "nil error") - AppDependencyProvider.shared.autofillLoginSession.endSession() - completion?(.failedToAuthenticate) - } - } + super.authenticate { error in + if error != nil { + AppDependencyProvider.shared.autofillLoginSession.endSession() } - } else { - state = .notAvailable - AppDependencyProvider.shared.autofillLoginSession.endSession() - completion?(.noAuthAvailable) - } - } - func invalidateContext() { - context.invalidate() + completion?(error) + } } } diff --git a/DuckDuckGo/AutofillLoginListViewModel.swift b/DuckDuckGo/AutofillLoginListViewModel.swift index 2295c54873..20abb86e80 100644 --- a/DuckDuckGo/AutofillLoginListViewModel.swift +++ b/DuckDuckGo/AutofillLoginListViewModel.swift @@ -61,7 +61,7 @@ final class AutofillLoginListViewModel: ObservableObject { case searchingNoResults } - let authenticator = AutofillLoginListAuthenticator() + let authenticator = AutofillLoginListAuthenticator(reason: UserText.autofillLoginListAuthenticationReason) var isSearching: Bool = false var authenticationNotRequired = false private var accounts = [SecureVaultModels.WebsiteAccount]() @@ -104,7 +104,7 @@ final class AutofillLoginListViewModel: ObservableObject { self.autofillNeverPromptWebsitesManager = autofillNeverPromptWebsitesManager updateData() - authenticationNotRequired = !hasAccountsSaved || AppDependencyProvider.shared.autofillLoginSession.isValidSession + authenticationNotRequired = !hasAccountsSaved || AppDependencyProvider.shared.autofillLoginSession.isSessionValid setupCancellables() } diff --git a/DuckDuckGo/AutofillLoginPromptViewController.swift b/DuckDuckGo/AutofillLoginPromptViewController.swift index 1133e169d2..d2c78acb49 100644 --- a/DuckDuckGo/AutofillLoginPromptViewController.swift +++ b/DuckDuckGo/AutofillLoginPromptViewController.swift @@ -112,7 +112,7 @@ extension AutofillLoginPromptViewController: AutofillLoginPromptViewModelDelegat Pixel.fire(pixel: .autofillLoginsFillLoginInlineManualConfirmed) } - if AppDependencyProvider.shared.autofillLoginSession.isValidSession { + if AppDependencyProvider.shared.autofillLoginSession.isSessionValid { dismiss(animated: true, completion: nil) completion?(account, false) return diff --git a/DuckDuckGo/AutofillLoginSession.swift b/DuckDuckGo/AutofillLoginSession.swift index 6c9fb84ff7..8249fab110 100644 --- a/DuckDuckGo/AutofillLoginSession.swift +++ b/DuckDuckGo/AutofillLoginSession.swift @@ -20,42 +20,21 @@ import Foundation import BrowserServicesKit -class AutofillLoginSession { +final class AutofillLoginSession: UserSession { - private enum Constants { - static let timeout: TimeInterval = 15 - } - - private var sessionCreationDate: Date? private var sessionAccount: SecureVaultModels.WebsiteAccount? - private let sessionTimeout: TimeInterval - - init(sessionTimeout: TimeInterval = Constants.timeout) { - self.sessionTimeout = sessionTimeout - } - - var isValidSession: Bool { - guard let sessionCreationDate = sessionCreationDate else { return false } - let timeInterval = Date().timeIntervalSince(sessionCreationDate) - // Check that timeInterval is > 0 to prevent a user circumventing by changing their device clock time - return timeInterval > 0 && timeInterval < sessionTimeout - } var lastAccessedAccount: SecureVaultModels.WebsiteAccount? { get { - return isValidSession ? sessionAccount : nil + return isSessionValid ? sessionAccount : nil } set { sessionAccount = newValue } } - func startSession() { - sessionCreationDate = Date() - } - - func endSession() { - sessionCreationDate = nil + override func endSession() { + super.endSession() lastAccessedAccount = nil } } diff --git a/DuckDuckGo/SyncSettingsViewController+PDFRendering.swift b/DuckDuckGo/SyncSettingsViewController+PDFRendering.swift index af406c6526..0a1a6e9d17 100644 --- a/DuckDuckGo/SyncSettingsViewController+PDFRendering.swift +++ b/DuckDuckGo/SyncSettingsViewController+PDFRendering.swift @@ -26,12 +26,16 @@ extension SyncSettingsViewController { func shareRecoveryPDF() { - let data = RecoveryPDFGenerator() - .generate(recoveryCode) + authenticateUser { [weak self] error in + guard error == nil, let self else { return } - let pdf = RecoveryCodeItem(data: data) - navigationController?.visibleViewController?.presentShareSheet(withItems: [pdf], - fromView: view) + let data = RecoveryPDFGenerator() + .generate(recoveryCode) + + let pdf = RecoveryCodeItem(data: data) + navigationController?.visibleViewController?.presentShareSheet(withItems: [pdf], + fromView: view) + } } func shareCode(_ code: String) { diff --git a/DuckDuckGo/SyncSettingsViewController+SyncDelegate.swift b/DuckDuckGo/SyncSettingsViewController+SyncDelegate.swift index 6ec419d7a2..1209d011a3 100644 --- a/DuckDuckGo/SyncSettingsViewController+SyncDelegate.swift +++ b/DuckDuckGo/SyncSettingsViewController+SyncDelegate.swift @@ -26,6 +26,18 @@ import AVFoundation extension SyncSettingsViewController: SyncManagementViewModelDelegate { + func authenticateUser() async -> Bool { + return await withCheckedContinuation { continuation in + authenticateUser { error in + if error == nil { + continuation.resume(returning: true) + } else { + continuation.resume(returning: false) + } + } + } + } + func launchAutofillViewController() { guard let mainVC = view.window?.rootViewController as? MainViewController else { return } dismiss(animated: true) @@ -53,17 +65,20 @@ extension SyncSettingsViewController: SyncManagementViewModelDelegate { } func createAccountAndStartSyncing(optionsViewModel: SyncSettingsViewModel) { - Task { @MainActor in - do { - self.dismissPresentedViewController() - self.showPreparingSync() - try await syncService.createAccount(deviceName: deviceName, deviceType: deviceType) - Pixel.fire(pixel: .syncSignupDirect, includedParameters: [.appVersion]) - self.rootView.model.syncEnabled(recoveryCode: recoveryCode) - self.refreshDevices() - navigationController?.topViewController?.dismiss(animated: true, completion: showRecoveryPDF) - } catch { - handleError(SyncErrorMessage.unableToSyncToServer, error: error, event: .syncSignupError) + authenticateUser { [weak self] error in + guard error == nil, let self else { return } + Task { @MainActor in + do { + self.dismissPresentedViewController() + self.showPreparingSync() + try await self.syncService.createAccount(deviceName: self.deviceName, deviceType: self.deviceType) + Pixel.fire(pixel: .syncSignupDirect, includedParameters: [.appVersion]) + self.rootView.model.syncEnabled(recoveryCode: self.recoveryCode) + self.refreshDevices() + self.navigationController?.topViewController?.dismiss(animated: true, completion: self.showRecoveryPDF) + } catch { + self.handleError(SyncErrorMessage.unableToSyncToServer, error: error, event: .syncSignupError) + } } } } @@ -103,12 +118,20 @@ extension SyncSettingsViewController: SyncManagementViewModelDelegate { } func showSyncWithAnotherDevice() { - collectCode(showConnectMode: true) + authenticateUser { [weak self] error in + guard error == nil, let self else { return } + + self.collectCode(showConnectMode: true) + } } func showRecoverData() { - dismissPresentedViewController() - collectCode(showConnectMode: false) + authenticateUser { [weak self] error in + guard error == nil, let self else { return } + + self.dismissPresentedViewController() + self.collectCode(showConnectMode: false) + } } func showDeviceConnected() { diff --git a/DuckDuckGo/SyncSettingsViewController.swift b/DuckDuckGo/SyncSettingsViewController.swift index f49d56003c..7db8cd9667 100644 --- a/DuckDuckGo/SyncSettingsViewController.swift +++ b/DuckDuckGo/SyncSettingsViewController.swift @@ -33,6 +33,9 @@ class SyncSettingsViewController: UIHostingController { let syncBookmarksAdapter: SyncBookmarksAdapter var connector: RemoteConnecting? + let userAuthenticator = UserAuthenticator(reason: UserText.syncUserUserAuthenticationReason) + let userSession = UserSession() + var recoveryCode: String { guard let code = syncService.account?.recoveryCode else { return "" @@ -88,6 +91,19 @@ class SyncSettingsViewController: UIHostingController { fatalError("init(coder:) has not been implemented") } + func authenticateUser(completion: @escaping (UserAuthenticator.AuthError?) -> Void) { + if !userSession.isSessionValid { + userAuthenticator.logOut() + } + + userAuthenticator.authenticate { [weak self] error in + if error == nil { + self?.userSession.startSession() + } + completion(error) + } + } + private func setUpSyncFeatureFlags(_ viewModel: SyncSettingsViewModel) { syncService.featureFlagsPublisher.prepend(syncService.featureFlags) .removeDuplicates() diff --git a/DuckDuckGo/UserAuthenticator.swift b/DuckDuckGo/UserAuthenticator.swift new file mode 100644 index 0000000000..52ab8ae8fb --- /dev/null +++ b/DuckDuckGo/UserAuthenticator.swift @@ -0,0 +1,91 @@ +// +// UserAuthenticator.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Common +import Foundation +import LocalAuthentication +import Core + +class UserAuthenticator { + enum AuthError: Equatable { + case noAuthAvailable + case failedToAuthenticate + } + + enum AuthenticationState { + case loggedIn, loggedOut, notAvailable + } + + public struct Notifications { + public static let invalidateContext = Notification.Name("com.duckduckgo.app.UserAuthenticator.invalidateContext") + } + + private var context = LAContext() + private var reason: String + @Published private(set) var state = AuthenticationState.loggedOut + + init(reason: String) { + self.reason = reason + } + + func logOut() { + state = .loggedOut + } + + func canAuthenticate() -> Bool { + var error: NSError? + let canAuthenticate = LAContext().canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) + return canAuthenticate + } + + func authenticate(completion: ((AuthError?) -> Void)? = nil) { + + if state == .loggedIn { + completion?(nil) + return + } + + context = LAContext() + context.localizedCancelTitle = UserText.autofillLoginListAuthenticationCancelButton + context.localizedReason = reason + + if canAuthenticate() { + let reason = reason + context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason ) { success, error in + + DispatchQueue.main.async { + if success { + self.state = .loggedIn + completion?(nil) + } else { + os_log("Failed to authenticate: %s", log: .generalLog, type: .debug, error?.localizedDescription ?? "nil error") + completion?(.failedToAuthenticate) + } + } + } + } else { + state = .notAvailable + completion?(.noAuthAvailable) + } + } + + func invalidateContext() { + context.invalidate() + } +} diff --git a/DuckDuckGo/UserSession.swift b/DuckDuckGo/UserSession.swift new file mode 100644 index 0000000000..74c608bd20 --- /dev/null +++ b/DuckDuckGo/UserSession.swift @@ -0,0 +1,49 @@ +// +// UserSession.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class UserSession { + + private enum Constants { + static let defaultTimeout: TimeInterval = 15 + } + + private var sessionCreationDate: Date? + private let sessionTimeout: TimeInterval + + public init(sessionTimeout: TimeInterval = Constants.defaultTimeout) { + self.sessionTimeout = sessionTimeout + } + + public var isSessionValid: Bool { + guard let sessionCreationDate = sessionCreationDate else { return false } + let timeInterval = Date().timeIntervalSince(sessionCreationDate) + // Check that timeInterval is > 0 to prevent a user circumventing by changing their device clock time + return timeInterval > 0 && timeInterval < sessionTimeout + } + + public func startSession() { + sessionCreationDate = Date() + } + + public func endSession() { + sessionCreationDate = nil + } +} diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 8d36552646..5b9b476aac 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -861,6 +861,7 @@ But if you *do* want a peek under the hood, you can find more information about // MARK: Sync + public static let syncUserUserAuthenticationReason = NSLocalizedString("sync.user.auth.reason", value:"Unlock device to set up Sync & Backup", comment: "Reason for auth when setting up Sync") public static let syncTurnOffConfirmTitle = NSLocalizedString("sync.turn.off.confirm.title", value:"Turn Off Sync?", comment: "Title of the dialog to confirm turning off Sync") public static let syncTurnOffConfirmMessage = NSLocalizedString("sync.turn.off.confirm.message", value:"This Device will no longer be able to access your synced data.", comment: "Message for the dialog to confirm turning off Sync") public static let syncTurnOffConfirmAction = NSLocalizedString("sync.turn.off.confirm.action", value:"Remove", comment: "Caption for a button to remove current device from Sync") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index 08268ce2dc..19241e7e48 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -2085,6 +2085,9 @@ But if you *do* want a peek under the hood, you can find more information about /* Title of the dialog to confirm turning off Sync */ "sync.turn.off.confirm.title" = "Turn Off Sync?"; +/* Reason for auth when setting up Sync */ +"sync.user.auth.reason" = "Unlock device to set up Sync & Backup"; + /* Data syncing unavailable warning message */ "sync.warning.data.syncing.disabled" = "Sorry, but Sync & Backup is currently unavailable. Please try again later."; diff --git a/DuckDuckGoTests/AutofillLoginSessionTests.swift b/DuckDuckGoTests/AutofillLoginSessionTests.swift index c7e35e346e..c3ed0733c8 100644 --- a/DuckDuckGoTests/AutofillLoginSessionTests.swift +++ b/DuckDuckGoTests/AutofillLoginSessionTests.swift @@ -27,18 +27,18 @@ final class AutofillLoginSessionTests: XCTestCase { private var autofillSession = AutofillLoginSession(sessionTimeout: 2) func testWhenThereIsNoSessionCreationDateThenAutofillSessionIsFalse() { - XCTAssertFalse(autofillSession.isValidSession) + XCTAssertFalse(autofillSession.isSessionValid) } func testWhenSessionStartedThenAutofillSessionIsValid() { autofillSession.startSession() - XCTAssertTrue(autofillSession.isValidSession) + XCTAssertTrue(autofillSession.isSessionValid) } func testWhenSessionEndedThenAutofillSessionIsInvalid() { autofillSession.startSession() autofillSession.endSession() - XCTAssertFalse(autofillSession.isValidSession) + XCTAssertFalse(autofillSession.isSessionValid) } func testWhenSessionExpiredThenAutofillSessionIsInvalid() { @@ -46,7 +46,7 @@ final class AutofillLoginSessionTests: XCTestCase { let sessionExpired = expectation(description: "testWhenSessionExpiredThenAutofillSessionIsInvalid") _ = XCTWaiter.wait(for: [sessionExpired], timeout: 2.2) - XCTAssertFalse(autofillSession.isValidSession) + XCTAssertFalse(autofillSession.isSessionValid) } func testWhenSessionIsValidAndAccountIsSetThenAccountIsReturned() { diff --git a/DuckDuckGoTests/SyncManagementViewModelTests.swift b/DuckDuckGoTests/SyncManagementViewModelTests.swift index 3fbf7ba3fa..d8735416cc 100644 --- a/DuckDuckGoTests/SyncManagementViewModelTests.swift +++ b/DuckDuckGoTests/SyncManagementViewModelTests.swift @@ -22,7 +22,7 @@ import XCTest /// To be fleshed out when UI is settled class SyncManagementViewModelTests: XCTestCase, SyncManagementViewModelDelegate { - + fileprivate var monitor = Monitor() lazy var model: SyncSettingsViewModel = { @@ -32,14 +32,14 @@ class SyncManagementViewModelTests: XCTestCase, SyncManagementViewModelDelegate }() var createAccountAndStartSyncingCalled = false - var caprturedOptionModel: SyncSettingsViewModel? + var capturedOptionModel: SyncSettingsViewModel? func testWhenSingleDeviceSetUpPressed_ThenManagerBecomesBusy_AndAccounCreationRequested() { model.startSyncPressed() XCTAssertTrue(model.isBusy) XCTAssertTrue(createAccountAndStartSyncingCalled) - XCTAssertNotNil(caprturedOptionModel) + XCTAssertNotNil(capturedOptionModel) } func testWhenShowRecoveryPDFPressed_ShowRecoveryPDFIsShown() { @@ -135,13 +135,17 @@ class SyncManagementViewModelTests: XCTestCase, SyncManagementViewModelDelegate } // MARK: Delegate functions + func authenticateUser() async -> Bool { + return true + } + func showSyncWithAnotherDeviceEnterText() { monitor.incrementCalls(function: #function.cleaningFunctionName()) } func createAccountAndStartSyncing(optionsViewModel: SyncSettingsViewModel) { createAccountAndStartSyncingCalled = true - caprturedOptionModel = optionsViewModel + capturedOptionModel = optionsViewModel } func showRecoverData() { diff --git a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift index a51474ebc3..93dd2b1df3 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SyncSettingsViewModel.swift @@ -22,6 +22,7 @@ import UIKit public protocol SyncManagementViewModelDelegate: AnyObject { + func authenticateUser() async -> Bool func showRecoverData() func showSyncWithAnotherDevice() func showRecoveryPDF() @@ -102,6 +103,10 @@ public class SyncSettingsViewModel: ObservableObject { } } + func authenticateUser() async -> Bool { + await delegate?.authenticateUser() ?? false + } + func disableSync() { isBusy = true Task { @MainActor in diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsViewExtension.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsViewExtension.swift index 95922b61a5..bfec4047d5 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsViewExtension.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/SyncSettingsViewExtension.swift @@ -75,7 +75,9 @@ extension SyncSettingsView { Section { Button(UserText.syncAndBackUpThisDeviceLink) { - isSyncWithSetUpSheetVisible = true + Task { @MainActor in + isSyncWithSetUpSheetVisible = await model.authenticateUser() + } } .sheet(isPresented: $isSyncWithSetUpSheetVisible, content: { SyncWithServerView(model: model, onCancel: { @@ -85,7 +87,9 @@ extension SyncSettingsView { .disabled(!model.isAccountCreationAvailable) Button(UserText.recoverSyncedDataLink) { - isRecoverSyncedDataSheetVisible = true + Task { @MainActor in + isRecoverSyncedDataSheetVisible = await model.authenticateUser() + } } .sheet(isPresented: $isRecoverSyncedDataSheetVisible, content: { RecoverSyncedDataView(model: model, onCancel: {