blob: e0e4a3f50a8516a2f57ceea63e9bd893e11c1420 [file] [log] [blame]
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/safe_browsing/android/safe_browsing_api_handler_bridge.h"
#include <memory>
#include <string>
#include <utility>
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/containers/contains.h"
#include "base/containers/flat_set.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/trace_event/trace_event.h"
#include "components/safe_browsing/android/jni_headers/SafeBrowsingApiBridge_jni.h"
#include "components/safe_browsing/android/safe_browsing_api_handler_util.h"
#include "components/safe_browsing/core/browser/db/v4_protocol_manager_util.h"
#include "components/safe_browsing/core/common/features.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
using base::android::AttachCurrentThread;
using base::android::ConvertJavaStringToUTF8;
using base::android::ConvertUTF8ToJavaString;
using base::android::JavaIntArrayToIntVector;
using base::android::JavaParamRef;
using base::android::ScopedJavaLocalRef;
using base::android::ToJavaIntArray;
using content::BrowserThread;
namespace safe_browsing {
namespace {
void RunCallbackOnSBThread(
std::unique_ptr<SafeBrowsingApiHandlerBridge::ResponseCallback> callback,
SBThreatType threat_type,
const ThreatMetadata& metadata) {
auto task_runner = base::FeatureList::IsEnabled(kSafeBrowsingOnUIThread)
? content::GetUIThreadTaskRunner({})
: content::GetIOThreadTaskRunner({});
task_runner->PostTask(
FROM_HERE, base::BindOnce(std::move(*callback), threat_type, metadata));
}
void ReportUmaResult(UmaRemoteCallResult result) {
UMA_HISTOGRAM_ENUMERATION("SB2.RemoteCall.Result", result,
UmaRemoteCallResult::MAX_VALUE);
}
// Validate the values returned from SafeBrowsing API are defined in enum. The
// response can be out of range if there is version mismatch between Chrome and
// the GMSCore APK, or the enums between c++ and java are not aligned.
bool IsResponseFromJavaValid(SafeBrowsingApiLookupResult lookup_result,
SafeBrowsingJavaThreatType threat_type,
const std::vector<int>& threat_attributes) {
bool is_lookup_result_recognized = false;
switch (lookup_result) {
case SafeBrowsingApiLookupResult::SUCCESS:
case SafeBrowsingApiLookupResult::FAILURE:
is_lookup_result_recognized = true;
break;
}
if (!is_lookup_result_recognized) {
return false;
}
bool is_threat_type_recognized = false;
switch (threat_type) {
case SafeBrowsingJavaThreatType::NO_THREAT:
case SafeBrowsingJavaThreatType::UNWANTED_SOFTWARE:
case SafeBrowsingJavaThreatType::POTENTIALLY_HARMFUL_APPLICATION:
case SafeBrowsingJavaThreatType::SOCIAL_ENGINEERING:
case SafeBrowsingJavaThreatType::SUBRESOURCE_FILTER:
case SafeBrowsingJavaThreatType::BILLING:
is_threat_type_recognized = true;
break;
}
if (!is_threat_type_recognized) {
return false;
}
for (int threat_attribute : threat_attributes) {
SafeBrowsingJavaThreatAttribute threat_attribute_enum =
static_cast<SafeBrowsingJavaThreatAttribute>(threat_attribute);
bool is_threat_attribute_recognized = false;
switch (threat_attribute_enum) {
case SafeBrowsingJavaThreatAttribute::CANARY:
case SafeBrowsingJavaThreatAttribute::FRAME_ONLY:
is_threat_attribute_recognized = true;
break;
}
if (!is_threat_attribute_recognized) {
return false;
}
}
// Not checking response_status here. This is to avoid the
// API adding a new success response_status while we haven't integrated the
// new value yet. In this case, we still want to return the threat_type.
// TODO(crbug.com/1444511): Add a histogram to track unrecognized status.
return true;
}
bool IsLookupSuccessful(SafeBrowsingApiLookupResult lookup_result,
SafeBrowsingJavaResponseStatus response_status) {
bool is_lookup_result_success = false;
switch (lookup_result) {
case SafeBrowsingApiLookupResult::SUCCESS:
is_lookup_result_success = true;
break;
case SafeBrowsingApiLookupResult::FAILURE:
break;
}
if (!is_lookup_result_success) {
return false;
}
// Note that we check explicit failure statuses instead of success statuses.
// This is to avoid the API adding a new success response_status while we
// haven't integrated the new value yet. The impact of a missing failure
// status is smaller since the API is expected to return a safe threat type in
// a failure anyway.
bool is_response_status_success = true;
switch (response_status) {
case SafeBrowsingJavaResponseStatus::SUCCESS_WITH_LOCAL_BLOCKLIST:
case SafeBrowsingJavaResponseStatus::SUCCESS_WITH_REAL_TIME:
case SafeBrowsingJavaResponseStatus::SUCCESS_FALLBACK_REAL_TIME_TIMEOUT:
case SafeBrowsingJavaResponseStatus::SUCCESS_FALLBACK_REAL_TIME_THROTTLED:
break;
case SafeBrowsingJavaResponseStatus::FAILURE_NETWORK_UNAVAILABLE:
case SafeBrowsingJavaResponseStatus::FAILURE_BLOCK_LIST_UNAVAILABLE:
is_response_status_success = false;
break;
}
return is_response_status_success;
}
// Convert a SBThreatType to a Java SafetyNet API threat type. We only support
// a few.
SafetyNetJavaThreatType SBThreatTypeToSafetyNetJavaThreatType(
const SBThreatType& sb_threat_type) {
switch (sb_threat_type) {
case SB_THREAT_TYPE_BILLING:
return SafetyNetJavaThreatType::BILLING;
case SB_THREAT_TYPE_SUBRESOURCE_FILTER:
return SafetyNetJavaThreatType::SUBRESOURCE_FILTER;
case SB_THREAT_TYPE_URL_PHISHING:
return SafetyNetJavaThreatType::SOCIAL_ENGINEERING;
case SB_THREAT_TYPE_URL_MALWARE:
return SafetyNetJavaThreatType::POTENTIALLY_HARMFUL_APPLICATION;
case SB_THREAT_TYPE_URL_UNWANTED:
return SafetyNetJavaThreatType::UNWANTED_SOFTWARE;
case SB_THREAT_TYPE_CSD_ALLOWLIST:
return SafetyNetJavaThreatType::CSD_ALLOWLIST;
default:
NOTREACHED();
return SafetyNetJavaThreatType::MAX_VALUE;
}
}
// Convert a vector of SBThreatTypes to JavaIntArray of Java SafetyNet API
// threat types.
ScopedJavaLocalRef<jintArray> SBThreatTypeSetToSafetyNetJavaArray(
JNIEnv* env,
const SBThreatTypeSet& threat_types) {
DCHECK_LT(0u, threat_types.size());
int int_threat_types[threat_types.size()];
int* itr = &int_threat_types[0];
for (auto threat_type : threat_types) {
*itr++ =
static_cast<int>(SBThreatTypeToSafetyNetJavaThreatType(threat_type));
}
return ToJavaIntArray(env, int_threat_types, threat_types.size());
}
// Convert a Java threat type for SafeBrowsing to a SBThreatType.
SBThreatType SafeBrowsingJavaToSBThreatType(
SafeBrowsingJavaThreatType java_threat_num) {
switch (java_threat_num) {
case SafeBrowsingJavaThreatType::NO_THREAT:
return SB_THREAT_TYPE_SAFE;
case SafeBrowsingJavaThreatType::UNWANTED_SOFTWARE:
return SB_THREAT_TYPE_URL_UNWANTED;
case SafeBrowsingJavaThreatType::POTENTIALLY_HARMFUL_APPLICATION:
return SB_THREAT_TYPE_URL_MALWARE;
case SafeBrowsingJavaThreatType::SOCIAL_ENGINEERING:
return SB_THREAT_TYPE_URL_PHISHING;
case SafeBrowsingJavaThreatType::SUBRESOURCE_FILTER:
return SB_THREAT_TYPE_SUBRESOURCE_FILTER;
case SafeBrowsingJavaThreatType::BILLING:
return SB_THREAT_TYPE_BILLING;
}
}
// Convert a SBThreatType to a Java threat type for SafeBrowsing. We only
// support a few.
SafeBrowsingJavaThreatType SBThreatTypeToSafeBrowsingApiJavaThreatType(
const SBThreatType& sb_threat_type) {
switch (sb_threat_type) {
case SB_THREAT_TYPE_URL_UNWANTED:
return SafeBrowsingJavaThreatType::UNWANTED_SOFTWARE;
case SB_THREAT_TYPE_URL_MALWARE:
return SafeBrowsingJavaThreatType::POTENTIALLY_HARMFUL_APPLICATION;
case SB_THREAT_TYPE_URL_PHISHING:
return SafeBrowsingJavaThreatType::SOCIAL_ENGINEERING;
case SB_THREAT_TYPE_SUBRESOURCE_FILTER:
return SafeBrowsingJavaThreatType::SUBRESOURCE_FILTER;
case SB_THREAT_TYPE_BILLING:
return SafeBrowsingJavaThreatType::BILLING;
default:
NOTREACHED();
return SafeBrowsingJavaThreatType::NO_THREAT;
}
}
// Convert a vector of SBThreatTypes to JavaIntArray of SafeBrowsing API's
// threat types.
ScopedJavaLocalRef<jintArray> SBThreatTypeSetToSafeBrowsingJavaArray(
JNIEnv* env,
const SBThreatTypeSet& threat_types) {
DCHECK_LT(0u, threat_types.size());
int int_threat_types[threat_types.size()];
int* itr = &int_threat_types[0];
for (auto threat_type : threat_types) {
*itr++ = static_cast<int>(
SBThreatTypeToSafeBrowsingApiJavaThreatType(threat_type));
}
return ToJavaIntArray(env, int_threat_types, threat_types.size());
}
// The map that holds the callback_id used to reference each pending request
// sent to Java, and the corresponding callback to call on receiving the
// response.
using PendingCallbacksMap = std::unordered_map<
jlong,
std::unique_ptr<SafeBrowsingApiHandlerBridge::ResponseCallback>>;
PendingCallbacksMap& GetPendingSafetyNetCallbacksMapOnSBThread() {
DCHECK_CURRENTLY_ON(base::FeatureList::IsEnabled(kSafeBrowsingOnUIThread)
? content::BrowserThread::UI
: content::BrowserThread::IO);
// Holds the list of callback objects that we are currently waiting to hear
// the result of from GmsCore.
// The key is a unique count-up integer.
static base::NoDestructor<PendingCallbacksMap> pending_safety_net_callbacks;
return *pending_safety_net_callbacks;
}
PendingCallbacksMap& GetPendingSafeBrowsingCallbacksMapOnSBThread() {
DCHECK_CURRENTLY_ON(base::FeatureList::IsEnabled(kSafeBrowsingOnUIThread)
? content::BrowserThread::UI
: content::BrowserThread::IO);
// Holds the list of callback objects that we are currently waiting to hear
// the result of from GmsCore.
// The key is a unique count-up integer.
static base::NoDestructor<PendingCallbacksMap>
pending_safe_browsing_callbacks;
return *pending_safe_browsing_callbacks;
}
bool StartAllowlistCheck(const GURL& url, const SBThreatType& sb_threat_type) {
DCHECK_CURRENTLY_ON(base::FeatureList::IsEnabled(kSafeBrowsingOnUIThread)
? content::BrowserThread::UI
: content::BrowserThread::IO);
JNIEnv* env = AttachCurrentThread();
if (!Java_SafeBrowsingApiBridge_ensureSafetyNetApiInitialized(env)) {
return false;
}
ScopedJavaLocalRef<jstring> j_url = ConvertUTF8ToJavaString(env, url.spec());
int j_threat_type =
static_cast<int>(SBThreatTypeToSafetyNetJavaThreatType(sb_threat_type));
return Java_SafeBrowsingApiBridge_startAllowlistLookup(env, j_url,
j_threat_type);
}
} // namespace
// static
SafeBrowsingApiHandlerBridge& SafeBrowsingApiHandlerBridge::GetInstance() {
static base::NoDestructor<SafeBrowsingApiHandlerBridge> instance;
return *instance.get();
}
// Respond to the URL reputation request by looking up the callback information
// stored in |pending_safety_net_callbacks|.
// |callback_id| is an int form of pointer to a ::ResponseCallback
// that will be called and then deleted here.
// |j_result_status| is one of those from SafeBrowsingApiHandlerBridge.java
// |metadata| is a JSON string classifying the threat if there is one.
void OnUrlCheckDoneOnSBThreadBySafetyNetApi(jlong callback_id,
jint j_result_status,
const std::string metadata) {
DCHECK_CURRENTLY_ON(base::FeatureList::IsEnabled(kSafeBrowsingOnUIThread)
? content::BrowserThread::UI
: content::BrowserThread::IO);
PendingCallbacksMap& pending_callbacks =
GetPendingSafetyNetCallbacksMapOnSBThread();
bool found = base::Contains(pending_callbacks, callback_id);
DCHECK(found) << "Not found in pending_safety_net_callbacks: " << callback_id;
if (!found)
return;
std::unique_ptr<SafeBrowsingApiHandlerBridge::ResponseCallback> callback =
std::move((pending_callbacks)[callback_id]);
pending_callbacks.erase(callback_id);
SafetyNetRemoteCallResultStatus result_status =
static_cast<SafetyNetRemoteCallResultStatus>(j_result_status);
if (result_status != SafetyNetRemoteCallResultStatus::SUCCESS) {
if (result_status == SafetyNetRemoteCallResultStatus::TIMEOUT) {
ReportUmaResult(UmaRemoteCallResult::TIMEOUT);
} else {
DCHECK_EQ(result_status, SafetyNetRemoteCallResultStatus::INTERNAL_ERROR);
ReportUmaResult(UmaRemoteCallResult::INTERNAL_ERROR);
}
std::move(*callback).Run(SB_THREAT_TYPE_SAFE, ThreatMetadata());
return;
}
// Shortcut for safe, so we don't have to parse JSON.
if (metadata == "{}") {
ReportUmaResult(UmaRemoteCallResult::SAFE);
std::move(*callback).Run(SB_THREAT_TYPE_SAFE, ThreatMetadata());
} else {
// Unsafe, assuming we can parse the JSON.
SBThreatType worst_threat;
ThreatMetadata threat_metadata;
ReportUmaResult(
ParseJsonFromGMSCore(metadata, &worst_threat, &threat_metadata));
std::move(*callback).Run(worst_threat, threat_metadata);
}
}
// Java->Native call, invoked when a SafetyNet check is done.
// |callback_id| is a key into the |pending_safety_net_callbacks| map, whose
// value is a ::ResponseCallback that will be called and then deleted on
// the IO thread.
// |result_status| is a @SafeBrowsingResult from SafetyNetApiHandler.java
// |metadata| is a JSON string classifying the threat if there is one.
// |check_delta| is the number of microseconds it took to look up the URL
// reputation from GmsCore.
//
// Careful note: this can be called on multiple threads, so make sure there is
// nothing thread unsafe happening here.
void JNI_SafeBrowsingApiBridge_OnUrlCheckDoneBySafetyNetApi(
JNIEnv* env,
jlong callback_id,
jint result_status,
const JavaParamRef<jstring>& metadata,
jlong check_delta) {
UMA_HISTOGRAM_COUNTS_10M("SB2.RemoteCall.CheckDelta", check_delta);
const std::string metadata_str =
(metadata ? ConvertJavaStringToUTF8(env, metadata) : "");
TRACE_EVENT1("safe_browsing",
"SafeBrowsingApiHandlerBridge::nUrlCheckDoneBySafetyNetApi",
"metadata", metadata_str);
auto task_runner =
base::FeatureList::IsEnabled(safe_browsing::kSafeBrowsingOnUIThread)
? content::GetUIThreadTaskRunner({})
: content::GetIOThreadTaskRunner({});
task_runner->PostTask(
FROM_HERE, base::BindOnce(&OnUrlCheckDoneOnSBThreadBySafetyNetApi,
callback_id, result_status, metadata_str));
}
// Respond to the URL reputation request by looking up the callback information
// stored in |pending_safe_browsing_callbacks|. Must be called on the original
// thread that starts the lookup.
void OnUrlCheckDoneOnSBThreadBySafeBrowsingApi(
jlong callback_id,
SafeBrowsingApiLookupResult lookup_result,
SafeBrowsingJavaThreatType threat_type,
std::vector<int> threat_attributes,
SafeBrowsingJavaResponseStatus response_status) {
DCHECK_CURRENTLY_ON(base::FeatureList::IsEnabled(kSafeBrowsingOnUIThread)
? content::BrowserThread::UI
: content::BrowserThread::IO);
PendingCallbacksMap& pending_callbacks =
GetPendingSafeBrowsingCallbacksMapOnSBThread();
bool found = base::Contains(pending_callbacks, callback_id);
DCHECK(found) << "Not found in pending_safe_browsing_callbacks: "
<< callback_id;
if (!found) {
return;
}
std::unique_ptr<SafeBrowsingApiHandlerBridge::ResponseCallback> callback =
std::move((pending_callbacks)[callback_id]);
pending_callbacks.erase(callback_id);
if (!IsResponseFromJavaValid(lookup_result, threat_type, threat_attributes)) {
std::move(*callback).Run(SB_THREAT_TYPE_SAFE, ThreatMetadata());
return;
}
if (!IsLookupSuccessful(lookup_result, response_status)) {
std::move(*callback).Run(SB_THREAT_TYPE_SAFE, ThreatMetadata());
return;
}
// The API currently doesn't have required threat types
// (ABUSIVE_EXPERIENCE_VIOLATION, BETTER_ADS_VIOLATION) to work with threat
// attributes, so threat attributes are currently disabled. It should not
// affect browse URL checks (mainframe and subresource URLs). However, this
// must be changed before it is used for subresource filter checks.
// Similarly, threat attributes must be consumed if we decide to use malware
// landing info on Android.
std::move(*callback).Run(SafeBrowsingJavaToSBThreatType(threat_type),
ThreatMetadata());
}
// Java->Native call, invoked when a SafeBrowsing check is done. |env| is the
// JNI environment that stores local pointers. |callback_id| is a key into the
// |pending_safe_browsing_callbacks| map, whose value is a ::ResponseCallback
// that will be called and then deleted on the IO thread. |j_lookup_result| is a
// @LookupResult from SafeBrowsingApiHandler.java. |j_threat_type| is the threat
// type that matched against the URL. |j_threat_attributes| is the threat
// attributes that matched against the URL. |j_response_status| reflects how the
// API gets the response. |check_delta_ms| is the number of microseconds it
// took to look up the URL reputation from GmsCore.
//
// Careful note: this can be called on multiple threads, so make sure there is
// nothing thread unsafe happening here.
void JNI_SafeBrowsingApiBridge_OnUrlCheckDoneBySafeBrowsingApi(
JNIEnv* env,
jlong callback_id,
jint j_lookup_result,
jint j_threat_type,
const JavaParamRef<jintArray>& j_threat_attributes,
jint j_response_status,
jlong check_delta_ms) {
// TODO(crbug.com/1444511): Add a histogram to log check_delta_ms.
auto task_runner =
base::FeatureList::IsEnabled(safe_browsing::kSafeBrowsingOnUIThread)
? content::GetUIThreadTaskRunner({})
: content::GetIOThreadTaskRunner({});
SafeBrowsingApiLookupResult lookup_result =
static_cast<SafeBrowsingApiLookupResult>(j_lookup_result);
SafeBrowsingJavaThreatType threat_type =
static_cast<SafeBrowsingJavaThreatType>(j_threat_type);
std::vector<int> threat_attributes;
JavaIntArrayToIntVector(env, j_threat_attributes, &threat_attributes);
SafeBrowsingJavaResponseStatus response_status =
static_cast<SafeBrowsingJavaResponseStatus>(j_response_status);
task_runner->PostTask(
FROM_HERE, base::BindOnce(&OnUrlCheckDoneOnSBThreadBySafeBrowsingApi,
callback_id, lookup_result, threat_type,
std::move(threat_attributes), response_status));
}
//
// SafeBrowsingApiHandlerBridge
//
SafeBrowsingApiHandlerBridge::~SafeBrowsingApiHandlerBridge() {}
void SafeBrowsingApiHandlerBridge::StartHashDatabaseUrlCheck(
std::unique_ptr<ResponseCallback> callback,
const GURL& url,
const SBThreatTypeSet& threat_types) {
StartUrlCheckBySafetyNet(std::move(callback), url, threat_types);
}
void SafeBrowsingApiHandlerBridge::StartHashRealTimeUrlCheck(
std::unique_ptr<ResponseCallback> callback,
const GURL& url,
const SBThreatTypeSet& threat_types) {
StartUrlCheckBySafeBrowsing(std::move(callback), url, threat_types,
SafeBrowsingJavaProtocol::REAL_TIME);
}
void SafeBrowsingApiHandlerBridge::StartUrlCheckBySafetyNet(
std::unique_ptr<ResponseCallback> callback,
const GURL& url,
const SBThreatTypeSet& threat_types) {
if (interceptor_for_testing_) {
// For testing, only check the interceptor.
interceptor_for_testing_->CheckBySafetyNet(std::move(callback), url);
return;
}
DCHECK_CURRENTLY_ON(base::FeatureList::IsEnabled(kSafeBrowsingOnUIThread)
? content::BrowserThread::UI
: content::BrowserThread::IO);
JNIEnv* env = AttachCurrentThread();
if (!Java_SafeBrowsingApiBridge_ensureSafetyNetApiInitialized(env)) {
// Mark all requests as safe. Only users who have an old, broken GMSCore or
// have sideloaded Chrome w/o PlayStore should land here.
RunCallbackOnSBThread(std::move(callback), SB_THREAT_TYPE_SAFE,
ThreatMetadata());
ReportUmaResult(UmaRemoteCallResult::UNSUPPORTED);
return;
}
jlong callback_id = next_safety_net_callback_id_++;
GetPendingSafetyNetCallbacksMapOnSBThread().insert(
{callback_id, std::move(callback)});
DCHECK(!threat_types.empty());
ScopedJavaLocalRef<jstring> j_url = ConvertUTF8ToJavaString(env, url.spec());
ScopedJavaLocalRef<jintArray> j_threat_types =
SBThreatTypeSetToSafetyNetJavaArray(env, threat_types);
Java_SafeBrowsingApiBridge_startUriLookupBySafetyNetApi(
env, callback_id, j_url, j_threat_types);
}
void SafeBrowsingApiHandlerBridge::StartUrlCheckBySafeBrowsing(
std::unique_ptr<ResponseCallback> callback,
const GURL& url,
const SBThreatTypeSet& threat_types,
const SafeBrowsingJavaProtocol& protocol) {
if (interceptor_for_testing_) {
// For testing, only check the interceptor.
interceptor_for_testing_->CheckBySafeBrowsing(std::move(callback), url);
return;
}
DCHECK_CURRENTLY_ON(base::FeatureList::IsEnabled(kSafeBrowsingOnUIThread)
? content::BrowserThread::UI
: content::BrowserThread::IO);
JNIEnv* env = AttachCurrentThread();
// TODO(crbug.com/1444511): Check if the device has required GMSCore version.
// If not, fall back to hash database check through SafetyNet API. Also add a
// histogram to track the proportion of users who don't have required version
// to inform when we can remove the fallback.
jlong callback_id = next_safe_browsing_callback_id_++;
GetPendingSafeBrowsingCallbacksMapOnSBThread().insert(
{callback_id, std::move(callback)});
DCHECK(!threat_types.empty());
ScopedJavaLocalRef<jstring> j_url = ConvertUTF8ToJavaString(env, url.spec());
ScopedJavaLocalRef<jintArray> j_threat_types =
SBThreatTypeSetToSafeBrowsingJavaArray(env, threat_types);
int j_int_protocol = static_cast<int>(protocol);
Java_SafeBrowsingApiBridge_startUriLookupBySafeBrowsingApi(
env, callback_id, j_url, j_threat_types, j_int_protocol);
}
bool SafeBrowsingApiHandlerBridge::StartCSDAllowlistCheck(const GURL& url) {
if (interceptor_for_testing_)
return false;
return StartAllowlistCheck(url, safe_browsing::SB_THREAT_TYPE_CSD_ALLOWLIST);
}
} // namespace safe_browsing