// Copyright 2019 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/ash/cryptauth/client_app_metadata_provider_service.h"
#include <string>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "base/feature_list.h"
#include "base/functional/callback.h"
#include "base/linux_util.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/no_destructor.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "base/version.h"
#include "chrome/browser/ash/cryptauth/cryptauth_device_id_provider_impl.h"
#include "chrome/browser/chrome_content_browser_client.h"
#include "chrome/common/pref_names.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/components/network/network_type_pattern.h"
#include "chromeos/ash/services/device_sync/proto/cryptauth_better_together_feature_metadata.pb.h"
#include "chromeos/ash/services/device_sync/public/cpp/gcm_constants.h"
#include "components/gcm_driver/instance_id/instance_id_driver.h"
#include "components/gcm_driver/instance_id/instance_id_profile_service.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/version_info/version_info.h"
#include "device/bluetooth/bluetooth_adapter.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
namespace ash {
namespace {
const char kInstanceIdScope[] = "GCM";
const char kDefaultModelName[] = "Chromebook";
const cryptauthv2::FeatureMetadata& GenerateFeatureMetadata() {
static const base::NoDestructor<cryptauthv2::FeatureMetadata>
feature_metadata([] {
cryptauthv2::BetterTogetherFeatureMetadata inner_metadata;
// Smart Lock and MultiDevice Setup are supported on all Chromebooks.
inner_metadata.add_supported_features(
cryptauthv2::
BetterTogetherFeatureMetadata_FeatureName_EASY_UNLOCK_CLIENT);
inner_metadata.add_supported_features(
cryptauthv2::
BetterTogetherFeatureMetadata_FeatureName_BETTER_TOGETHER_CLIENT);
// Instant Tethering is only supported if the associated flag enabled.
if (base::FeatureList::IsEnabled(features::kInstantTethering)) {
inner_metadata.add_supported_features(
cryptauthv2::
BetterTogetherFeatureMetadata_FeatureName_MAGIC_TETHER_CLIENT);
}
// Phone Hub is only supported if the associated flag is enabled.
if (features::IsPhoneHubEnabled()) {
inner_metadata.add_supported_features(
cryptauthv2::
BetterTogetherFeatureMetadata_FeatureName_PHONE_HUB_CLIENT);
}
// Wifi Sync Android is only supported if the associated flag is
// enabled.
if (features::IsWifiSyncAndroidEnabled()) {
inner_metadata.add_supported_features(
cryptauthv2::
BetterTogetherFeatureMetadata_FeatureName_WIFI_SYNC_CLIENT);
}
// Eche is only supported if the associated flag is enabled.
if (features::IsEcheSWAEnabled()) {
inner_metadata.add_supported_features(
cryptauthv2::
BetterTogetherFeatureMetadata_FeatureName_ECHE_CLIENT);
}
// Camera Roll is only supported if the associated flag is enabled.
if (features::IsPhoneHubCameraRollEnabled()) {
inner_metadata.add_supported_features(
cryptauthv2::
BetterTogetherFeatureMetadata_FeatureName_PHONE_HUB_CAMERA_ROLL_CLIENT);
}
// Note: |inner_metadata|'s enabled_features field is deprecated and
// left unset here (the server ignores this value when processing the
// received proto).
cryptauthv2::FeatureMetadata feature_metadata;
feature_metadata.set_feature_type(
cryptauthv2::FeatureMetadata_Feature_BETTER_TOGETHER);
feature_metadata.set_metadata(inner_metadata.SerializeAsString());
return feature_metadata;
}());
return *feature_metadata;
}
cryptauthv2::ApplicationSpecificMetadata GenerateApplicationSpecificMetadata(
const std::string& gcm_registration_id,
int64_t software_version_code) {
cryptauthv2::ApplicationSpecificMetadata metadata;
metadata.set_gcm_registration_id(gcm_registration_id);
// Chrome OS system notifications are always enabled.
metadata.set_notification_enabled(true);
metadata.set_device_software_version(base::GetLinuxDistro());
metadata.set_device_software_version_code(software_version_code);
metadata.set_device_software_package(device_sync::kCryptAuthGcmAppId);
return metadata;
}
void LogInstanceIdTokenFetchRetries(int count) {
base::UmaHistogramExactLinear(
"CryptAuth.ClientAppMetadataInstanceIdTokenFetch.Retries", count, 2);
}
} // namespace
// static
void ClientAppMetadataProviderService::RegisterProfilePrefs(
PrefRegistrySimple* registry) {
registry->RegisterStringPref(::prefs::kCryptAuthInstanceId, std::string());
registry->RegisterStringPref(::prefs::kCryptAuthInstanceIdToken,
std::string());
}
// static
int64_t ClientAppMetadataProviderService::ConvertVersionCodeToInt64(
const std::string& version_code_str) {
static const size_t kNumDigitsInLastSection = 3;
std::string version_code_copy = version_code_str;
size_t last_period_index = version_code_copy.rfind('.');
if (last_period_index != std::string::npos) {
// If there are fewer than |kNumDigitsInLastSection| digits in the last
// section, add extra '0' characters.
size_t num_digits_to_add = kNumDigitsInLastSection + last_period_index -
(version_code_copy.size() - 1u);
version_code_copy.insert(last_period_index + 1u /* pos */,
std::string(num_digits_to_add, '0') /* str */);
}
int64_t code = 0;
for (auto it = version_code_copy.cbegin(); it != version_code_copy.cend();
++it) {
if (*it < '0' || *it > '9')
continue;
code = code * 10 + (*it - '0');
}
return code;
}
ClientAppMetadataProviderService::ClientAppMetadataProviderService(
PrefService* pref_service,
NetworkStateHandler* network_state_handler,
instance_id::InstanceIDProfileService* instance_id_profile_service)
: pref_service_(pref_service),
network_state_handler_(network_state_handler),
instance_id_profile_service_(instance_id_profile_service) {}
ClientAppMetadataProviderService::~ClientAppMetadataProviderService() {
// If there are any pending callbacks, invoke them before this object is
// deleted.
InvokePendingCallbacks();
}
void ClientAppMetadataProviderService::GetClientAppMetadata(
const std::string& gcm_registration_id,
GetMetadataCallback callback) {
pending_callbacks_.push_back(std::move(callback));
// If the metadata has already been computed, provide it to the callback
// immediately.
if (client_app_metadata_) {
// If |gcm_registration_id| is different from a previously-supplied ID,
// replace the ID with this new one.
DCHECK_EQ(1, client_app_metadata_->application_specific_metadata_size());
client_app_metadata_->mutable_application_specific_metadata(0 /* index */)
->set_gcm_registration_id(gcm_registration_id);
InvokePendingCallbacks();
return;
}
// If |instance_id_profile_service_| is null, Shutdown() has been called and
// there should be no further attempt to calculate the ClientAppMetadata,
// since this could result in touching deleted memory.
if (!instance_id_profile_service_) {
InvokePendingCallbacks();
return;
}
bool was_already_in_progress = pending_gcm_registration_id_.has_value();
pending_gcm_registration_id_ = gcm_registration_id;
// If metadata is currently being computed, update
// |pending_gcm_registration_id_| and wait for the ongoing attempt to complete
// before continuing.
if (was_already_in_progress)
return;
device::BluetoothAdapterFactory::Get()->GetAdapter(base::BindOnce(
&ClientAppMetadataProviderService::OnBluetoothAdapterFetched,
weak_ptr_factory_.GetWeakPtr()));
}
void ClientAppMetadataProviderService::Shutdown() {
// Null out |instance_id_profile_service_| to signify that it should no longer
// be used.
instance_id_profile_service_ = nullptr;
// If the ClientAppMetadata is currently being computed and this class is
// waiting for an asynchronous operation to return, stop the computation now
// to ensure that deleted memory is not touched.
weak_ptr_factory_.InvalidateWeakPtrs();
pending_gcm_registration_id_.reset();
InvokePendingCallbacks();
}
void ClientAppMetadataProviderService::OnBluetoothAdapterFetched(
scoped_refptr<device::BluetoothAdapter> bluetooth_adapter) {
base::SysInfo::GetHardwareInfo(
base::BindOnce(&ClientAppMetadataProviderService::OnHardwareInfoFetched,
weak_ptr_factory_.GetWeakPtr(), bluetooth_adapter));
}
void ClientAppMetadataProviderService::OnHardwareInfoFetched(
scoped_refptr<device::BluetoothAdapter> bluetooth_adapter,
base::SysInfo::HardwareInfo hardware_info) {
GetInstanceId()->GetID(base::BindOnce(
&ClientAppMetadataProviderService::OnInstanceIdFetched,
weak_ptr_factory_.GetWeakPtr(), bluetooth_adapter, hardware_info));
}
void ClientAppMetadataProviderService::OnInstanceIdFetched(
scoped_refptr<device::BluetoothAdapter> bluetooth_adapter,
const base::SysInfo::HardwareInfo& hardware_info,
const std::string& instance_id) {
DCHECK(!instance_id.empty());
std::string previous_instance_id =
pref_service_->GetString(::prefs::kCryptAuthInstanceId);
if (!previous_instance_id.empty()) {
base::UmaHistogramBoolean("CryptAuth.InstanceId.DidInstanceIdChange",
previous_instance_id != instance_id);
}
pref_service_->SetString(::prefs::kCryptAuthInstanceId, instance_id);
GetInstanceId()->GetToken(
device_sync::
kCryptAuthV2EnrollmentAuthorizedEntity /* authorized_entity */,
kInstanceIdScope /* scope */, base::TimeDelta() /* time_to_live */,
{} /* flags */,
base::BindOnce(
&ClientAppMetadataProviderService::OnInstanceIdTokenFetched,
weak_ptr_factory_.GetWeakPtr(), bluetooth_adapter, hardware_info,
instance_id));
}
void ClientAppMetadataProviderService::OnInstanceIdTokenFetched(
scoped_refptr<device::BluetoothAdapter> bluetooth_adapter,
const base::SysInfo::HardwareInfo& hardware_info,
const std::string& instance_id,
const std::string& token,
instance_id::InstanceID::Result result) {
// If the |token| doesn't begin with the |instance_id|, we have to re-create
// the entire InstanceID and remove the old one from storage.
if (token.find(':') != std::string::npos &&
!base::StartsWith(token, instance_id,
base::CompareCase::INSENSITIVE_ASCII)) {
GetInstanceId()->DeleteID(base::BindOnce(
&ClientAppMetadataProviderService::OnInstanceIdDeleted,
weak_ptr_factory_.GetWeakPtr(), bluetooth_adapter, hardware_info));
return;
}
LogInstanceIdTokenFetchRetries(instance_id_recreated_ ? 1 : 0);
std::string gcm_registration_id = *pending_gcm_registration_id_;
pending_gcm_registration_id_.reset();
instance_id_recreated_ = false;
UMA_HISTOGRAM_ENUMERATION(
"CryptAuth.ClientAppMetadataInstanceIdTokenFetch.Result", result);
// If fetching the token failed, invoke the pending callbacks with a null
// ClientAppMetadata.
if (result != instance_id::InstanceID::Result::SUCCESS) {
PA_LOG(WARNING) << "ClientAppMetadataProviderService::"
<< "OnInstanceIdTokenFetched(): Failed to fetch InstanceID "
<< "token; result: " << result << ".";
InvokePendingCallbacks();
return;
}
DCHECK(!token.empty());
std::string previous_instance_id_token =
pref_service_->GetString(::prefs::kCryptAuthInstanceIdToken);
if (!previous_instance_id_token.empty()) {
base::UmaHistogramBoolean("CryptAuth.InstanceId.DidInstanceIdTokenChange",
previous_instance_id_token != token);
}
pref_service_->SetString(::prefs::kCryptAuthInstanceIdToken, token);
cryptauthv2::ClientAppMetadata metadata;
metadata.add_application_specific_metadata()->CopyFrom(
GenerateApplicationSpecificMetadata(gcm_registration_id,
SoftwareVersionCodeAsInt64()));
metadata.set_instance_id(instance_id);
metadata.set_instance_id_token(token);
metadata.set_long_device_id(
CryptAuthDeviceIdProviderImpl::GetInstance()->GetDeviceId());
metadata.set_locale(ChromeContentBrowserClient().GetApplicationLocale());
metadata.set_device_os_version(base::GetLinuxDistro());
metadata.set_device_os_version_code(SoftwareVersionCodeAsInt64());
metadata.set_device_os_release(std::string(version_info::GetVersionNumber()));
metadata.set_device_os_codename(std::string(version_info::GetProductName()));
// device_display_diagonal_mils is unused because it only applies to
// phones/tablets.
metadata.set_device_display_diagonal_mils(0);
base::UmaHistogramBoolean("CryptAuth.ClientAppMetadata.IsModelEmpty",
hardware_info.model.empty());
metadata.set_device_model(hardware_info.model.empty() ? kDefaultModelName
: hardware_info.model);
base::UmaHistogramBoolean("CryptAuth.ClientAppMetadata.IsManufacturerEmpty",
hardware_info.manufacturer.empty());
metadata.set_device_manufacturer(hardware_info.manufacturer);
metadata.set_device_type(cryptauthv2::ClientAppMetadata_DeviceType_CHROME);
metadata.set_using_secure_screenlock(
pref_service_->GetBoolean(prefs::kEnableAutoScreenLock));
// Auto-unlock here refers to concepts such as "trusted places" and "trusted
// peripherals." Chromebooks do support Smart Lock (i.e., unlocking via the
// user's phone), but these fields are unrelated.
metadata.set_auto_unlock_screenlock_supported(false);
metadata.set_auto_unlock_screenlock_enabled(false);
metadata.set_bluetooth_radio_supported(bluetooth_adapter->IsPresent());
metadata.set_bluetooth_radio_enabled(bluetooth_adapter->IsPowered());
// Within Chrome, there is no way to determine if Bluetooth Classic vs. BLE is
// supported. Since BLE was released in ~2011, it is a safe assumption that
// devices still receiving Chrome OS updates do support BLE, so rely on
// BluetoothAdapter::IsPresent() for this field as well.
metadata.set_ble_radio_supported(bluetooth_adapter->IsPresent());
metadata.set_mobile_data_supported(
network_state_handler_->IsTechnologyAvailable(
NetworkTypePattern::Cellular()));
// The tethering_supported field does not refer to use of Instant Tethering;
// rather, this indicates that Chrome OS devices cannot create their own WiFi
// hotspots.
metadata.set_tethering_supported(false);
metadata.add_feature_metadata()->CopyFrom(GenerateFeatureMetadata());
// Note: |metadata|'s bluetooth_address field is not set since enrollment
// occurs before any opt-in which would alert the users that their Bluetooth
// addresses are being uploaded.
// "Pixel experience" refers only to Android devices, so set this field to
// false even if this Chromebook is a Pixelbook, Pixel Slate, etc.
metadata.set_pixel_experience(false);
// |metadata| is being constructed in the browser process (i.e., outside of
// the ARC++ container).
metadata.set_arc_plus_plus(false);
metadata.set_hardware_user_presence_supported(false);
metadata.set_user_verification_supported(true);
metadata.set_trusted_execution_environment_supported(false);
metadata.set_dedicated_secure_element_supported(false);
client_app_metadata_ = metadata;
InvokePendingCallbacks();
}
void ClientAppMetadataProviderService::OnInstanceIdDeleted(
scoped_refptr<device::BluetoothAdapter> bluetooth_adapter,
const base::SysInfo::HardwareInfo& hardware_info,
instance_id::InstanceID::Result result) {
instance_id_profile_service_->driver()->RemoveInstanceID(
device_sync::kCryptAuthGcmAppId);
if (instance_id_recreated_) {
LogInstanceIdTokenFetchRetries(2);
PA_LOG(WARNING) << "ClientAppMetadataProviderService::"
<< "OnInstanceIdDeleted(): Instance Id deleted twice in a "
<< "row, aborting; result: " << result << ".";
pending_gcm_registration_id_.reset();
instance_id_recreated_ = false;
InvokePendingCallbacks();
return;
}
instance_id_recreated_ = true;
OnHardwareInfoFetched(bluetooth_adapter, hardware_info);
}
instance_id::InstanceID* ClientAppMetadataProviderService::GetInstanceId() {
DCHECK(instance_id_profile_service_);
DCHECK(instance_id_profile_service_->driver());
return instance_id_profile_service_->driver()->GetInstanceID(
device_sync::kCryptAuthGcmAppId);
}
int64_t ClientAppMetadataProviderService::SoftwareVersionCodeAsInt64() {
static const int64_t version_code =
ConvertVersionCodeToInt64(std::string(version_info::GetVersionNumber()));
return version_code;
}
void ClientAppMetadataProviderService::InvokePendingCallbacks() {
auto it = pending_callbacks_.begin();
while (it != pending_callbacks_.end()) {
std::move(*it).Run(client_app_metadata_);
it = pending_callbacks_.erase(it);
}
}
} // namespace ash