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