blob: ff3f56092317ec32bb25f570559b6dbff1fa42ad [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/safe_browsing/phishy_interaction_tracker.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "build/buildflag.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#include "components/safe_browsing/content/browser/ui_manager.h"
#include "content/public/browser/browser_thread.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/events/keycodes/keyboard_codes.h"
namespace safe_browsing {
namespace {
using base::RecordAction;
using base::UserMetricsAction;
void RecordUserStartsPhishyInteraction() {
RecordAction(UserMetricsAction("PhishyPage.UserStartsInteraction"));
}
void RecordFirstClickEvent() {
RecordAction(UserMetricsAction("PhishyPage.FirstClickEvent"));
}
void RecordFirstKeyEvent() {
RecordAction(UserMetricsAction("PhishyPage.FirstKeyEvent"));
}
void RecordFirstPasteEvent() {
RecordAction(UserMetricsAction("PhishyPage.FirstPasteEvent"));
}
void RecordFinishedInteractionUMAData(int click_count,
int key_count,
int paste_count) {
base::UmaHistogramCounts100("SafeBrowsing.PhishySite.ClickEventCount",
click_count);
base::UmaHistogramCounts100("SafeBrowsing.PhishySite.KeyEventCount",
key_count);
base::UmaHistogramCounts100("SafeBrowsing.PhishySite.PasteEventCount",
paste_count);
RecordAction(UserMetricsAction("PhishyPage.UserStopsInteraction"));
}
} // namespace
PhishyPageInteractionDetails::PhishyPageInteractionDetails(
int occurrence_count,
int64_t first_timestamp,
int64_t last_timestamp)
: occurrence_count(occurrence_count),
first_timestamp(first_timestamp),
last_timestamp(last_timestamp) {}
PhishyInteractionTracker::PhishyInteractionTracker(
content::WebContents* web_contents)
: web_contents_(web_contents), inactivity_delay_(base::Minutes(5)) {
ResetLoggingHelpers();
}
PhishyInteractionTracker::~PhishyInteractionTracker() {
// If there was any data to log, `WebContentsDestroyed()` should have already
// handled it.
DCHECK(!is_phishy_ || is_data_logged_);
}
void PhishyInteractionTracker::WebContentsDestroyed() {
if (is_phishy_ && !is_data_logged_) {
LogPageData();
inactivity_timer_.Stop();
}
}
void PhishyInteractionTracker::HandlePageChanged() {
if (is_phishy_ && !is_data_logged_) {
LogPageData();
}
ResetLoggingHelpers();
inactivity_timer_.Stop();
current_url_ = web_contents_->GetLastCommittedURL().GetWithEmptyPath();
current_page_url_ =
web_contents_->GetController().GetLastCommittedEntry()->GetURL();
is_phishy_ = IsSitePhishy();
if (is_phishy_) {
RecordUserStartsPhishyInteraction();
}
}
void PhishyInteractionTracker::HandlePasteEvent() {
if (is_phishy_) {
HandlePhishyInteraction(ClientSafeBrowsingReportRequest::
PhishySiteInteraction::PHISHY_PASTE_EVENT);
}
}
void PhishyInteractionTracker::HandleInputEvent(
const blink::WebInputEvent& event) {
if (!is_phishy_) {
return;
}
if (event.GetType() == blink::WebInputEvent::Type::kMouseDown) {
HandlePhishyInteraction(ClientSafeBrowsingReportRequest::
PhishySiteInteraction::PHISHY_CLICK_EVENT);
return;
}
#if BUILDFLAG(IS_ANDROID)
// On Android, key down events are triggered if a user types in through a
// number bar on Android keyboard. If text is typed in through other parts of
// Android keyboard, ImeTextCommittedEvent is triggered instead.
if (event.GetType() == blink::WebInputEvent::Type::kKeyDown) {
HandlePhishyInteraction(ClientSafeBrowsingReportRequest::
PhishySiteInteraction::PHISHY_KEY_EVENT);
}
#else // !BUILDFLAG(IS_ANDROID)
if (event.GetType() == blink::WebInputEvent::Type::kChar) {
const blink::WebKeyboardEvent& key_event =
static_cast<const blink::WebKeyboardEvent&>(event);
// Key & 0x1f corresponds to the value of the key when either the control or
// command key is pressed. This detects CTRL+V, COMMAND+V, and CTRL+SHIFT+V.
if (key_event.windows_key_code == (ui::VKEY_V & 0x1f)) {
HandlePhishyInteraction(ClientSafeBrowsingReportRequest::
PhishySiteInteraction::PHISHY_PASTE_EVENT);
} else {
HandlePhishyInteraction(ClientSafeBrowsingReportRequest::
PhishySiteInteraction::PHISHY_KEY_EVENT);
}
}
#endif // BUILDFLAG(IS_ANDROID)
}
void PhishyInteractionTracker::ResetLoggingHelpers() {
is_phishy_ = false;
is_data_logged_ = false;
phishy_page_interaction_data_.clear();
}
bool PhishyInteractionTracker::IsSitePhishy() {
if (!ui_manager_for_testing_ && !g_browser_process->safe_browsing_service()) {
return false;
}
safe_browsing::SafeBrowsingUIManager* ui_manager_ =
ui_manager_for_testing_
? ui_manager_for_testing_.get()
: g_browser_process->safe_browsing_service()->ui_manager().get();
safe_browsing::SBThreatType current_threat_type;
if (!ui_manager_->IsUrlAllowlistedOrPendingForWebContents(
current_url_,
web_contents_->GetController().GetLastCommittedEntry(), web_contents_,
/*allowlist_only=*/true, &current_threat_type)) {
return false;
}
return current_threat_type == SBThreatType::SB_THREAT_TYPE_URL_PHISHING ||
current_threat_type ==
SBThreatType::SB_THREAT_TYPE_URL_CLIENT_SIDE_PHISHING ||
current_threat_type == SBThreatType::SB_THREAT_TYPE_SUSPICIOUS_SITE;
}
void PhishyInteractionTracker::HandlePhishyInteraction(
const ClientSafeBrowsingReportRequest::PhishySiteInteraction::
PhishySiteInteractionType& interaction) {
int new_occurrence_count = 1;
int64_t new_first_timestamp =
base::Time::Now().InMillisecondsSinceUnixEpoch();
int64_t new_last_timestamp = base::Time::Now().InMillisecondsSinceUnixEpoch();
last_interaction_ts_ = base::Time::Now();
// Log if first occurrence of the interaction.
if (!phishy_page_interaction_data_.contains(interaction)) {
RecordFirstInteractionOccurrence(interaction);
} else {
new_occurrence_count +=
phishy_page_interaction_data_.at(interaction).occurrence_count;
new_first_timestamp =
phishy_page_interaction_data_.at(interaction).first_timestamp;
}
phishy_page_interaction_data_.insert_or_assign(
interaction,
PhishyPageInteractionDetails(new_occurrence_count, new_first_timestamp,
new_last_timestamp));
inactivity_timer_.Start(FROM_HERE, inactivity_delay_, this,
&PhishyInteractionTracker::MaybeLogIfUserInactive);
}
void PhishyInteractionTracker::RecordFirstInteractionOccurrence(
ClientSafeBrowsingReportRequest::PhishySiteInteraction::
PhishySiteInteractionType interaction) {
switch (interaction) {
case ClientSafeBrowsingReportRequest::PhishySiteInteraction::
PHISHY_CLICK_EVENT:
RecordFirstClickEvent();
break;
case ClientSafeBrowsingReportRequest::PhishySiteInteraction::
PHISHY_KEY_EVENT:
RecordFirstKeyEvent();
break;
case ClientSafeBrowsingReportRequest::PhishySiteInteraction::
PHISHY_PASTE_EVENT:
RecordFirstPasteEvent();
break;
default:
break;
}
}
void PhishyInteractionTracker::MaybeLogIfUserInactive() {
if (IsUserInactive() && !is_data_logged_) {
LogPageData();
}
}
void PhishyInteractionTracker::LogPageData() {
RecordFinishedInteractionUMAData(
phishy_page_interaction_data_.contains(
ClientSafeBrowsingReportRequest::PhishySiteInteraction::
PHISHY_CLICK_EVENT)
? phishy_page_interaction_data_
.at(ClientSafeBrowsingReportRequest::PhishySiteInteraction::
PHISHY_CLICK_EVENT)
.occurrence_count
: 0,
phishy_page_interaction_data_.contains(
ClientSafeBrowsingReportRequest::PhishySiteInteraction::
PHISHY_KEY_EVENT)
? phishy_page_interaction_data_
.at(ClientSafeBrowsingReportRequest::PhishySiteInteraction::
PHISHY_KEY_EVENT)
.occurrence_count
: 0,
phishy_page_interaction_data_.contains(
ClientSafeBrowsingReportRequest::PhishySiteInteraction::
PHISHY_PASTE_EVENT)
? phishy_page_interaction_data_
.at(ClientSafeBrowsingReportRequest::PhishySiteInteraction::
PHISHY_PASTE_EVENT)
.occurrence_count
: 0);
// Send PHISHY_SITE_INTERACTIONS report for relevant OSes.
#if BUILDFLAG(FULL_SAFE_BROWSING)
g_browser_process->safe_browsing_service()->SendPhishyInteractionsReport(
Profile::FromBrowserContext(web_contents_->GetBrowserContext()),
current_url_, current_page_url_, phishy_page_interaction_data_);
#endif
is_data_logged_ = true;
}
void PhishyInteractionTracker::SetInactivityDelayForTesting(
base::TimeDelta inactivity_delay) {
inactivity_delay_ = inactivity_delay;
}
} // namespace safe_browsing