From a351848582cfb1f831b99e652d005d4b3b853d4f Mon Sep 17 00:00:00 2001 From: Thomas Espach Date: Tue, 27 Aug 2024 14:21:52 +0100 Subject: [PATCH 1/2] Add PhishingDetection wrapper. --- .../PhishingDetection/PhishingDetection.swift | 150 ++++++++++++++++++ .../PhishingDetectionStateManager.swift | 31 ++++ 2 files changed, 181 insertions(+) create mode 100644 DuckDuckGo/PhishingDetection/PhishingDetection.swift create mode 100644 DuckDuckGo/PhishingDetection/PhishingDetectionStateManager.swift diff --git a/DuckDuckGo/PhishingDetection/PhishingDetection.swift b/DuckDuckGo/PhishingDetection/PhishingDetection.swift new file mode 100644 index 0000000000..3cb723783e --- /dev/null +++ b/DuckDuckGo/PhishingDetection/PhishingDetection.swift @@ -0,0 +1,150 @@ +// +// PhishingDetection.swift +// +// 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 +import PhishingDetection +import Combine +import BrowserServicesKit +import PixelKit +import Common + +/// PhishingDetection is implemented using two datasets that are embedded into the client as a Bundle in `DataProvider`, +/// and kept up to date by `DataActivities` and `UpdateManager`. If the feature is disabled in `Preferences`, +/// we stop the background tasks and don't check `isMalicious` on any URLs. +protocol PhishingSiteDetecting { + func checkIsMaliciousIfEnabled(url: URL) async -> Bool +} + +public class PhishingDetection: PhishingSiteDetecting { + static let shared: PhishingDetection = PhishingDetection() + private var detector: PhishingDetecting + private var updateManager: PhishingDetectionUpdateManaging + private var dataActivities: PhishingDetectionDataActivityHandling + private var detectionPreferences: PhishingDetectionPreferences + private var dataStore: PhishingDetectionDataSaving + private var featureFlagger: FeatureFlagger + private var config: PrivacyConfiguration + private var cancellable: AnyCancellable? + private let revision: Int + private let filterSetURL: URL + private let filterSetDataSHA: String + private let hashPrefixURL: URL + private let hashPrefixDataSHA: String + + private init( + revision: Int = 1653367, + filterSetURL: URL = Bundle.main.url(forResource: "filterSet", withExtension: "json")!, + filterSetDataSHA: String = "edd913cb0a579c2b163a01347531ed78976bfaf1d14b96a658c4a39d34a70ffc", + hashPrefixURL: URL = Bundle.main.url(forResource: "hashPrefixes", withExtension: "json")!, + hashPrefixDataSHA: String = "c61349d196c46db9155ca654a0d33368ee0f33766fcd63e5a20f1d5c92026dc5", + detectionClient: PhishingDetectionAPIClient = PhishingDetectionAPIClient(), + dataProvider: PhishingDetectionDataProvider? = nil, + dataStore: PhishingDetectionDataSaving? = nil, + detector: PhishingDetecting? = nil, + updateManager: PhishingDetectionUpdateManaging? = nil, + dataActivities: PhishingDetectionDataActivityHandling? = nil, + detectionPreferences: PhishingDetectionPreferences = PhishingDetectionPreferences.shared, + featureFlagger: FeatureFlagger? = nil + ) { + self.revision = revision + self.filterSetURL = filterSetURL + self.filterSetDataSHA = filterSetDataSHA + self.hashPrefixURL = hashPrefixURL + self.hashPrefixDataSHA = hashPrefixDataSHA + + let resolvedDataProvider = dataProvider ?? PhishingDetectionDataProvider( + revision: revision, + filterSetURL: filterSetURL, + filterSetDataSHA: filterSetDataSHA, + hashPrefixURL: hashPrefixURL, + hashPrefixDataSHA: hashPrefixDataSHA + ) + self.dataStore = dataStore ?? PhishingDetectionDataStore(dataProvider: resolvedDataProvider) + self.detector = detector ?? PhishingDetector(apiClient: detectionClient, dataStore: self.dataStore, eventMapping: + EventMapping {event, _, params, _ in + switch event { + case .errorPageShown(clientSideHit: let clientSideHit): + PixelKit.fire(PhishingDetectionEvents.errorPageShown(clientSideHit: clientSideHit)) + case .iframeLoaded: + PixelKit.fire(PhishingDetectionEvents.iframeLoaded) + case .visitSite: + PixelKit.fire(PhishingDetectionEvents.visitSite) + case .updateTaskFailed48h(error: let error): + PixelKit.fire(PhishingDetectionEvents.updateTaskFailed48h(error: error)) + } + }) + self.updateManager = updateManager ?? PhishingDetectionUpdateManager(client: detectionClient, dataStore: self.dataStore) + self.dataActivities = dataActivities ?? PhishingDetectionDataActivities(phishingDetectionDataProvider: resolvedDataProvider, updateManager: self.updateManager) + self.detectionPreferences = detectionPreferences + self.featureFlagger = NSApp.delegateTyped.featureFlagger + self.config = AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager.privacyConfig + if let featureFlagger = featureFlagger, + featureFlagger.isFeatureOn(.phishingDetection), + self.detectionPreferences.isEnabled { + startUpdateTasks() + } + self.setupBindings() + } + + convenience init( + dataActivities: PhishingDetectionDataActivityHandling, + dataStore: PhishingDetectionDataSaving, + detector: PhishingDetecting + ) { + self.init( + dataStore: dataStore, + detector: detector, + dataActivities: dataActivities + ) + } + + private func setupBindings() { + cancellable = detectionPreferences.$isEnabled.sink { [weak self] isEnabled in + self?.handleIsEnabledChange(enabled: isEnabled) + } + } + + public func checkIsMaliciousIfEnabled(url: URL) async -> Bool { + if config.isFeature(.phishingDetection, enabledForDomain: url.host), + detectionPreferences.isEnabled { + return await detector.isMalicious(url: url) + } else { + return false + } + } + + private func handleIsEnabledChange(enabled: Bool) { + if enabled { + startUpdateTasks() + } else { + stopUpdateTasks() + } + } + + private func startUpdateTasks() { + dataActivities.start() + } + + private func stopUpdateTasks() { + dataActivities.stop() + } + + deinit { + cancellable?.cancel() + } +} diff --git a/DuckDuckGo/PhishingDetection/PhishingDetectionStateManager.swift b/DuckDuckGo/PhishingDetection/PhishingDetectionStateManager.swift new file mode 100644 index 0000000000..c1442733ad --- /dev/null +++ b/DuckDuckGo/PhishingDetection/PhishingDetectionStateManager.swift @@ -0,0 +1,31 @@ +// +// PhishingDetectionStateManager.swift +// +// 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 + +public protocol PhishingTabStateManaging { + var didBypassError: Bool { get set } + var isShowingPhishingError: Bool { get set } +} + +public class PhishingTabStateManager: PhishingTabStateManaging { + public var didBypassError: Bool = false + public var isShowingPhishingError: Bool = false + + public init(){} +} From f27aa3ec68987478354fa2ff83b565d58906fc13 Mon Sep 17 00:00:00 2001 From: Thomas Espach Date: Tue, 27 Aug 2024 14:22:39 +0100 Subject: [PATCH 2/2] Add preferences view and feature flag. --- .../FeatureFlagging/Model/FeatureFlag.swift | 6 +++ .../PhishingDetectionPreferences.swift | 50 +++++++++++++++++++ .../View/PreferencesGeneralView.swift | 16 ++++++ .../View/PreferencesRootView.swift | 1 + 4 files changed, 73 insertions(+) create mode 100644 DuckDuckGo/PhishingDetection/PhishingDetectionPreferences.swift diff --git a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift index 11919d7bd0..8cdc11fd8b 100644 --- a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift +++ b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift @@ -22,6 +22,8 @@ import BrowserServicesKit public enum FeatureFlag: String { case debugMenu case sslCertificatesBypass + case phishingDetection + case phishingDetectionPreferences /// Add experimental atb parameter to SERP queries for internal users to display Privacy Reminder /// https://app.asana.com/0/1199230911884351/1205979030848528/f @@ -43,6 +45,10 @@ extension FeatureFlag: FeatureFlagSourceProviding { return .internalOnly case .sslCertificatesBypass: return .remoteReleasable(.subfeature(sslCertificatesSubfeature.allowBypass)) + case .phishingDetection: + return .remoteReleasable(.subfeature(phishingDetectionSubfeature.allowErrorPage)) + case .phishingDetectionPreferences: + return .remoteReleasable(.subfeature(phishingDetectionSubfeature.allowPreferencesToggle)) case .deduplicateLoginsOnImport: return .remoteReleasable(.subfeature(AutofillSubfeature.deduplicateLoginsOnImport)) case .freemiumPIR: diff --git a/DuckDuckGo/PhishingDetection/PhishingDetectionPreferences.swift b/DuckDuckGo/PhishingDetection/PhishingDetectionPreferences.swift new file mode 100644 index 0000000000..bc2bd31d36 --- /dev/null +++ b/DuckDuckGo/PhishingDetection/PhishingDetectionPreferences.swift @@ -0,0 +1,50 @@ +// +// PhishingDetectionPreferences.swift +// +// 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 +import Combine + +protocol PhishingDetectionPreferencesPersistor { + var isEnabled: Bool { get set } +} + +struct PhishingDetectionPreferencesUserDefaultsPersistor: PhishingDetectionPreferencesPersistor { + + @UserDefaultsWrapper(key: .phishingDetectionEnabled, defaultValue: true) + var isEnabled: Bool +} + +final class PhishingDetectionPreferences: ObservableObject { + + static let shared = PhishingDetectionPreferences() + + @Published + var isEnabled: Bool { + didSet { + persistor.isEnabled = isEnabled + } + } + + init(persistor: PhishingDetectionPreferencesPersistor = PhishingDetectionPreferencesUserDefaultsPersistor()) { + self.persistor = persistor + self.isEnabled = persistor.isEnabled + } + + private var persistor: PhishingDetectionPreferencesPersistor + private var cancellables: Set = [] +} diff --git a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift index 956df10da6..8ecab2ee30 100644 --- a/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesGeneralView.swift @@ -31,9 +31,11 @@ extension Preferences { @ObservedObject var searchModel: SearchPreferences @ObservedObject var tabsModel: TabsPreferences @ObservedObject var dataClearingModel: DataClearingPreferences + @ObservedObject var phishingDetectionModel: PhishingDetectionPreferences @State private var showingCustomHomePageSheet = false @State private var isAddedToDock = false var dockCustomizer: DockCustomizer + let featureFlagger = NSApp.delegateTyped.featureFlagger var body: some View { PreferencePane(UserText.general) { @@ -190,6 +192,20 @@ extension Preferences { isOn: $downloadsModel.alwaysRequestDownloadLocation) } } + + // SECTION 7: Phishing Detection + if featureFlagger.isFeatureOn(.phishingDetectionPreferences) { + PreferencePaneSection(UserText.phishingDetectionHeader) { + PreferencePaneSubSection { + ToggleMenuItem(UserText.phishingDetectionIsEnabled, + isOn: $phishingDetectionModel.isEnabled) + }.padding(.bottom, 5) + Text(UserText.phishingDetectionEnabledWarning) + .font(.footnote) + .foregroundColor(.red) + .padding(.top, 5) + } + } } } } diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index ab2a539db6..a7abfd6f01 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -94,6 +94,7 @@ enum Preferences { searchModel: SearchPreferences.shared, tabsModel: TabsPreferences.shared, dataClearingModel: DataClearingPreferences.shared, + phishingDetectionModel: PhishingDetectionPreferences.shared, dockCustomizer: DockCustomizer()) case .sync: SyncView()