Commit 641d348a authored by Vincent Boisselle's avatar Vincent Boisselle Committed by Commit Bot

Expose user demographics from SyncService.

Summary of changes:
  * Sync now handles all the logic of retrieving demographics, which includes deciding when and when not to provide demographics.
  * Added computation of the user age using the now time.
  * Added a check to not report demographics when user is not old enough.
  * Added age fuzzing.
  * Added more unittests for SyncPrefs::GetUserDemographics().
  * Added demographic checks to make sure that provided demographics have low entropy to protect privacy.
  * Added more explanations about privacy protection.

Bug: 983303
Change-Id: I4ef12059b0ad71a6dc656de0d59e8e9369ba6111
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1707109
Commit-Queue: Vincent Boisselle <vincb@google.com>
Reviewed-by: default avatarMarc Treib <treib@chromium.org>
Reviewed-by: default avatarAlexei Svitkine <asvitkine@chromium.org>
Cr-Commit-Position: refs/heads/master@{#680396}
parent 6556b2ce
...@@ -122,8 +122,14 @@ const char kEnableLocalSyncBackend[] = "sync.enable_local_sync_backend"; ...@@ -122,8 +122,14 @@ const char kEnableLocalSyncBackend[] = "sync.enable_local_sync_backend";
const char kLocalSyncBackendDir[] = "sync.local_sync_backend_dir"; const char kLocalSyncBackendDir[] = "sync.local_sync_backend_dir";
// Stores birth year of the syncing user that is provided by the sync server. // Stores birth year of the syncing user that is provided by the sync server.
// Should not be logged to UMA directly.
const char kSyncDemographicsBirthYear[] = "sync.demographics.birth_year"; const char kSyncDemographicsBirthYear[] = "sync.demographics.birth_year";
// Stores offset that is used to randomize the birth year for metrics reporting.
// Should not be logged to UMA directly.
const char kSyncDemographicsBirthYearNoiseOffset[] =
"sync.demographics.birth_year_offset";
// Stores the encoded gender of the syncing user that is provided by the sync // Stores the encoded gender of the syncing user that is provided by the sync
// server. The gender is encoded using the Gender enum defined in // server. The gender is encoded using the Gender enum defined in
// metrics::UserDemographicsProto // metrics::UserDemographicsProto
......
...@@ -81,6 +81,7 @@ extern const char kEnableLocalSyncBackend[]; ...@@ -81,6 +81,7 @@ extern const char kEnableLocalSyncBackend[];
extern const char kLocalSyncBackendDir[]; extern const char kLocalSyncBackendDir[];
extern const char kSyncDemographicsBirthYear[]; extern const char kSyncDemographicsBirthYear[];
extern const char kSyncDemographicsBirthYearNoiseOffset[];
extern const char kSyncDemographicsGender[]; extern const char kSyncDemographicsGender[];
} // namespace prefs } // namespace prefs
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
#include "base/files/file_path.h" #include "base/files/file_path.h"
#include "base/logging.h" #include "base/logging.h"
#include "base/path_service.h" #include "base/path_service.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h" #include "base/strings/string_number_conversions.h"
#include "base/values.h" #include "base/values.h"
#include "components/pref_registry/pref_registry_syncable.h" #include "components/pref_registry/pref_registry_syncable.h"
...@@ -47,9 +48,6 @@ const char kSyncSpareBootstrapToken[] = "sync.spare_bootstrap_token"; ...@@ -47,9 +48,6 @@ const char kSyncSpareBootstrapToken[] = "sync.spare_bootstrap_token";
// kSyncRequested. // kSyncRequested.
const char kSyncSuppressStart[] = "sync.suppress_start"; const char kSyncSuppressStart[] = "sync.suppress_start";
// Default value of a demographic pref when it does not exist.
constexpr int kUserDemographicPrefDefaultValue = -1;
std::vector<std::string> GetObsoleteUserTypePrefs() { std::vector<std::string> GetObsoleteUserTypePrefs() {
return {prefs::kSyncAutofillProfile, return {prefs::kSyncAutofillProfile,
prefs::kSyncAutofillWallet, prefs::kSyncAutofillWallet,
...@@ -118,12 +116,58 @@ const char* GetPrefNameForType(UserSelectableType type) { ...@@ -118,12 +116,58 @@ const char* GetPrefNameForType(UserSelectableType type) {
return nullptr; return nullptr;
} }
// Gets an offset to add noise to the birth year. If not present in prefs, the
// offset will be randomly generated within the offset range and cached in
// prefs.
int GetBirthYearOffset(PrefService* pref_service) {
int offset =
pref_service->GetInteger(prefs::kSyncDemographicsBirthYearNoiseOffset);
if (offset == kUserDemographicsBirthYearNoiseOffsetDefaultValue) {
// Generate a random offset when not cached in prefs.
offset = base::RandInt(-kUserDemographicsBirthYearNoiseOffsetRange,
kUserDemographicsBirthYearNoiseOffsetRange);
pref_service->SetInteger(prefs::kSyncDemographicsBirthYearNoiseOffset,
offset);
}
return offset;
}
// Determines whether the user can provide birth year considering that: (1) it
// is not possible to infer the month and the day of the birth date when the
// user is in an age transition, and (2) only users of at least 18 years old can
// report demographics.
bool CanUserProvideBirthYear(base::Time now, int user_birth_year) {
base::Time::Exploded exploded_now_time;
now.LocalExplode(&exploded_now_time);
// Use > rather than >= because we want to be sure that the user is at
// least |kUserDemographicsMinAgeInYears| without disclosing their birth date,
// which requires to add an extra year margin to minimal age to be safe. For
// example, if we are in 2019-07-10 (now) and the user was born in 1999-08-10,
// the user is not yet 20 years old (minimal age) but we cannot know that
// because we only have access to the year of the dates (2019 and 1999
// respectively). If we make sure that the minimal age is at least 21, we are
// 100% sure that the user will be at least 20 years old when reporting
// demographics.
return exploded_now_time.year - user_birth_year >
kUserDemographicsMinAgeInYears;
}
// Gets user's birth year from prefs. // Gets user's birth year from prefs.
base::Optional<int> GetUserBirthYear(const PrefService& pref_service) { base::Optional<int> GetUserBirthYear(PrefService* pref_service,
int birth_year = pref_service.GetInteger(prefs::kSyncDemographicsBirthYear); base::Time now) {
int birth_year = pref_service->GetInteger(prefs::kSyncDemographicsBirthYear);
// Verify that there is a birth year. // Verify that there is a birth year.
if (birth_year == kUserDemographicPrefDefaultValue) if (birth_year == kUserDemographicsBirthYearDefaultValue)
return base::nullopt;
// Add noise to birth year.
birth_year += GetBirthYearOffset(pref_service);
DCHECK(!now.is_null());
// Verify that the user is old enough to provide demographics.
if (!CanUserProvideBirthYear(now, birth_year))
return base::nullopt; return base::nullopt;
return birth_year; return birth_year;
...@@ -135,14 +179,24 @@ base::Optional<metrics::UserDemographicsProto_Gender> GetUserGender( ...@@ -135,14 +179,24 @@ base::Optional<metrics::UserDemographicsProto_Gender> GetUserGender(
int gender_int = pref_service.GetInteger(prefs::kSyncDemographicsGender); int gender_int = pref_service.GetInteger(prefs::kSyncDemographicsGender);
// Verify gender is not default. // Verify gender is not default.
if (gender_int == kUserDemographicPrefDefaultValue) if (gender_int == kUserDemographicsGenderDefaultValue)
return base::nullopt; return base::nullopt;
// Verify the gender number is a valid UserDemographicsProto_Gender encoding. // Verify the gender number is a valid UserDemographicsProto_Gender encoding.
if (!metrics::UserDemographicsProto_Gender_IsValid(gender_int)) if (!metrics::UserDemographicsProto_Gender_IsValid(gender_int))
return base::nullopt; return base::nullopt;
return metrics::UserDemographicsProto_Gender(gender_int); auto gender = metrics::UserDemographicsProto_Gender(gender_int);
// Verify that the gender is in the set of genders that have large populations
// of a similar size (i.e., male and female) to preserve anonymity of genders
// with smaller populations.
if (gender != metrics::UserDemographicsProto::GENDER_FEMALE &&
gender != metrics::UserDemographicsProto::GENDER_MALE) {
return base::nullopt;
}
return gender;
} }
} // namespace } // namespace
...@@ -211,10 +265,13 @@ void SyncPrefs::RegisterProfilePrefs( ...@@ -211,10 +265,13 @@ void SyncPrefs::RegisterProfilePrefs(
// Demographic prefs. // Demographic prefs.
registry->RegisterIntegerPref( registry->RegisterIntegerPref(
prefs::kSyncDemographicsBirthYear, kUserDemographicPrefDefaultValue, prefs::kSyncDemographicsBirthYear, kUserDemographicsBirthYearDefaultValue,
user_prefs::PrefRegistrySyncable::SYNCABLE_PRIORITY_PREF); user_prefs::PrefRegistrySyncable::SYNCABLE_PRIORITY_PREF);
registry->RegisterIntegerPref( registry->RegisterIntegerPref(
prefs::kSyncDemographicsGender, kUserDemographicPrefDefaultValue, prefs::kSyncDemographicsBirthYearNoiseOffset,
kUserDemographicsBirthYearNoiseOffsetDefaultValue);
registry->RegisterIntegerPref(
prefs::kSyncDemographicsGender, kUserDemographicsGenderDefaultValue,
user_prefs::PrefRegistrySyncable::SYNCABLE_PRIORITY_PREF); user_prefs::PrefRegistrySyncable::SYNCABLE_PRIORITY_PREF);
// Obsolete prefs that will be removed after a grace period. // Obsolete prefs that will be removed after a grace period.
...@@ -247,6 +304,7 @@ void SyncPrefs::ClearPreferences() { ...@@ -247,6 +304,7 @@ void SyncPrefs::ClearPreferences() {
// Clear user demographics. // Clear user demographics.
pref_service_->ClearPref(prefs::kSyncDemographicsBirthYear); pref_service_->ClearPref(prefs::kSyncDemographicsBirthYear);
pref_service_->ClearPref(prefs::kSyncDemographicsBirthYearNoiseOffset);
pref_service_->ClearPref(prefs::kSyncDemographicsGender); pref_service_->ClearPref(prefs::kSyncDemographicsGender);
ClearDirectoryConsistencyPreferences(); ClearDirectoryConsistencyPreferences();
...@@ -544,20 +602,25 @@ bool SyncPrefs::IsLocalSyncEnabled() const { ...@@ -544,20 +602,25 @@ bool SyncPrefs::IsLocalSyncEnabled() const {
return local_sync_enabled_; return local_sync_enabled_;
} }
base::Optional<UserDemographics> SyncPrefs::GetUserDemographics() { base::Optional<UserDemographics> SyncPrefs::GetUserDemographics(
UserDemographics user_demographics; base::Time now) {
// Verify that the now time is available. There are situations where the now
// time cannot be provided.
if (now.is_null())
return base::nullopt;
// Get and set birth year. // Get birth year and gender.
base::Optional<int> birth_year = GetUserBirthYear(*pref_service_); base::Optional<int> birth_year = GetUserBirthYear(pref_service_, now);
if (!birth_year.has_value()) if (!birth_year.has_value())
return base::nullopt; return base::nullopt;
user_demographics.birth_year = *birth_year;
// Get and set gender.
base::Optional<metrics::UserDemographicsProto_Gender> gender = base::Optional<metrics::UserDemographicsProto_Gender> gender =
GetUserGender(*pref_service_); GetUserGender(*pref_service_);
if (!gender.value()) if (!gender.has_value())
return base::nullopt; return base::nullopt;
// Set birth year and gender in demographics.
UserDemographics user_demographics;
user_demographics.birth_year = *birth_year;
user_demographics.gender = *gender; user_demographics.gender = *gender;
return user_demographics; return user_demographics;
......
...@@ -185,10 +185,8 @@ class SyncPrefs : public CryptoSyncPrefs, ...@@ -185,10 +185,8 @@ class SyncPrefs : public CryptoSyncPrefs,
bool IsLocalSyncEnabled() const; bool IsLocalSyncEnabled() const;
// Gets user demographics. Returns an empty optional if not all demographics // Gets user demographics. Returns an empty optional if not all demographics
// are available. Birth year and gender are mutually inclusive to avoid being // are available.
// able to infer demographic information that has low entropy (e.g., custom base::Optional<UserDemographics> GetUserDemographics(base::Time now);
// gender).
base::Optional<UserDemographics> GetUserDemographics();
private: private:
static void RegisterTypeSelectedPref(user_prefs::PrefRegistrySyncable* prefs, static void RegisterTypeSelectedPref(user_prefs::PrefRegistrySyncable* prefs,
......
This diff is collapsed.
...@@ -9,6 +9,30 @@ ...@@ -9,6 +9,30 @@
namespace syncer { namespace syncer {
// Default value for user gender when no value has been set.
constexpr int kUserDemographicsGenderDefaultValue = -1;
// Default value for user gender enum when no value has been set.
constexpr metrics::UserDemographicsProto_Gender
kUserDemographicGenderDefaultEnumValue =
metrics::UserDemographicsProto_Gender_Gender_MIN;
// Default value for user gender when no value has been set.
constexpr int kUserDemographicsBirthYearDefaultValue = -1;
// Default value for birth year offset when no value has been set. Set to a
// really high value that |kUserDemographicsBirthYearNoiseOffsetRange| will
// never go over.
constexpr int kUserDemographicsBirthYearNoiseOffsetDefaultValue = 100;
// Boundary of the +/- range in years within witch to randomly pick an offset to
// add to the user birth year. This adds noise to the birth year to not allow
// someone to accurately know a user's age. Possible offsets range from -2 to 2.
constexpr int kUserDemographicsBirthYearNoiseOffsetRange = 2;
// Minimal user age in years to provide demographics for.
constexpr int kUserDemographicsMinAgeInYears = 20;
// Container of user demographics. // Container of user demographics.
struct UserDemographics { struct UserDemographics {
int birth_year = 0; int birth_year = 0;
......
...@@ -156,6 +156,11 @@ void FakeSyncService::GetAllNodesForDebugging( ...@@ -156,6 +156,11 @@ void FakeSyncService::GetAllNodesForDebugging(
void FakeSyncService::SetInvalidationsForSessionsEnabled(bool enabled) {} void FakeSyncService::SetInvalidationsForSessionsEnabled(bool enabled) {}
base::Optional<UserDemographics> FakeSyncService::GetUserDemographics(
base::Time now) {
return base::nullopt;
}
void FakeSyncService::Shutdown() {} void FakeSyncService::Shutdown() {}
} // namespace syncer } // namespace syncer
...@@ -65,6 +65,7 @@ class FakeSyncService : public SyncService { ...@@ -65,6 +65,7 @@ class FakeSyncService : public SyncService {
const base::Callback<void(std::unique_ptr<base::ListValue>)>& callback) const base::Callback<void(std::unique_ptr<base::ListValue>)>& callback)
override; override;
void SetInvalidationsForSessionsEnabled(bool enabled) override; void SetInvalidationsForSessionsEnabled(bool enabled) override;
base::Optional<UserDemographics> GetUserDemographics(base::Time now) override;
// KeyedService implementation. // KeyedService implementation.
void Shutdown() override; void Shutdown() override;
......
...@@ -54,6 +54,8 @@ class MockSyncService : public SyncService { ...@@ -54,6 +54,8 @@ class MockSyncService : public SyncService {
MOCK_METHOD1(TriggerRefresh, void(const ModelTypeSet& types)); MOCK_METHOD1(TriggerRefresh, void(const ModelTypeSet& types));
MOCK_METHOD1(ReadyForStartChanged, void(syncer::ModelType type)); MOCK_METHOD1(ReadyForStartChanged, void(syncer::ModelType type));
MOCK_METHOD1(SetInvalidationsForSessionsEnabled, void(bool enabled)); MOCK_METHOD1(SetInvalidationsForSessionsEnabled, void(bool enabled));
MOCK_METHOD1(GetUserDemographics,
base::Optional<UserDemographics>(base::Time now));
MOCK_METHOD1(AddObserver, void(SyncServiceObserver* observer)); MOCK_METHOD1(AddObserver, void(SyncServiceObserver* observer));
MOCK_METHOD1(RemoveObserver, void(SyncServiceObserver* observer)); MOCK_METHOD1(RemoveObserver, void(SyncServiceObserver* observer));
......
...@@ -1679,6 +1679,16 @@ void ProfileSyncService::SetInvalidationsForSessionsEnabled(bool enabled) { ...@@ -1679,6 +1679,16 @@ void ProfileSyncService::SetInvalidationsForSessionsEnabled(bool enabled) {
} }
} }
base::Optional<UserDemographics> ProfileSyncService::GetUserDemographics(
base::Time now) {
// Do not provide demographics when sync is disabled because user demographics
// should only be provided when the other sync data is also provided.
if (!IsSyncFeatureEnabled())
return base::nullopt;
return sync_prefs_.GetUserDemographics(now);
}
base::WeakPtr<JsController> ProfileSyncService::GetJsController() { base::WeakPtr<JsController> ProfileSyncService::GetJsController() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return sync_js_controller_.AsWeakPtr(); return sync_js_controller_.AsWeakPtr();
......
...@@ -135,6 +135,7 @@ class ProfileSyncService : public SyncService, ...@@ -135,6 +135,7 @@ class ProfileSyncService : public SyncService,
void TriggerRefresh(const ModelTypeSet& types) override; void TriggerRefresh(const ModelTypeSet& types) override;
void ReadyForStartChanged(ModelType type) override; void ReadyForStartChanged(ModelType type) override;
void SetInvalidationsForSessionsEnabled(bool enabled) override; void SetInvalidationsForSessionsEnabled(bool enabled) override;
base::Optional<UserDemographics> GetUserDemographics(base::Time now) override;
void AddObserver(SyncServiceObserver* observer) override; void AddObserver(SyncServiceObserver* observer) override;
void RemoveObserver(SyncServiceObserver* observer) override; void RemoveObserver(SyncServiceObserver* observer) override;
bool HasObserver(const SyncServiceObserver* observer) const override; bool HasObserver(const SyncServiceObserver* observer) const override;
......
...@@ -12,9 +12,11 @@ ...@@ -12,9 +12,11 @@
#include "base/location.h" #include "base/location.h"
#include "base/macros.h" #include "base/macros.h"
#include "base/memory/weak_ptr.h" #include "base/memory/weak_ptr.h"
#include "base/optional.h"
#include "base/time/time.h" #include "base/time/time.h"
#include "components/keyed_service/core/keyed_service.h" #include "components/keyed_service/core/keyed_service.h"
#include "components/sync/base/model_type.h" #include "components/sync/base/model_type.h"
#include "components/sync/base/user_demographics.h"
#include "components/sync/driver/sync_service_observer.h" #include "components/sync/driver/sync_service_observer.h"
struct CoreAccountInfo; struct CoreAccountInfo;
...@@ -345,6 +347,16 @@ class SyncService : public KeyedService { ...@@ -345,6 +347,16 @@ class SyncService : public KeyedService {
// page is opened. // page is opened.
virtual void SetInvalidationsForSessionsEnabled(bool enabled) = 0; virtual void SetInvalidationsForSessionsEnabled(bool enabled) = 0;
//////////////////////////////////////////////////////////////////////////////
// USER DEMOGRAPHICS
//////////////////////////////////////////////////////////////////////////////
// Gets user synced demographics. See documentation of function
// SyncPrefs::GetUserDemographicss for more details. You need to provide an
// accurate |now| time that represents the user's current time.
virtual base::Optional<UserDemographics> GetUserDemographics(
base::Time now) = 0;
////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////
// OBSERVERS // OBSERVERS
////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////
......
...@@ -87,6 +87,11 @@ void TestSyncService::SetLastCycleSnapshot(const SyncCycleSnapshot& snapshot) { ...@@ -87,6 +87,11 @@ void TestSyncService::SetLastCycleSnapshot(const SyncCycleSnapshot& snapshot) {
last_cycle_snapshot_ = snapshot; last_cycle_snapshot_ = snapshot;
} }
void TestSyncService::SetUserDemographics(
const base::Optional<UserDemographics>& demographics) {
user_demographics_ = demographics;
}
void TestSyncService::SetEmptyLastCycleSnapshot() { void TestSyncService::SetEmptyLastCycleSnapshot() {
SetLastCycleSnapshot(SyncCycleSnapshot()); SetLastCycleSnapshot(SyncCycleSnapshot());
} }
...@@ -268,6 +273,11 @@ void TestSyncService::GetAllNodesForDebugging( ...@@ -268,6 +273,11 @@ void TestSyncService::GetAllNodesForDebugging(
void TestSyncService::SetInvalidationsForSessionsEnabled(bool enabled) {} void TestSyncService::SetInvalidationsForSessionsEnabled(bool enabled) {}
base::Optional<UserDemographics> TestSyncService::GetUserDemographics(
base::Time now) {
return user_demographics_;
}
void TestSyncService::Shutdown() {} void TestSyncService::Shutdown() {}
} // namespace syncer } // namespace syncer
...@@ -38,6 +38,9 @@ class TestSyncService : public SyncService { ...@@ -38,6 +38,9 @@ class TestSyncService : public SyncService {
void SetPreferredDataTypes(const ModelTypeSet& types); void SetPreferredDataTypes(const ModelTypeSet& types);
void SetActiveDataTypes(const ModelTypeSet& types); void SetActiveDataTypes(const ModelTypeSet& types);
void SetLastCycleSnapshot(const SyncCycleSnapshot& snapshot); void SetLastCycleSnapshot(const SyncCycleSnapshot& snapshot);
void SetUserDemographics(
const base::Optional<UserDemographics>& demographics);
// Convenience versions of the above, for when the caller doesn't care about // Convenience versions of the above, for when the caller doesn't care about
// the particular values in the snapshot, just whether there is one. // the particular values in the snapshot, just whether there is one.
void SetEmptyLastCycleSnapshot(); void SetEmptyLastCycleSnapshot();
...@@ -97,6 +100,7 @@ class TestSyncService : public SyncService { ...@@ -97,6 +100,7 @@ class TestSyncService : public SyncService {
const base::Callback<void(std::unique_ptr<base::ListValue>)>& callback) const base::Callback<void(std::unique_ptr<base::ListValue>)>& callback)
override; override;
void SetInvalidationsForSessionsEnabled(bool enabled) override; void SetInvalidationsForSessionsEnabled(bool enabled) override;
base::Optional<UserDemographics> GetUserDemographics(base::Time now) override;
// KeyedService implementation. // KeyedService implementation.
void Shutdown() override; void Shutdown() override;
...@@ -124,6 +128,8 @@ class TestSyncService : public SyncService { ...@@ -124,6 +128,8 @@ class TestSyncService : public SyncService {
GURL sync_service_url_; GURL sync_service_url_;
base::Optional<UserDemographics> user_demographics_;
DISALLOW_COPY_AND_ASSIGN(TestSyncService); DISALLOW_COPY_AND_ASSIGN(TestSyncService);
}; };
......
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