| // Copyright (c) 2012 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/chromeos/policy/auto_enrollment_client.h" |
| |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "base/guid.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/message_loop/message_loop_proxy.h" |
| #include "base/metrics/histogram.h" |
| #include "base/metrics/sparse_histogram.h" |
| #include "base/prefs/pref_registry_simple.h" |
| #include "base/prefs/pref_service.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/chromeos/policy/device_cloud_policy_manager_chromeos.h" |
| #include "chrome/browser/policy/browser_policy_connector.h" |
| #include "chrome/common/pref_names.h" |
| #include "chromeos/chromeos_switches.h" |
| #include "components/policy/core/common/cloud/device_management_service.h" |
| #include "components/policy/core/common/cloud/system_policy_request_context.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/common/content_client.h" |
| #include "crypto/sha2.h" |
| #include "net/url_request/url_request_context_getter.h" |
| #include "url/gurl.h" |
| |
| using content::BrowserThread; |
| |
| namespace em = enterprise_management; |
| |
| namespace { |
| |
| // UMA histogram names. |
| const char kUMAProtocolTime[] = "Enterprise.AutoEnrollmentProtocolTime"; |
| const char kUMAExtraTime[] = "Enterprise.AutoEnrollmentExtraTime"; |
| const char kUMARequestStatus[] = "Enterprise.AutoEnrollmentRequestStatus"; |
| const char kUMANetworkErrorCode[] = |
| "Enterprise.AutoEnrollmentRequestNetworkErrorCode"; |
| |
| // The modulus value is sent in an int64 field in the protobuf, whose maximum |
| // value is 2^63-1. So 2^64 and 2^63 can't be represented as moduli and the |
| // max is 2^62 (when the moduli are restricted to powers-of-2). |
| const int kMaximumPower = 62; |
| |
| // Returns the int value of the |switch_name| argument, clamped to the [0, 62] |
| // interval. Returns 0 if the argument doesn't exist or isn't an int value. |
| int GetSanitizedArg(const std::string& switch_name) { |
| CommandLine* command_line = CommandLine::ForCurrentProcess(); |
| if (!command_line->HasSwitch(switch_name)) |
| return 0; |
| std::string value = command_line->GetSwitchValueASCII(switch_name); |
| int int_value; |
| if (!base::StringToInt(value, &int_value)) { |
| LOG(ERROR) << "Switch \"" << switch_name << "\" is not a valid int. " |
| << "Defaulting to 0."; |
| return 0; |
| } |
| if (int_value < 0) { |
| LOG(ERROR) << "Switch \"" << switch_name << "\" can't be negative. " |
| << "Using 0"; |
| return 0; |
| } |
| if (int_value > kMaximumPower) { |
| LOG(ERROR) << "Switch \"" << switch_name << "\" can't be greater than " |
| << kMaximumPower << ". Using " << kMaximumPower; |
| return kMaximumPower; |
| } |
| return int_value; |
| } |
| |
| // Returns the power of the next power-of-2 starting at |value|. |
| int NextPowerOf2(int64 value) { |
| for (int i = 0; i <= kMaximumPower; ++i) { |
| if ((GG_INT64_C(1) << i) >= value) |
| return i; |
| } |
| // No other value can be represented in an int64. |
| return kMaximumPower + 1; |
| } |
| |
| } // namespace |
| |
| namespace policy { |
| |
| AutoEnrollmentClient::AutoEnrollmentClient( |
| const base::Closure& callback, |
| DeviceManagementService* service, |
| PrefService* local_state, |
| scoped_refptr<net::URLRequestContextGetter> system_request_context, |
| const std::string& serial_number, |
| int power_initial, |
| int power_limit) |
| : completion_callback_(callback), |
| should_auto_enroll_(false), |
| device_id_(base::GenerateGUID()), |
| power_initial_(power_initial), |
| power_limit_(power_limit), |
| requests_sent_(0), |
| device_management_service_(service), |
| local_state_(local_state) { |
| request_context_ = new SystemPolicyRequestContext( |
| system_request_context, |
| content::GetUserAgent( |
| GURL(device_management_service_->GetServerUrl()))); |
| |
| DCHECK_LE(power_initial_, power_limit_); |
| DCHECK(!completion_callback_.is_null()); |
| if (!serial_number.empty()) |
| serial_number_hash_ = crypto::SHA256HashString(serial_number); |
| net::NetworkChangeNotifier::AddNetworkChangeObserver(this); |
| } |
| |
| AutoEnrollmentClient::~AutoEnrollmentClient() { |
| net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this); |
| } |
| |
| // static |
| void AutoEnrollmentClient::RegisterPrefs(PrefRegistrySimple* registry) { |
| registry->RegisterBooleanPref(prefs::kShouldAutoEnroll, false); |
| registry->RegisterIntegerPref(prefs::kAutoEnrollmentPowerLimit, -1); |
| } |
| |
| // static |
| bool AutoEnrollmentClient::IsDisabled() { |
| CommandLine* command_line = CommandLine::ForCurrentProcess(); |
| return !command_line->HasSwitch( |
| chromeos::switches::kEnterpriseEnrollmentInitialModulus) && |
| !command_line->HasSwitch( |
| chromeos::switches::kEnterpriseEnrollmentModulusLimit); |
| } |
| |
| // static |
| AutoEnrollmentClient* AutoEnrollmentClient::Create( |
| const base::Closure& completion_callback) { |
| // The client won't do anything if |service| is NULL. |
| DeviceManagementService* service = NULL; |
| if (IsDisabled()) { |
| VLOG(1) << "Auto-enrollment is disabled"; |
| } else { |
| BrowserPolicyConnector* connector = |
| g_browser_process->browser_policy_connector(); |
| service = connector->device_management_service(); |
| service->ScheduleInitialization(0); |
| } |
| |
| int power_initial = GetSanitizedArg( |
| chromeos::switches::kEnterpriseEnrollmentInitialModulus); |
| int power_limit = GetSanitizedArg( |
| chromeos::switches::kEnterpriseEnrollmentModulusLimit); |
| if (power_initial > power_limit) { |
| LOG(ERROR) << "Initial auto-enrollment modulus is larger than the limit, " |
| << "clamping to the limit."; |
| power_initial = power_limit; |
| } |
| |
| return new AutoEnrollmentClient( |
| completion_callback, |
| service, |
| g_browser_process->local_state(), |
| g_browser_process->system_request_context(), |
| DeviceCloudPolicyManagerChromeOS::GetMachineID(), |
| power_initial, |
| power_limit); |
| } |
| |
| // static |
| void AutoEnrollmentClient::CancelAutoEnrollment() { |
| PrefService* local_state = g_browser_process->local_state(); |
| local_state->SetBoolean(prefs::kShouldAutoEnroll, false); |
| local_state->CommitPendingWrite(); |
| } |
| |
| void AutoEnrollmentClient::Start() { |
| // Drop the previous job and reset state. |
| request_job_.reset(); |
| should_auto_enroll_ = false; |
| time_start_ = base::Time(); // reset to null. |
| |
| if (GetCachedDecision()) { |
| VLOG(1) << "AutoEnrollmentClient: using cached decision: " |
| << should_auto_enroll_; |
| } else if (device_management_service_) { |
| if (serial_number_hash_.empty()) { |
| LOG(ERROR) << "Failed to get the hash of the serial number, " |
| << "will not attempt to auto-enroll."; |
| } else { |
| time_start_ = base::Time::Now(); |
| SendRequest(power_initial_); |
| // Don't invoke the callback now. |
| return; |
| } |
| } |
| |
| // Auto-enrollment can't even start, so we're done. |
| OnProtocolDone(); |
| } |
| |
| void AutoEnrollmentClient::CancelAndDeleteSoon() { |
| if (time_start_.is_null()) { |
| // The client isn't running, just delete it. |
| delete this; |
| } else { |
| // Client still running, but our owner isn't interested in the result |
| // anymore. Wait until the protocol completes to measure the extra time |
| // needed. |
| time_extra_start_ = base::Time::Now(); |
| completion_callback_.Reset(); |
| } |
| } |
| |
| void AutoEnrollmentClient::OnNetworkChanged( |
| net::NetworkChangeNotifier::ConnectionType type) { |
| if (GetCachedDecision()) { |
| // A previous request already obtained a definitive response from the |
| // server, so there is no point in retrying; it will get the same decision. |
| return; |
| } |
| |
| if (type != net::NetworkChangeNotifier::CONNECTION_NONE && |
| !completion_callback_.is_null() && |
| !request_job_ && |
| device_management_service_ && |
| !serial_number_hash_.empty()) { |
| VLOG(1) << "Retrying auto enrollment check after network changed"; |
| time_start_ = base::Time::Now(); |
| SendRequest(power_initial_); |
| } |
| } |
| |
| bool AutoEnrollmentClient::GetCachedDecision() { |
| const PrefService::Preference* should_enroll_pref = |
| local_state_->FindPreference(prefs::kShouldAutoEnroll); |
| const PrefService::Preference* previous_limit_pref = |
| local_state_->FindPreference(prefs::kAutoEnrollmentPowerLimit); |
| bool should_auto_enroll = false; |
| int previous_limit = -1; |
| |
| if (!should_enroll_pref || |
| should_enroll_pref->IsDefaultValue() || |
| !should_enroll_pref->GetValue()->GetAsBoolean(&should_auto_enroll) || |
| !previous_limit_pref || |
| previous_limit_pref->IsDefaultValue() || |
| !previous_limit_pref->GetValue()->GetAsInteger(&previous_limit) || |
| power_limit_ > previous_limit) { |
| return false; |
| } |
| |
| should_auto_enroll_ = should_auto_enroll; |
| return true; |
| } |
| |
| void AutoEnrollmentClient::SendRequest(int power) { |
| if (power < 0 || power > power_limit_ || serial_number_hash_.empty()) { |
| NOTREACHED(); |
| OnRequestDone(); |
| return; |
| } |
| |
| requests_sent_++; |
| |
| // Only power-of-2 moduli are supported for now. These are computed by taking |
| // the lower |power| bits of the hash. |
| uint64 remainder = 0; |
| for (int i = 0; 8 * i < power; ++i) { |
| uint64 byte = serial_number_hash_[31 - i] & 0xff; |
| remainder = remainder | (byte << (8 * i)); |
| } |
| remainder = remainder & ((GG_UINT64_C(1) << power) - 1); |
| |
| request_job_.reset( |
| device_management_service_->CreateJob( |
| DeviceManagementRequestJob::TYPE_AUTO_ENROLLMENT, |
| request_context_.get())); |
| request_job_->SetClientID(device_id_); |
| em::DeviceAutoEnrollmentRequest* request = |
| request_job_->GetRequest()->mutable_auto_enrollment_request(); |
| request->set_remainder(remainder); |
| request->set_modulus(GG_INT64_C(1) << power); |
| request_job_->Start(base::Bind(&AutoEnrollmentClient::OnRequestCompletion, |
| base::Unretained(this))); |
| } |
| |
| void AutoEnrollmentClient::OnRequestCompletion( |
| DeviceManagementStatus status, |
| int net_error, |
| const em::DeviceManagementResponse& response) { |
| if (status != DM_STATUS_SUCCESS || !response.has_auto_enrollment_response()) { |
| LOG(ERROR) << "Auto enrollment error: " << status; |
| UMA_HISTOGRAM_SPARSE_SLOWLY(kUMARequestStatus, status); |
| if (status == DM_STATUS_REQUEST_FAILED) |
| UMA_HISTOGRAM_SPARSE_SLOWLY(kUMANetworkErrorCode, -net_error); |
| // The client will retry if a network change is detected. |
| OnRequestDone(); |
| return; |
| } |
| |
| const em::DeviceAutoEnrollmentResponse& enrollment_response = |
| response.auto_enrollment_response(); |
| if (enrollment_response.has_expected_modulus()) { |
| // Server is asking us to retry with a different modulus. |
| int64 modulus = enrollment_response.expected_modulus(); |
| int power = NextPowerOf2(modulus); |
| if ((GG_INT64_C(1) << power) != modulus) { |
| LOG(WARNING) << "Auto enrollment: the server didn't ask for a power-of-2 " |
| << "modulus. Using the closest power-of-2 instead " |
| << "(" << modulus << " vs 2^" << power << ")"; |
| } |
| if (requests_sent_ >= 2) { |
| LOG(ERROR) << "Auto enrollment error: already retried with an updated " |
| << "modulus but the server asked for a new one again: " |
| << power; |
| } else if (power > power_limit_) { |
| LOG(ERROR) << "Auto enrollment error: the server asked for a larger " |
| << "modulus than the client accepts (" << power << " vs " |
| << power_limit_ << ")."; |
| } else { |
| // Retry at most once with the modulus that the server requested. |
| if (power <= power_initial_) { |
| LOG(WARNING) << "Auto enrollment: the server asked to use a modulus (" |
| << power << ") that isn't larger than the first used (" |
| << power_initial_ << "). Retrying anyway."; |
| } |
| // Remember this value, so that eventual retries start with the correct |
| // modulus. |
| power_initial_ = power; |
| SendRequest(power); |
| return; |
| } |
| } else { |
| // Server should have sent down a list of hashes to try. |
| should_auto_enroll_ = IsSerialInProtobuf(enrollment_response.hash()); |
| // Cache the current decision in local_state, so that it is reused in case |
| // the device reboots before enrolling. |
| local_state_->SetBoolean(prefs::kShouldAutoEnroll, should_auto_enroll_); |
| local_state_->SetInteger(prefs::kAutoEnrollmentPowerLimit, power_limit_); |
| local_state_->CommitPendingWrite(); |
| VLOG(1) << "Auto enrollment complete, should_auto_enroll = " |
| << should_auto_enroll_; |
| } |
| |
| // Auto-enrollment done. |
| UMA_HISTOGRAM_SPARSE_SLOWLY(kUMARequestStatus, DM_STATUS_SUCCESS); |
| OnProtocolDone(); |
| } |
| |
| bool AutoEnrollmentClient::IsSerialInProtobuf( |
| const google::protobuf::RepeatedPtrField<std::string>& hashes) { |
| for (int i = 0; i < hashes.size(); ++i) { |
| if (hashes.Get(i) == serial_number_hash_) |
| return true; |
| } |
| return false; |
| } |
| |
| void AutoEnrollmentClient::OnProtocolDone() { |
| // The mininum time can't be 0, must be at least 1. |
| static const base::TimeDelta kMin = base::TimeDelta::FromMilliseconds(1); |
| static const base::TimeDelta kMax = base::TimeDelta::FromMinutes(5); |
| // However, 0 can still be sampled. |
| static const base::TimeDelta kZero = base::TimeDelta::FromMilliseconds(0); |
| static const int kBuckets = 50; |
| |
| base::Time now = base::Time::Now(); |
| if (!time_start_.is_null()) { |
| base::TimeDelta delta = now - time_start_; |
| UMA_HISTOGRAM_CUSTOM_TIMES(kUMAProtocolTime, delta, kMin, kMax, kBuckets); |
| } |
| base::TimeDelta delta = kZero; |
| if (!time_extra_start_.is_null()) |
| delta = now - time_extra_start_; |
| // This samples |kZero| when there was no need for extra time, so that we can |
| // measure the ratio of users that succeeded without needing a delay to the |
| // total users going through OOBE. |
| UMA_HISTOGRAM_CUSTOM_TIMES(kUMAExtraTime, delta, kMin, kMax, kBuckets); |
| |
| if (!completion_callback_.is_null()) |
| completion_callback_.Run(); |
| |
| OnRequestDone(); |
| } |
| |
| void AutoEnrollmentClient::OnRequestDone() { |
| request_job_.reset(); |
| time_start_ = base::Time(); |
| |
| if (completion_callback_.is_null()) { |
| // CancelAndDeleteSoon() was invoked before. |
| base::MessageLoopProxy::current()->DeleteSoon(FROM_HERE, this); |
| } |
| } |
| |
| } // namespace policy |