From c2f2c368562e2d1db705019b0c894319f097bb07 Mon Sep 17 00:00:00 2001 From: Cy Okeke Date: Fri, 14 Mar 2025 16:26:32 +0100 Subject: [PATCH] Get the known phishing domain from the server --- .../browser/src/background/main.background.ts | 15 ++ .../services/phishing-detection.service.ts | 141 ++++++++++++++++-- .../scheduling/scheduled-task-name.enum.ts | 1 + 3 files changed, 141 insertions(+), 16 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index cd65220936e..66efb3568fd 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -272,6 +272,8 @@ import BrowserMemoryStorageService from "../platform/services/browser-memory-sto import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service"; import I18nService from "../platform/services/i18n.service"; import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; +import { PhishingApiService } from "../platform/services/phishing-api.service"; +import { PhishingDetectionService } from "../platform/services/phishing-detection.service"; import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { PopupViewCacheBackgroundService } from "../platform/services/popup-view-cache-background.service"; @@ -710,6 +712,15 @@ export default class MainBackground { this.vaultTimeoutSettingsService, ); + // Initialize phishing detection services + const phishingApiService = new PhishingApiService(this.apiService); + PhishingDetectionService.initialize( + phishingApiService, + this.logService, + this.storageService, + this.taskSchedulerService, + ); + this.fileUploadService = new FileUploadService(this.logService, this.apiService); this.cipherFileUploadService = new CipherFileUploadService( this.apiService, @@ -1327,6 +1338,10 @@ export default class MainBackground { this.syncServiceListener?.listener$().subscribe(); await this.autoSubmitLoginBackground.init(); + // Set up phishing detection tab event listeners + const phishingDetectionService = new PhishingDetectionService(); + phishingDetectionService.setupTabEventListeners(); + if ( BrowserApi.isManifestVersion(2) && (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) diff --git a/apps/browser/src/platform/services/phishing-detection.service.ts b/apps/browser/src/platform/services/phishing-detection.service.ts index 01bcb2d874a..20032441ceb 100644 --- a/apps/browser/src/platform/services/phishing-detection.service.ts +++ b/apps/browser/src/platform/services/phishing-detection.service.ts @@ -1,23 +1,139 @@ +import { Subscription } from "rxjs"; + +import { PhishingApiServiceAbstraction } from "@bitwarden/common/abstractions/phishing-api.service.abstraction"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; +import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling/task-scheduler.service"; import { BrowserApi } from "../browser/browser-api"; export class PhishingDetectionService { - private static knownPhishingDomains = new Set(); + private static knownPhishingDomains = new Set(); + private static lastUpdateTime: number = 0; + private static readonly UPDATE_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + private static readonly STORAGE_KEY = "phishing_domains_cache"; + private static phishingApiService: PhishingApiServiceAbstraction; + private static logService: LogService; + private static storageService: AbstractStorageService; + private static taskSchedulerService: TaskSchedulerService; + private static updateCacheSubscription: Subscription | null = null; + private static isUpdating = false; + + static initialize( + phishingApiService: PhishingApiServiceAbstraction, + logService: LogService, + storageService: AbstractStorageService, + taskSchedulerService: TaskSchedulerService, + ) { + PhishingDetectionService.phishingApiService = phishingApiService; + PhishingDetectionService.logService = logService; + PhishingDetectionService.storageService = storageService; + PhishingDetectionService.taskSchedulerService = taskSchedulerService; + + // Register the update task + this.taskSchedulerService.registerTaskHandler( + ScheduledTaskNames.phishingDomainUpdate, + async () => { + try { + await this.updateKnownPhishingDomains(); + } catch (error) { + this.logService.error("Failed to update phishing domains in task handler:", error); + } + }, + ); + + // Initial load of cached domains + void this.loadCachedDomains(); + + // Set up periodic updates every 24 hours + this.setupPeriodicUpdates(); + } + + private static setupPeriodicUpdates() { + // Clean up any existing subscription + if (this.updateCacheSubscription) { + this.updateCacheSubscription.unsubscribe(); + } + + this.updateCacheSubscription = this.taskSchedulerService.setInterval( + ScheduledTaskNames.phishingDomainUpdate, + this.UPDATE_INTERVAL, + ); + } + + private static async loadCachedDomains() { + try { + const cachedData = await this.storageService.get<{ domains: string[]; timestamp: number }>( + this.STORAGE_KEY, + ); + if (cachedData) { + this.knownPhishingDomains = new Set(cachedData.domains); + this.lastUpdateTime = cachedData.timestamp; + } + + // If cache is empty or expired, trigger an immediate update + if ( + this.knownPhishingDomains.size === 0 || + Date.now() - this.lastUpdateTime >= this.UPDATE_INTERVAL + ) { + await this.updateKnownPhishingDomains(); + } + } catch (error) { + this.logService.error("Failed to load cached phishing domains:", error); + } + } static checkUrl(url: string): boolean { const domain = Utils.getDomain(url); - return PhishingDetectionService.knownPhishingDomains.has(domain); + return domain ? PhishingDetectionService.knownPhishingDomains.has(domain) : false; } - // @TODO: We need to flesh this out to actually use the real data that comes from the server. - // This method can be run using a background worker once a day or at a similar interval. - static updateKnownPhishingDomains(): void {} + static async updateKnownPhishingDomains(): Promise { + // Prevent concurrent updates + if (this.isUpdating) { + this.logService.warning("Update already in progress, skipping..."); + return; + } - // @TODO: This can be remove once we implement the real code. - static loadMockedData() { - PhishingDetectionService.knownPhishingDomains.add("google.com"); - PhishingDetectionService.knownPhishingDomains.add("atlassian.net"); + this.isUpdating = true; + try { + const domains = await PhishingDetectionService.phishingApiService.getKnownPhishingDomains(); + + // Clear old domains to prevent memory leaks + PhishingDetectionService.knownPhishingDomains.clear(); + + // Add new domains + domains.forEach((domain: string) => { + if (domain) { + // Only add valid domains + PhishingDetectionService.knownPhishingDomains.add(domain); + } + }); + + PhishingDetectionService.lastUpdateTime = Date.now(); + + // Cache the updated domains + await this.storageService.save(this.STORAGE_KEY, { + domains: Array.from(this.knownPhishingDomains), + timestamp: this.lastUpdateTime, + }); + } catch (error) { + PhishingDetectionService.logService.error("Failed to update phishing domains:", error); + } finally { + this.isUpdating = false; + } + } + + static cleanup() { + if (this.updateCacheSubscription) { + this.updateCacheSubscription.unsubscribe(); + this.updateCacheSubscription = null; + } + this.knownPhishingDomains.clear(); + this.lastUpdateTime = 0; + this.isUpdating = false; } static async getActiveUrl(): Promise { @@ -38,10 +154,6 @@ export class PhishingDetectionService { BrowserApi.addListener(chrome.tabs.onUpdated, async (tabId, changeInfo, tab) => { if (changeInfo.status === "complete") { const activeUrl = await PhishingDetectionService.getActiveUrl(); - - // Debugging - console.log("Tab changed:", { tab, changeInfo, tabId }); - const isPhishingDomain = PhishingDetectionService.checkUrl(activeUrl); if (isPhishingDomain) { @@ -51,6 +163,3 @@ export class PhishingDetectionService { }); } } - -// Initializing the data for local development -PhishingDetectionService.loadMockedData(); diff --git a/libs/common/src/platform/scheduling/scheduled-task-name.enum.ts b/libs/common/src/platform/scheduling/scheduled-task-name.enum.ts index 990a4c77c94..7cc96873748 100644 --- a/libs/common/src/platform/scheduling/scheduled-task-name.enum.ts +++ b/libs/common/src/platform/scheduling/scheduled-task-name.enum.ts @@ -8,6 +8,7 @@ export const ScheduledTaskNames = { eventUploadsInterval: "eventUploadsInterval", vaultTimeoutCheckInterval: "vaultTimeoutCheckInterval", clearPopupViewCache: "clearPopupViewCache", + phishingDomainUpdate: "phishingDomainUpdate", } as const; export type ScheduledTaskName = (typeof ScheduledTaskNames)[keyof typeof ScheduledTaskNames];