auto_enrollment_client.cc revision 4e180b6a0b4720a9b8e9e959a882386f690f08ff
1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/chromeos/policy/auto_enrollment_client.h"
6
7#include "base/bind.h"
8#include "base/command_line.h"
9#include "base/guid.h"
10#include "base/location.h"
11#include "base/logging.h"
12#include "base/message_loop/message_loop_proxy.h"
13#include "base/metrics/histogram.h"
14#include "base/metrics/sparse_histogram.h"
15#include "base/prefs/pref_registry_simple.h"
16#include "base/prefs/pref_service.h"
17#include "base/strings/string_number_conversions.h"
18#include "chrome/browser/browser_process.h"
19#include "chrome/browser/chromeos/policy/device_cloud_policy_manager_chromeos.h"
20#include "chrome/browser/policy/browser_policy_connector.h"
21#include "chrome/browser/policy/cloud/device_management_service.h"
22#include "chrome/common/pref_names.h"
23#include "chromeos/chromeos_switches.h"
24#include "content/public/browser/browser_thread.h"
25#include "crypto/sha2.h"
26#include "net/url_request/url_request_context_getter.h"
27
28using content::BrowserThread;
29
30namespace em = enterprise_management;
31
32namespace {
33
34// UMA histogram names.
35const char kUMAProtocolTime[] = "Enterprise.AutoEnrollmentProtocolTime";
36const char kUMAExtraTime[] = "Enterprise.AutoEnrollmentExtraTime";
37const char kUMARequestStatus[] = "Enterprise.AutoEnrollmentRequestStatus";
38const char kUMANetworkErrorCode[] =
39    "Enterprise.AutoEnrollmentRequestNetworkErrorCode";
40
41// The modulus value is sent in an int64 field in the protobuf, whose maximum
42// value is 2^63-1. So 2^64 and 2^63 can't be represented as moduli and the
43// max is 2^62 (when the moduli are restricted to powers-of-2).
44const int kMaximumPower = 62;
45
46// Returns the int value of the |switch_name| argument, clamped to the [0, 62]
47// interval. Returns 0 if the argument doesn't exist or isn't an int value.
48int GetSanitizedArg(const std::string& switch_name) {
49  CommandLine* command_line = CommandLine::ForCurrentProcess();
50  if (!command_line->HasSwitch(switch_name))
51    return 0;
52  std::string value = command_line->GetSwitchValueASCII(switch_name);
53  int int_value;
54  if (!base::StringToInt(value, &int_value)) {
55    LOG(ERROR) << "Switch \"" << switch_name << "\" is not a valid int. "
56               << "Defaulting to 0.";
57    return 0;
58  }
59  if (int_value < 0) {
60    LOG(ERROR) << "Switch \"" << switch_name << "\" can't be negative. "
61               << "Using 0";
62    return 0;
63  }
64  if (int_value > kMaximumPower) {
65    LOG(ERROR) << "Switch \"" << switch_name << "\" can't be greater than "
66               << kMaximumPower << ". Using " << kMaximumPower;
67    return kMaximumPower;
68  }
69  return int_value;
70}
71
72// Returns the power of the next power-of-2 starting at |value|.
73int NextPowerOf2(int64 value) {
74  for (int i = 0; i <= kMaximumPower; ++i) {
75    if ((GG_INT64_C(1) << i) >= value)
76      return i;
77  }
78  // No other value can be represented in an int64.
79  return kMaximumPower + 1;
80}
81
82}  // namespace
83
84namespace policy {
85
86AutoEnrollmentClient::AutoEnrollmentClient(const base::Closure& callback,
87                                           DeviceManagementService* service,
88                                           PrefService* local_state,
89                                           const std::string& serial_number,
90                                           int power_initial,
91                                           int power_limit)
92    : completion_callback_(callback),
93      should_auto_enroll_(false),
94      device_id_(base::GenerateGUID()),
95      power_initial_(power_initial),
96      power_limit_(power_limit),
97      requests_sent_(0),
98      device_management_service_(service),
99      local_state_(local_state) {
100  DCHECK_LE(power_initial_, power_limit_);
101  DCHECK(!completion_callback_.is_null());
102  if (!serial_number.empty())
103    serial_number_hash_ = crypto::SHA256HashString(serial_number);
104  net::NetworkChangeNotifier::AddNetworkChangeObserver(this);
105}
106
107AutoEnrollmentClient::~AutoEnrollmentClient() {
108  net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
109}
110
111// static
112void AutoEnrollmentClient::RegisterPrefs(PrefRegistrySimple* registry) {
113  registry->RegisterBooleanPref(prefs::kShouldAutoEnroll, false);
114  registry->RegisterIntegerPref(prefs::kAutoEnrollmentPowerLimit, -1);
115}
116
117// static
118bool AutoEnrollmentClient::IsDisabled() {
119  CommandLine* command_line = CommandLine::ForCurrentProcess();
120  return !command_line->HasSwitch(
121             chromeos::switches::kEnterpriseEnrollmentInitialModulus) &&
122         !command_line->HasSwitch(
123             chromeos::switches::kEnterpriseEnrollmentModulusLimit);
124}
125
126// static
127AutoEnrollmentClient* AutoEnrollmentClient::Create(
128    const base::Closure& completion_callback) {
129  // The client won't do anything if |service| is NULL.
130  DeviceManagementService* service = NULL;
131  if (IsDisabled()) {
132    VLOG(1) << "Auto-enrollment is disabled";
133  } else {
134    BrowserPolicyConnector* connector =
135        g_browser_process->browser_policy_connector();
136    service = connector->device_management_service();
137    service->ScheduleInitialization(0);
138  }
139
140  int power_initial = GetSanitizedArg(
141      chromeos::switches::kEnterpriseEnrollmentInitialModulus);
142  int power_limit = GetSanitizedArg(
143      chromeos::switches::kEnterpriseEnrollmentModulusLimit);
144  if (power_initial > power_limit) {
145    LOG(ERROR) << "Initial auto-enrollment modulus is larger than the limit, "
146               << "clamping to the limit.";
147    power_initial = power_limit;
148  }
149
150  return new AutoEnrollmentClient(
151      completion_callback,
152      service,
153      g_browser_process->local_state(),
154      DeviceCloudPolicyManagerChromeOS::GetMachineID(),
155      power_initial,
156      power_limit);
157}
158
159// static
160void AutoEnrollmentClient::CancelAutoEnrollment() {
161  PrefService* local_state = g_browser_process->local_state();
162  local_state->SetBoolean(prefs::kShouldAutoEnroll, false);
163  local_state->CommitPendingWrite();
164}
165
166void AutoEnrollmentClient::Start() {
167  // Drop the previous job and reset state.
168  request_job_.reset();
169  should_auto_enroll_ = false;
170  time_start_ = base::Time();  // reset to null.
171
172  if (GetCachedDecision()) {
173    VLOG(1) << "AutoEnrollmentClient: using cached decision: "
174            << should_auto_enroll_;
175  } else if (device_management_service_) {
176    if (serial_number_hash_.empty()) {
177      LOG(ERROR) << "Failed to get the hash of the serial number, "
178                 << "will not attempt to auto-enroll.";
179    } else {
180      time_start_ = base::Time::Now();
181      SendRequest(power_initial_);
182      // Don't invoke the callback now.
183      return;
184    }
185  }
186
187  // Auto-enrollment can't even start, so we're done.
188  OnProtocolDone();
189}
190
191void AutoEnrollmentClient::CancelAndDeleteSoon() {
192  if (time_start_.is_null()) {
193    // The client isn't running, just delete it.
194    delete this;
195  } else {
196    // Client still running, but our owner isn't interested in the result
197    // anymore. Wait until the protocol completes to measure the extra time
198    // needed.
199    time_extra_start_ = base::Time::Now();
200    completion_callback_.Reset();
201  }
202}
203
204void AutoEnrollmentClient::OnNetworkChanged(
205    net::NetworkChangeNotifier::ConnectionType type) {
206  if (GetCachedDecision()) {
207    // A previous request already obtained a definitive response from the
208    // server, so there is no point in retrying; it will get the same decision.
209    return;
210  }
211
212  if (type != net::NetworkChangeNotifier::CONNECTION_NONE &&
213      !completion_callback_.is_null() &&
214      !request_job_ &&
215      device_management_service_ &&
216      !serial_number_hash_.empty()) {
217    VLOG(1) << "Retrying auto enrollment check after network changed";
218    time_start_ = base::Time::Now();
219    SendRequest(power_initial_);
220  }
221}
222
223bool AutoEnrollmentClient::GetCachedDecision() {
224  const PrefService::Preference* should_enroll_pref =
225      local_state_->FindPreference(prefs::kShouldAutoEnroll);
226  const PrefService::Preference* previous_limit_pref =
227      local_state_->FindPreference(prefs::kAutoEnrollmentPowerLimit);
228  bool should_auto_enroll = false;
229  int previous_limit = -1;
230
231  if (!should_enroll_pref ||
232      should_enroll_pref->IsDefaultValue() ||
233      !should_enroll_pref->GetValue()->GetAsBoolean(&should_auto_enroll) ||
234      !previous_limit_pref ||
235      previous_limit_pref->IsDefaultValue() ||
236      !previous_limit_pref->GetValue()->GetAsInteger(&previous_limit) ||
237      power_limit_ > previous_limit) {
238    return false;
239  }
240
241  should_auto_enroll_ = should_auto_enroll;
242  return true;
243}
244
245void AutoEnrollmentClient::SendRequest(int power) {
246  if (power < 0 || power > power_limit_ || serial_number_hash_.empty()) {
247    NOTREACHED();
248    OnRequestDone();
249    return;
250  }
251
252  requests_sent_++;
253
254  // Only power-of-2 moduli are supported for now. These are computed by taking
255  // the lower |power| bits of the hash.
256  uint64 remainder = 0;
257  for (int i = 0; 8 * i < power; ++i) {
258    uint64 byte = serial_number_hash_[31 - i] & 0xff;
259    remainder = remainder | (byte << (8 * i));
260  }
261  remainder = remainder & ((GG_UINT64_C(1) << power) - 1);
262
263  request_job_.reset(
264      device_management_service_->CreateJob(
265          DeviceManagementRequestJob::TYPE_AUTO_ENROLLMENT));
266  request_job_->SetClientID(device_id_);
267  em::DeviceAutoEnrollmentRequest* request =
268      request_job_->GetRequest()->mutable_auto_enrollment_request();
269  request->set_remainder(remainder);
270  request->set_modulus(GG_INT64_C(1) << power);
271  request_job_->Start(base::Bind(&AutoEnrollmentClient::OnRequestCompletion,
272                                 base::Unretained(this)));
273}
274
275void AutoEnrollmentClient::OnRequestCompletion(
276    DeviceManagementStatus status,
277    int net_error,
278    const em::DeviceManagementResponse& response) {
279  if (status != DM_STATUS_SUCCESS || !response.has_auto_enrollment_response()) {
280    LOG(ERROR) << "Auto enrollment error: " << status;
281    UMA_HISTOGRAM_SPARSE_SLOWLY(kUMARequestStatus, status);
282    if (status == DM_STATUS_REQUEST_FAILED)
283      UMA_HISTOGRAM_SPARSE_SLOWLY(kUMANetworkErrorCode, -net_error);
284    // The client will retry if a network change is detected.
285    OnRequestDone();
286    return;
287  }
288
289  const em::DeviceAutoEnrollmentResponse& enrollment_response =
290      response.auto_enrollment_response();
291  if (enrollment_response.has_expected_modulus()) {
292    // Server is asking us to retry with a different modulus.
293    int64 modulus = enrollment_response.expected_modulus();
294    int power = NextPowerOf2(modulus);
295    if ((GG_INT64_C(1) << power) != modulus) {
296      LOG(WARNING) << "Auto enrollment: the server didn't ask for a power-of-2 "
297                   << "modulus. Using the closest power-of-2 instead "
298                   << "(" << modulus << " vs 2^" << power << ")";
299    }
300    if (requests_sent_ >= 2) {
301      LOG(ERROR) << "Auto enrollment error: already retried with an updated "
302                 << "modulus but the server asked for a new one again: "
303                 << power;
304    } else if (power > power_limit_) {
305      LOG(ERROR) << "Auto enrollment error: the server asked for a larger "
306                 << "modulus than the client accepts (" << power << " vs "
307                 << power_limit_ << ").";
308    } else {
309      // Retry at most once with the modulus that the server requested.
310      if (power <= power_initial_) {
311        LOG(WARNING) << "Auto enrollment: the server asked to use a modulus ("
312                     << power << ") that isn't larger than the first used ("
313                     << power_initial_ << "). Retrying anyway.";
314      }
315      // Remember this value, so that eventual retries start with the correct
316      // modulus.
317      power_initial_ = power;
318      SendRequest(power);
319      return;
320    }
321  } else {
322    // Server should have sent down a list of hashes to try.
323    should_auto_enroll_ = IsSerialInProtobuf(enrollment_response.hash());
324    // Cache the current decision in local_state, so that it is reused in case
325    // the device reboots before enrolling.
326    local_state_->SetBoolean(prefs::kShouldAutoEnroll, should_auto_enroll_);
327    local_state_->SetInteger(prefs::kAutoEnrollmentPowerLimit, power_limit_);
328    local_state_->CommitPendingWrite();
329    LOG(INFO) << "Auto enrollment complete, should_auto_enroll = "
330              << should_auto_enroll_;
331  }
332
333  // Auto-enrollment done.
334  UMA_HISTOGRAM_SPARSE_SLOWLY(kUMARequestStatus, DM_STATUS_SUCCESS);
335  OnProtocolDone();
336}
337
338bool AutoEnrollmentClient::IsSerialInProtobuf(
339      const google::protobuf::RepeatedPtrField<std::string>& hashes) {
340  for (int i = 0; i < hashes.size(); ++i) {
341    if (hashes.Get(i) == serial_number_hash_)
342      return true;
343  }
344  return false;
345}
346
347void AutoEnrollmentClient::OnProtocolDone() {
348  // The mininum time can't be 0, must be at least 1.
349  static const base::TimeDelta kMin = base::TimeDelta::FromMilliseconds(1);
350  static const base::TimeDelta kMax = base::TimeDelta::FromMinutes(5);
351  // However, 0 can still be sampled.
352  static const base::TimeDelta kZero = base::TimeDelta::FromMilliseconds(0);
353  static const int kBuckets = 50;
354
355  base::Time now = base::Time::Now();
356  if (!time_start_.is_null()) {
357    base::TimeDelta delta = now - time_start_;
358    UMA_HISTOGRAM_CUSTOM_TIMES(kUMAProtocolTime, delta, kMin, kMax, kBuckets);
359  }
360  base::TimeDelta delta = kZero;
361  if (!time_extra_start_.is_null())
362    delta = now - time_extra_start_;
363  // This samples |kZero| when there was no need for extra time, so that we can
364  // measure the ratio of users that succeeded without needing a delay to the
365  // total users going through OOBE.
366  UMA_HISTOGRAM_CUSTOM_TIMES(kUMAExtraTime, delta, kMin, kMax, kBuckets);
367
368  if (!completion_callback_.is_null())
369    completion_callback_.Run();
370
371  OnRequestDone();
372}
373
374void AutoEnrollmentClient::OnRequestDone() {
375  request_job_.reset();
376  time_start_ = base::Time();
377
378  if (completion_callback_.is_null()) {
379    // CancelAndDeleteSoon() was invoked before.
380    base::MessageLoopProxy::current()->DeleteSoon(FROM_HERE, this);
381  }
382}
383
384}  // namespace policy
385