blob: 482f0629a76dcf44ddc5dc05b1ea756280abc31d [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// 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/notification_image_reporter.h"
#include <cmath>
#include <vector>
#include "base/bind.h"
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/memory/ref_counted_memory.h"
#include "base/metrics/histogram_functions.h"
#include "base/rand_util.h"
#include "base/task_scheduler/post_task.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/net/chrome_report_sender.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#include "components/safe_browsing/common/safe_browsing_prefs.h"
#include "components/safe_browsing/db/database_manager.h"
#include "components/safe_browsing/db/whitelist_checker_client.h"
#include "components/safe_browsing/proto/csd.pb.h"
#include "components/variations/variations_associated_data.h"
#include "content/public/browser/browser_thread.h"
#include "net/base/net_errors.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "skia/ext/image_operations.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/geometry/size.h"
#include "url/gurl.h"
using content::BrowserThread;
namespace safe_browsing {
namespace {
const size_t kMaxReportsPerDay = 5;
const base::Feature kNotificationImageReporterFeature{
"NotificationImageReporterFeature", base::FEATURE_ENABLED_BY_DEFAULT};
const char kReportChance[] = "ReportChance";
const char kDefaultMimeType[] = "image/png";
// Passed to ReportSender::Send as an ErrorCallback, so must take a GURL, but it
// is unused.
void LogReportResult(int net_error, int http_response_code) {
base::UmaHistogramSparse("SafeBrowsing.NotificationImageReporter.NetError",
net_error);
}
constexpr net::NetworkTrafficAnnotationTag
kNotificationImageReporterTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("notification_image_reporter", R"(
semantics {
sender: "Safe Browsing"
description:
"When an Image Notification is show on Android, and the user has "
"opted into Safe Browsing Extended Reporting, a small fraction of "
"images from non-whitelisted domains will be uploaded to Google to "
"look for malicious images."
trigger:
"An image notification is triggered, the user is opted-in to "
"extended reporting, and a random dice-roll picks this image to "
"report."
data:
"The actual image and the origin that triggered the notificaton."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: NO
setting:
"Users can control this feature via the 'Automatically report "
"details of possible security incidents to Google' setting under "
"'Privacy'. The feature is disabled by default."
chrome_policy {
SafeBrowsingExtendedReportingOptInAllowed {
policy_options {mode: MANDATORY}
SafeBrowsingExtendedReportingOptInAllowed: false
}
}
})");
} // namespace
const char NotificationImageReporter::kReportingUploadUrl[] =
"https://safebrowsing.googleusercontent.com/safebrowsing/clientreport/"
"notification-image";
NotificationImageReporter::NotificationImageReporter(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: url_loader_factory_(url_loader_factory), weak_factory_(this) {}
NotificationImageReporter::~NotificationImageReporter() {
}
void NotificationImageReporter::ReportNotificationImage(
Profile* profile,
const scoped_refptr<SafeBrowsingDatabaseManager>& database_manager,
const GURL& origin,
const SkBitmap& image) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(profile);
DCHECK_EQ(origin, origin.GetOrigin());
DCHECK(origin.is_valid());
// Skip whitelisted origins to cut down on report volume.
if (!database_manager) {
SkippedReporting();
return;
}
// Query the CSD Whitelist asynchronously on the IO thread.
base::Callback<void(bool)> result_callback =
base::Bind(&NotificationImageReporter::OnWhitelistCheckDoneOnIO,
weak_factory_.GetWeakPtr(), profile, origin, image);
BrowserThread::PostTask(
BrowserThread::IO, FROM_HERE,
base::BindOnce(WhitelistCheckerClient::StartCheckCsdWhitelist,
database_manager, origin, result_callback));
}
void NotificationImageReporter::OnWhitelistCheckDoneOnIO(
base::WeakPtr<NotificationImageReporter> weak_ptr,
Profile* profile,
const GURL& origin,
const SkBitmap& image,
bool match_whitelist) {
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::BindOnce(&NotificationImageReporter::OnWhitelistCheckDone, weak_ptr,
profile, origin, image, match_whitelist));
}
void NotificationImageReporter::OnWhitelistCheckDone(Profile* profile,
const GURL& origin,
const SkBitmap& image,
bool match_whitelist) {
if (match_whitelist) {
SkippedReporting();
return;
}
// Sample a Finch-controlled fraction only.
double report_chance = GetReportChance();
if (base::RandDouble() >= report_chance) {
SkippedReporting();
return;
}
// Avoid exceeding kMaxReportsPerDay.
base::Time a_day_ago = base::Time::Now() - base::TimeDelta::FromDays(1);
while (!report_times_.empty() &&
report_times_.front() < /* older than */ a_day_ago) {
report_times_.pop();
}
if (report_times_.size() >= kMaxReportsPerDay) {
SkippedReporting();
return;
}
// n.b. we write to report_times_ here even if we'll later end up skipping
// reporting because GetExtendedReportingLevel was not SBER_LEVEL_SCOUT. That
// saves us two thread hops, with the downside that we may underreport
// notifications on the first day that a user opts in to SBER_LEVEL_SCOUT.
report_times_.push(base::Time::Now());
// Skip reporting unless SBER2 Scout is enabled.
if (GetExtendedReportingLevel(*profile->GetPrefs()) != SBER_LEVEL_SCOUT) {
SkippedReporting();
return;
}
base::PostTaskWithTraits(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BACKGROUND},
base::BindOnce(
&NotificationImageReporter::DownscaleNotificationImageOnBlockingPool,
weak_factory_.GetWeakPtr(), origin, image));
}
double NotificationImageReporter::GetReportChance() const {
// Get the report_chance from the Finch experiment. If there is no active
// experiment, it will be set to the default of 0.
double report_chance = variations::GetVariationParamByFeatureAsDouble(
kNotificationImageReporterFeature, kReportChance, 0.0);
if (report_chance < 0.0 || report_chance > 1.0) {
DLOG(WARNING) << "Illegal value " << report_chance << " for the parameter "
<< kReportChance << ". The value should be between 0 and 1.";
report_chance = 0.0;
}
return report_chance;
}
void NotificationImageReporter::SkippedReporting() {}
// static
void NotificationImageReporter::DownscaleNotificationImageOnBlockingPool(
const base::WeakPtr<NotificationImageReporter>& weak_ptr,
const GURL& origin,
const SkBitmap& image) {
// Downscale to fit within 512x512. TODO(johnme): Get this from Finch.
const double MAX_SIZE = 512;
SkBitmap downscaled_image = image;
if ((image.width() > MAX_SIZE || image.height() > MAX_SIZE) &&
image.width() > 0 && image.height() > 0) {
double scale =
std::min(MAX_SIZE / image.width(), MAX_SIZE / image.height());
downscaled_image =
skia::ImageOperations::Resize(image, skia::ImageOperations::RESIZE_GOOD,
std::lround(scale * image.width()),
std::lround(scale * image.height()));
}
// Encode as PNG.
std::vector<unsigned char> png_bytes;
if (!gfx::PNGCodec::EncodeBGRASkBitmap(downscaled_image, false, &png_bytes)) {
NOTREACHED();
return;
}
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::BindOnce(
&NotificationImageReporter::SendReport, weak_ptr, origin,
base::RefCountedBytes::TakeVector(&png_bytes),
gfx::Size(downscaled_image.width(), downscaled_image.height()),
gfx::Size(image.width(), image.height())));
}
void NotificationImageReporter::SendReport(
const GURL& origin,
scoped_refptr<base::RefCountedMemory> data,
const gfx::Size& dimensions,
const gfx::Size& original_dimensions) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
NotificationImageReportRequest report;
report.set_notification_origin(origin.spec());
report.mutable_image()->set_data(data->front(), data->size());
report.mutable_image()->set_mime_type(kDefaultMimeType);
report.mutable_image()->mutable_dimensions()->set_width(dimensions.width());
report.mutable_image()->mutable_dimensions()->set_height(dimensions.height());
if (dimensions != original_dimensions) {
report.mutable_image()->mutable_original_dimensions()->set_width(
original_dimensions.width());
report.mutable_image()->mutable_original_dimensions()->set_height(
original_dimensions.height());
}
std::string serialized_report;
report.SerializeToString(&serialized_report);
SendReportInternal(GURL(kReportingUploadUrl), "application/octet-stream",
serialized_report);
}
void NotificationImageReporter::SendReportInternal(
const GURL& url,
const std::string& content_type,
const std::string& report) {
::SendReport(url_loader_factory_, kNotificationImageReporterTrafficAnnotation,
url, content_type, report,
base::Bind(&LogReportResult, net::OK, net::HTTP_OK),
base::Bind(&LogReportResult));
// TODO(johnme): Consider logging bandwidth and/or duration to UMA.
}
} // namespace safe_browsing