| // 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 |