Commit 509a8ddd authored by Tarun Bansal's avatar Tarun Bansal Committed by Commit Bot

Add random noise to client hints

Host specific random noise is added to the client hints.

Change-Id: Ic08ac42095e9f484f91f1d8123b79b749eb3fe13
Bug: 826950
Reviewed-on: https://chromium-review.googlesource.com/1002232
Commit-Queue: Tarun Bansal <tbansal@chromium.org>
Reviewed-by: default avatarRyan Sturm <ryansturm@chromium.org>
Cr-Commit-Position: refs/heads/master@{#549571}
parent 5d11ee41
...@@ -3,9 +3,13 @@ ...@@ -3,9 +3,13 @@
// found in the LICENSE file. // found in the LICENSE file.
#include <cmath> #include <cmath>
#include <functional>
#include <string>
#include "chrome/browser/client_hints/client_hints.h" #include "chrome/browser/client_hints/client_hints.h"
#include "base/memory/ptr_util.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h" #include "base/strings/string_number_conversions.h"
#include "build/build_config.h" #include "build/build_config.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h" #include "chrome/browser/content_settings/host_content_settings_map_factory.h"
...@@ -37,6 +41,35 @@ ...@@ -37,6 +41,35 @@
namespace { namespace {
uint8_t randomization_salt = 0;
constexpr size_t kMaxRandomNumbers = 21;
// Returns the randomization salt (weak and insecure) that should be used when
// adding noise to the network quality metrics. This is known only to the
// device, and is generated only once. This makes it possible to add the same
// amount of noise for a given origin.
uint8_t RandomizationSalt() {
if (randomization_salt == 0)
randomization_salt = base::RandInt(1, kMaxRandomNumbers);
DCHECK_LE(1, randomization_salt);
DCHECK_GE(kMaxRandomNumbers, randomization_salt);
return randomization_salt;
}
double GetRandomMultiplier(const std::string& host) {
// The random number should be a function of the hostname to reduce
// cross-origin fingerprinting. The random number should also be a function
// of randomized salt which is known only to the device. This prevents
// origin from removing noise from the estimates.
unsigned hash = std::hash<std::string>{}(host) + RandomizationSalt();
double random_multiplier =
0.9 + static_cast<double>((hash % kMaxRandomNumbers)) * 0.01;
DCHECK_LE(0.90, random_multiplier);
DCHECK_GE(1.10, random_multiplier);
return random_multiplier;
}
bool IsJavaScriptAllowed(Profile* profile, const GURL& url) { bool IsJavaScriptAllowed(Profile* profile, const GURL& url) {
return HostContentSettingsMapFactory::GetForProfile(profile) return HostContentSettingsMapFactory::GetForProfile(profile)
->GetContentSetting(url, url, CONTENT_SETTINGS_TYPE_JAVASCRIPT, ->GetContentSetting(url, url, CONTENT_SETTINGS_TYPE_JAVASCRIPT,
...@@ -78,6 +111,61 @@ double GetDeviceScaleFactor() { ...@@ -78,6 +111,61 @@ double GetDeviceScaleFactor() {
namespace client_hints { namespace client_hints {
namespace internal {
unsigned long RoundRtt(const std::string& host,
const base::Optional<base::TimeDelta>& rtt) {
// Limit the size of the buckets and the maximum reported value to reduce
// fingerprinting.
static const size_t kGranularityMsec = 50;
static const double kMaxRttMsec = 3.0 * 1000;
if (!rtt.has_value()) {
// RTT is unavailable. So, return the fastest value.
return 0;
}
double rtt_msec = static_cast<double>(rtt.value().InMilliseconds());
rtt_msec *= GetRandomMultiplier(host);
rtt_msec = std::min(rtt_msec, kMaxRttMsec);
DCHECK_LE(0, rtt_msec);
DCHECK_GE(kMaxRttMsec, rtt_msec);
// Round down to the nearest kBucketSize msec value.
return std::round(rtt_msec / kGranularityMsec) * kGranularityMsec;
}
double RoundMbps(const std::string& host,
const base::Optional<double>& downlink_mbps) {
// Limit the size of the buckets and the maximum reported value to reduce
// fingerprinting.
static const size_t kGranularityKbps = 50;
static const double kMaxDownlinkKbps = 10.0 * 1000;
double downlink_kbps = 0;
if (!downlink_mbps.has_value()) {
// Throughput is unavailable. So, return the fastest value.
downlink_kbps = kMaxDownlinkKbps;
} else {
downlink_kbps = downlink_mbps.value() * 1000;
}
downlink_kbps *= GetRandomMultiplier(host);
downlink_kbps = std::min(downlink_kbps, kMaxDownlinkKbps);
DCHECK_LE(0, downlink_kbps);
DCHECK_GE(kMaxDownlinkKbps, downlink_kbps);
// Round down to the nearest kGranularityKbps kbps value.
double downlink_kbps_rounded =
std::round(downlink_kbps / kGranularityKbps) * kGranularityKbps;
// Convert from Kbps to Mbps.
return downlink_kbps_rounded / 1000;
}
} // namespace internal
std::unique_ptr<net::HttpRequestHeaders> std::unique_ptr<net::HttpRequestHeaders>
GetAdditionalNavigationRequestClientHintsHeaders( GetAdditionalNavigationRequestClientHintsHeaders(
content::BrowserContext* context, content::BrowserContext* context,
...@@ -176,26 +264,20 @@ GetAdditionalNavigationRequestClientHintsHeaders( ...@@ -176,26 +264,20 @@ GetAdditionalNavigationRequestClientHintsHeaders(
UINetworkQualityEstimatorServiceFactory::GetForProfile( UINetworkQualityEstimatorServiceFactory::GetForProfile(
Profile::FromBrowserContext(context)); Profile::FromBrowserContext(context));
// TODO(crbug.com/826950): Add host specific noise and bucketization to RTT
// and downlink values.
if (web_client_hints.IsEnabled(blink::mojom::WebClientHintsType::kRtt)) { if (web_client_hints.IsEnabled(blink::mojom::WebClientHintsType::kRtt)) {
if (estimator->GetHttpRTT()) {
additional_headers->SetHeader( additional_headers->SetHeader(
blink::kClientHintsHeaderMapping[static_cast<int>( blink::kClientHintsHeaderMapping[static_cast<int>(
blink::mojom::WebClientHintsType::kRtt)], blink::mojom::WebClientHintsType::kRtt)],
base::NumberToString(estimator->GetHttpRTT()->InMilliseconds())); base::NumberToString(
} internal::RoundRtt(url.host(), estimator->GetHttpRTT())));
} }
if (web_client_hints.IsEnabled(blink::mojom::WebClientHintsType::kDownlink)) { if (web_client_hints.IsEnabled(blink::mojom::WebClientHintsType::kDownlink)) {
if (estimator->GetDownstreamThroughputKbps()) {
additional_headers->SetHeader( additional_headers->SetHeader(
blink::kClientHintsHeaderMapping[static_cast<int>( blink::kClientHintsHeaderMapping[static_cast<int>(
blink::mojom::WebClientHintsType::kDownlink)], blink::mojom::WebClientHintsType::kDownlink)],
base::NumberToString( base::NumberToString(internal::RoundMbps(
((double)estimator->GetDownstreamThroughputKbps().value()) / url.host(), estimator->GetDownstreamThroughputKbps())));
1024));
}
} }
if (web_client_hints.IsEnabled(blink::mojom::WebClientHintsType::kEct)) { if (web_client_hints.IsEnabled(blink::mojom::WebClientHintsType::kEct)) {
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
#include <memory> #include <memory>
#include "base/memory/ref_counted.h" #include "base/memory/ref_counted.h"
#include "base/optional.h"
class GURL; class GURL;
...@@ -26,6 +27,20 @@ class URLRequest; ...@@ -26,6 +27,20 @@ class URLRequest;
namespace client_hints { namespace client_hints {
namespace internal {
// Returns |rtt| after adding host-specific random noise, and rounding it as
// per the NetInfo spec to improve privacy.
unsigned long RoundRtt(const std::string& host,
const base::Optional<base::TimeDelta>& rtt);
// Returns |downlink_mbps| after adding host-specific random noise, and
// rounding it as per the NetInfo spec and to improve privacy.
double RoundMbps(const std::string& host,
const base::Optional<double>& downlink_mbps);
} // namespace internal
// Allow the embedder to return additional headers related to client hints that // Allow the embedder to return additional headers related to client hints that
// should be sent when fetching |url|. May return a nullptr. // should be sent when fetching |url|. May return a nullptr.
std::unique_ptr<net::HttpRequestHeaders> std::unique_ptr<net::HttpRequestHeaders>
......
...@@ -346,14 +346,22 @@ class ClientHintsBrowserTest : public InProcessBrowserTest { ...@@ -346,14 +346,22 @@ class ClientHintsBrowserTest : public InProcessBrowserTest {
const net::test_server::HttpRequest& request) const { const net::test_server::HttpRequest& request) const {
// Effective connection type is forced to 2G using command line in these // Effective connection type is forced to 2G using command line in these
// tests. // tests.
double value = 0.0; int rtt_value = 0.0;
EXPECT_TRUE(
base::StringToDouble(request.headers.find("rtt")->second, &value));
EXPECT_LE(0, value);
EXPECT_TRUE( EXPECT_TRUE(
base::StringToDouble(request.headers.find("downlink")->second, &value)); base::StringToInt(request.headers.find("rtt")->second, &rtt_value));
EXPECT_LE(0, value); EXPECT_LE(0, rtt_value);
// Verify that RTT value is a multiple of 50 milliseconds.
EXPECT_EQ(0, rtt_value % 50);
EXPECT_GE(3000, rtt_value);
double mbps_value = 0.0;
EXPECT_TRUE(base::StringToDouble(request.headers.find("downlink")->second,
&mbps_value));
EXPECT_LE(0, mbps_value);
// Verify that the mbps value is a multiple of 0.050 mbps.
// Allow for small amount of noise due to double to integer conversions.
EXPECT_NEAR(0, (static_cast<int>(mbps_value * 1000)) % 50, 1);
EXPECT_GE(10.0, mbps_value);
EXPECT_FALSE(request.headers.find("ect")->second.empty()); EXPECT_FALSE(request.headers.find("ect")->second.empty());
} }
......
// Copyright 2018 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/client_hints/client_hints.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "testing/gtest/include/gtest/gtest.h"
class ClientHintsTest : public testing::Test {
public:
ClientHintsTest() {}
~ClientHintsTest() override {}
private:
DISALLOW_COPY_AND_ASSIGN(ClientHintsTest);
};
TEST_F(ClientHintsTest, RttRoundedOff) {
EXPECT_EQ(0u, client_hints::internal::RoundRtt(
"", base::TimeDelta::FromMilliseconds(1023)) %
50);
EXPECT_EQ(0u, client_hints::internal::RoundRtt(
"", base::TimeDelta::FromMilliseconds(6787)) %
50);
EXPECT_EQ(0u, client_hints::internal::RoundRtt(
"", base::TimeDelta::FromMilliseconds(12)) %
50);
EXPECT_EQ(0u, client_hints::internal::RoundRtt(
"foo.com", base::TimeDelta::FromMilliseconds(1023)) %
50);
EXPECT_EQ(0u, client_hints::internal::RoundRtt(
"foo.com", base::TimeDelta::FromMilliseconds(1193)) %
50);
EXPECT_EQ(0u, client_hints::internal::RoundRtt(
"foo.com", base::TimeDelta::FromMilliseconds(12)) %
50);
}
TEST_F(ClientHintsTest, DownlinkRoundedOff) {
EXPECT_GE(1, static_cast<int>(client_hints::internal::RoundMbps("", 0.1023) *
1000) %
50);
EXPECT_GE(
1, static_cast<int>(client_hints::internal::RoundMbps("", 0.012) * 1000) %
50);
EXPECT_GE(1, static_cast<int>(client_hints::internal::RoundMbps("", 2.1023) *
1000) %
50);
EXPECT_GE(
1, static_cast<int>(client_hints::internal::RoundMbps("foo.com", 0.1023) *
1000) %
50);
EXPECT_GE(1, static_cast<int>(
client_hints::internal::RoundMbps("foo.com", 0.012) * 1000) %
50);
EXPECT_GE(
1, static_cast<int>(client_hints::internal::RoundMbps("foo.com", 2.1023) *
1000) %
50);
EXPECT_GE(1,
static_cast<int>(
client_hints::internal::RoundMbps("foo.com", 12.1023) * 1000) %
50);
}
// Verify that the value of RTT after adding noise is within approximately 10%
// of the original value. Note that the difference between the final value of
// RTT and the original value may be slightly more than 10% due to rounding off.
// To handle that, the maximum absolute difference allowed is set to a value
// slightly larger than 10% of the original metric value.
TEST_F(ClientHintsTest, FinalRttWithin10PercentValue) {
EXPECT_NEAR(98,
client_hints::internal::RoundRtt(
"", base::TimeDelta::FromMilliseconds(98)),
100);
EXPECT_NEAR(1023,
client_hints::internal::RoundRtt(
"", base::TimeDelta::FromMilliseconds(1023)),
200);
EXPECT_NEAR(1193,
client_hints::internal::RoundRtt(
"", base::TimeDelta::FromMilliseconds(1193)),
200);
EXPECT_NEAR(2750,
client_hints::internal::RoundRtt(
"", base::TimeDelta::FromMilliseconds(2750)),
400);
}
// Verify that the value of downlink after adding noise is within approximately
// 10% of the original value. Note that the difference between the final value
// of downlink and the original value may be slightly more than 10% due to
// rounding off. To handle that, the maximum absolute difference allowed is set
// to a value slightly larger than 10% of the original metric value.
TEST_F(ClientHintsTest, FinalDownlinkWithin10PercentValue) {
EXPECT_NEAR(0.098, client_hints::internal::RoundMbps("", 0.098), 0.1);
EXPECT_NEAR(1.023, client_hints::internal::RoundMbps("", 1.023), 0.2);
EXPECT_NEAR(1.193, client_hints::internal::RoundMbps("", 1.193), 0.2);
EXPECT_NEAR(7.523, client_hints::internal::RoundMbps("", 7.523), 0.9);
EXPECT_NEAR(9.999, client_hints::internal::RoundMbps("", 9.999), 1.2);
}
TEST_F(ClientHintsTest, RttMaxValue) {
EXPECT_GE(3000u, client_hints::internal::RoundRtt(
"", base::TimeDelta::FromMilliseconds(1023)));
EXPECT_GE(3000u, client_hints::internal::RoundRtt(
"", base::TimeDelta::FromMilliseconds(2789)));
EXPECT_GE(3000u, client_hints::internal::RoundRtt(
"", base::TimeDelta::FromMilliseconds(6023)));
EXPECT_EQ(0u, client_hints::internal::RoundRtt(
"", base::TimeDelta::FromMilliseconds(1023)) %
50);
EXPECT_EQ(0u, client_hints::internal::RoundRtt(
"", base::TimeDelta::FromMilliseconds(2789)) %
50);
EXPECT_EQ(0u, client_hints::internal::RoundRtt(
"", base::TimeDelta::FromMilliseconds(6023)) %
50);
}
TEST_F(ClientHintsTest, DownlinkMaxValue) {
EXPECT_GE(10.0, client_hints::internal::RoundMbps("", 0.1023));
EXPECT_GE(10.0, client_hints::internal::RoundMbps("", 2.1023));
EXPECT_GE(10.0, client_hints::internal::RoundMbps("", 100.1023));
EXPECT_GE(1, static_cast<int>(client_hints::internal::RoundMbps("", 0.1023) *
1000) %
50);
EXPECT_GE(1, static_cast<int>(client_hints::internal::RoundMbps("", 2.1023) *
1000) %
50);
EXPECT_GE(1, static_cast<int>(
client_hints::internal::RoundMbps("", 100.1023) * 1000) %
50);
}
TEST_F(ClientHintsTest, RttRandomized) {
const int initial_value = client_hints::internal::RoundRtt(
"example.com", base::TimeDelta::FromMilliseconds(1023));
bool network_quality_randomized_by_host = false;
// There is a 1/20 chance that the same random noise is selected for two
// different hosts. Run this test across 20 hosts to reduce the chances of
// test failing to (1/20)^20.
for (size_t i = 0; i < 20; ++i) {
int value = client_hints::internal::RoundRtt(
base::IntToString(i), base::TimeDelta::FromMilliseconds(1023));
// If |value| is different than |initial_value|, it implies that RTT is
// randomized by host. This verifies the behavior, and test can be ended.
if (value != initial_value)
network_quality_randomized_by_host = true;
}
EXPECT_TRUE(network_quality_randomized_by_host);
// Calling RoundRtt for same host should return the same result.
for (size_t i = 0; i < 20; ++i) {
int value = client_hints::internal::RoundRtt(
"example.com", base::TimeDelta::FromMilliseconds(1023));
EXPECT_EQ(initial_value, value);
}
}
TEST_F(ClientHintsTest, DownlinkRandomized) {
const int initial_value =
client_hints::internal::RoundMbps("example.com", 1.023);
bool network_quality_randomized_by_host = false;
// There is a 1/20 chance that the same random noise is selected for two
// different hosts. Run this test across 20 hosts to reduce the chances of
// test failing to (1/20)^20.
for (size_t i = 0; i < 20; ++i) {
int value = client_hints::internal::RoundMbps(base::IntToString(i), 1.023);
// If |value| is different than |initial_value|, it implies that downlink is
// randomized by host. This verifies the behavior, and test can be ended.
if (value != initial_value)
network_quality_randomized_by_host = true;
}
EXPECT_TRUE(network_quality_randomized_by_host);
// Calling RoundMbps for same host should return the same result.
for (size_t i = 0; i < 20; ++i) {
int value = client_hints::internal::RoundMbps("example.com", 1.023);
EXPECT_EQ(initial_value, value);
}
}
...@@ -2279,6 +2279,7 @@ test("unit_tests") { ...@@ -2279,6 +2279,7 @@ test("unit_tests") {
"../browser/budget_service/budget_manager_unittest.cc", "../browser/budget_service/budget_manager_unittest.cc",
"../browser/chrome_content_browser_client_unittest.cc", "../browser/chrome_content_browser_client_unittest.cc",
"../browser/chrome_process_singleton_win_unittest.cc", "../browser/chrome_process_singleton_win_unittest.cc",
"../browser/client_hints/client_hints_unittest.cc",
"../browser/command_updater_impl_unittest.cc", "../browser/command_updater_impl_unittest.cc",
"../browser/component_updater/chrome_component_updater_configurator_unittest.cc", "../browser/component_updater/chrome_component_updater_configurator_unittest.cc",
"../browser/component_updater/component_installer_errors_unittest.cc", "../browser/component_updater/component_installer_errors_unittest.cc",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment