Skip to content

Commit

Permalink
Require device auth to be set in order to use Sync
Browse files Browse the repository at this point in the history
  • Loading branch information
bwaresiak committed Apr 12, 2024
1 parent 3372fce commit f3c5dd9
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 8 deletions.
2 changes: 2 additions & 0 deletions DuckDuckGo/Common/Extensions/URLExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,8 @@ extension URL {

static var fullDiskAccess = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles")!

static var touchIDAndPassword = URL(string: "x-apple.systempreferences:com.apple.preferences.password")!

// MARK: - Blob URLs

var isBlobURL: Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Foundation
enum DeviceAuthenticationResult {
case success
case failure
case noAuthAvailable

var authenticated: Bool {
return self == .success
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ final class LocalAuthenticationService: DeviceAuthenticationService {
func authenticateDevice(reason: String, result: @escaping DeviceAuthenticationResultHandler) {
let context = LAContext()

var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else {
result(.noAuthAvailable)
return
}

context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { authenticated, _ in
DispatchQueue.main.async {
let authenticationResult: DeviceAuthenticationResult = authenticated ? .success : .failure
Expand Down
36 changes: 30 additions & 6 deletions DuckDuckGo/Preferences/Model/SyncPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -559,15 +559,19 @@ extension SyncPreferences: ManagementDialogModelDelegate {
return
}

let data = RecoveryPDFGenerator()
.generate(recoveryCode)

Task { @MainActor in
let authenticationResult = await userAuthenticator.authenticateUser(reason: .syncSettings)
guard authenticationResult.authenticated else {
if authenticationResult == .noAuthAvailable {
presentDialog(for: .empty)
managementDialogModel.syncErrorMessage = SyncErrorMessage(type: .unableToAuthenticateOnDevice, description: "")
}
return
}

let data = RecoveryPDFGenerator()
.generate(recoveryCode)

let panel = NSSavePanel.savePanelWithFileTypeChooser(fileTypes: [.pdf], suggestedFilename: "Sync Data Recovery - DuckDuckGo.pdf")
let response = await panel.begin()

Expand Down Expand Up @@ -607,7 +611,12 @@ extension SyncPreferences: ManagementDialogModelDelegate {

@MainActor
func syncWithAnotherDevicePressed() async {
guard await userAuthenticator.authenticateUser(reason: .syncSettings).authenticated else {
let authenticationResult = await userAuthenticator.authenticateUser(reason: .syncSettings)
guard authenticationResult.authenticated else {
if authenticationResult == .noAuthAvailable {
presentDialog(for: .empty)
managementDialogModel.syncErrorMessage = SyncErrorMessage(type: .unableToAuthenticateOnDevice, description: "")
}
return
}
if isSyncEnabled {
Expand All @@ -619,15 +628,25 @@ extension SyncPreferences: ManagementDialogModelDelegate {

@MainActor
func syncWithServerPressed() async {
guard await userAuthenticator.authenticateUser(reason: .syncSettings).authenticated else {
let authenticationResult = await userAuthenticator.authenticateUser(reason: .syncSettings)
guard authenticationResult.authenticated else {
if authenticationResult == .noAuthAvailable {
presentDialog(for: .empty)
managementDialogModel.syncErrorMessage = SyncErrorMessage(type: .unableToAuthenticateOnDevice, description: "")
}
return
}
presentDialog(for: .syncWithServer)
}

@MainActor
func recoverDataPressed() async {
guard await userAuthenticator.authenticateUser(reason: .syncSettings).authenticated else {
let authenticationResult = await userAuthenticator.authenticateUser(reason: .syncSettings)
guard authenticationResult.authenticated else {
if authenticationResult == .noAuthAvailable {
presentDialog(for: .empty)
managementDialogModel.syncErrorMessage = SyncErrorMessage(type: .unableToAuthenticateOnDevice, description: "")
}
return
}
presentDialog(for: .recoverSyncedData)
Expand All @@ -652,6 +671,11 @@ extension SyncPreferences: ManagementDialogModelDelegate {
showDevicesSynced()
}

@MainActor
func openSystemPasswordSettings() {
NSWorkspace.shared.open(URL.touchIDAndPassword)
}

@MainActor
private func showDevicesSynced() {
presentDialog(for: .nowSyncing)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,30 @@
}
}
},
"alert.sync-device-auth-error" : {
"comment" : "Title for an error alert",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Sync & Backup Error"
}
}
}
},
"alert.sync-device-auth-error-button" : {
"comment" : "Button Title of an error alert",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Go to Settings"
}
}
}
},
"alert.sync-error" : {
"comment" : "Title for sync error alert",
"extractionState" : "extracted_with_value",
Expand Down Expand Up @@ -179,6 +203,18 @@
}
}
},
"alert.unable-to-authenticate-device" : {
"comment" : "Description for unable to authenticate error",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "A device password is required to use Sync & Backup."
}
}
}
},
"alert.unable-to-create-recovery-pdf-description" : {
"comment" : "Description for unable to create recovery pdf error",
"extractionState" : "extracted_with_value",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public protocol ManagementDialogModelDelegate: AnyObject {
func recoveryCodePasted(_ code: String)
func enterRecoveryCodePressed()
func copyCode()
func openSystemPasswordSettings()
}

public final class ManagementDialogModel: ObservableObject {
Expand All @@ -54,6 +55,8 @@ public final class ManagementDialogModel: ObservableObject {
}

public func endFlow() {
syncErrorMessage?.type.onButtonPressed(delegate: delegate)
syncErrorMessage = nil
currentDialog = nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,15 @@ public enum SyncErrorType {
case unableToRemoveDevice
case invalidCode
case unableCreateRecoveryPDF
case unableToAuthenticateOnDevice

var title: String {
return UserText.syncErrorAlertTitle
switch self {
case .unableToAuthenticateOnDevice:
return UserText.syncDeviceAuthenticationErrorAlertTitle
default:
return UserText.syncErrorAlertTitle
}
}

var description: String {
Expand All @@ -93,6 +99,26 @@ public enum SyncErrorType {
return UserText.invalidCodeDescription
case .unableCreateRecoveryPDF:
return UserText.unableCreateRecoveryPdfDescription
case .unableToAuthenticateOnDevice:
return UserText.unableToAuthenticateDevice
}
}

var buttonTitle: String {
switch self {
case .unableToAuthenticateOnDevice:
return UserText.syncDeviceAuthenticationErrorAlertButton
default:
return UserText.ok
}
}

func onButtonPressed(delegate: ManagementDialogModelDelegate?) {
switch self {
case .unableToAuthenticateOnDevice:
delegate?.openSystemPasswordSettings()
default:
break
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public enum ManagementDialogKind: Equatable {
case syncWithServer
case enterRecoveryCode(code: String)
case recoverSyncedData
case empty
}

public struct ManagementDialog: View {
Expand All @@ -49,6 +50,10 @@ public struct ManagementDialog: View {
return typeDescription + "\n" + errorDescription
}

var buttonTitle: String {
return model.syncErrorMessage?.type.buttonTitle ?? UserText.ok
}

public init(model: ManagementDialogModel, recoveryCodeModel: RecoveryCodeViewModel = .init()) {
self.model = model
self.recoveryCodeModel = recoveryCodeModel
Expand All @@ -60,7 +65,7 @@ public struct ManagementDialog: View {
Alert(
title: Text(errorTitle),
message: Text(errorDescription),
dismissButton: .default(Text(UserText.ok)) {
dismissButton: .default(Text(buttonTitle)) {
model.endFlow()
}
)
Expand Down
3 changes: 3 additions & 0 deletions LocalPackages/SyncUI/Sources/SyncUI/internal/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ enum UserText {
}

static let syncErrorAlertTitle = NSLocalizedString("alert.sync-error", bundle: Bundle.module, value: "Sync & Backup Error", comment: "Title for sync error alert")
static let syncDeviceAuthenticationErrorAlertTitle = NSLocalizedString("alert.sync-device-auth-error", bundle: Bundle.module, value: "Sync & Backup Error", comment: "Title for an error alert")
static let syncDeviceAuthenticationErrorAlertButton = NSLocalizedString("alert.sync-device-auth-error-button", bundle: Bundle.module, value: "Go to Settings", comment: "Button Title of an error alert")
static let unableToAuthenticateDevice = NSLocalizedString("alert.unable-to-authenticate-device", bundle: Bundle.module, value: "A device password is required to use Sync & Backup.", comment: "Description for unable to authenticate error")
static let unableToSyncToServerDescription = NSLocalizedString("alert.unable-to-sync-to-server-description", bundle: Bundle.module, value: "Unable to connect to the server.", comment: "Description for unable to sync to server error")
static let unableToSyncWithAnotherDeviceDescription = NSLocalizedString("alert.unable-to-sync-with-another-device-description", bundle: Bundle.module, value: "Unable to Sync with another device.", comment: "Description for unable to sync with another device error")
static let unableToMergeTwoAccountsDescription = NSLocalizedString("alert.unable-to-merge-two-accounts-description", bundle: Bundle.module, value: "To pair these devices, turn off Sync & Backup on one device then tap \"Sync With Another Device\" on the other device.", comment: "Description for unable to merge two accounts error")
Expand Down

0 comments on commit f3c5dd9

Please sign in to comment.