blob: b97adf146b28cce167024a9e166fd04d0b4f4905 [file] [log] [blame]
// Copyright 2018 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_impl.h"
#include <stdint.h>
#include "base/bind.h"
#include "base/guid.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/optional.h"
#include "base/strings/string_number_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "chrome/browser/ash/login/enrollment/auto_enrollment_controller.h"
#include "chrome/browser/chromeos/policy/server_backed_device_state.h"
#include "chrome/common/chrome_content_client.h"
#include "chrome/common/pref_names.h"
#include "components/policy/core/common/cloud/device_management_service.h"
#include "components/policy/core/common/cloud/dm_auth.h"
#include "components/policy/core/common/cloud/dmserver_job_configurations.h"
#include "components/policy/core/common/cloud/enterprise_metrics.h"
#include "components/policy/proto/device_management_backend.pb.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/network_service_instance.h"
#include "crypto/sha2.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "third_party/private_membership/src/private_membership_rlwe_client.h"
#include "url/gurl.h"
using content::BrowserThread;
namespace psm_rlwe = private_membership::rlwe;
namespace em = enterprise_management;
namespace policy {
namespace {
using EnrollmentCheckType =
em::DeviceAutoEnrollmentRequest::EnrollmentCheckType;
// Timeout for running PSM protocol.
constexpr base::TimeDelta kPsmTimeout = base::TimeDelta::FromSeconds(15);
// Returns the power of the next power-of-2 starting at |value|.
int NextPowerOf2(int64_t value) {
for (int i = 0; i <= AutoEnrollmentClient::kMaximumPower; ++i) {
if ((INT64_C(1) << i) >= value)
return i;
}
// No other value can be represented in an int64_t.
return AutoEnrollmentClient::kMaximumPower + 1;
}
// Sets or clears a value in a dictionary.
void UpdateDict(base::DictionaryValue* dict,
const char* pref_path,
bool set_or_clear,
std::unique_ptr<base::Value> value) {
if (set_or_clear)
dict->Set(pref_path, std::move(value));
else
dict->Remove(pref_path, NULL);
}
// Converts a restore mode enum value from the DM protocol for FRE into the
// corresponding prefs string constant.
std::string ConvertRestoreMode(
em::DeviceStateRetrievalResponse::RestoreMode restore_mode) {
switch (restore_mode) {
case em::DeviceStateRetrievalResponse::RESTORE_MODE_NONE:
return std::string();
case em::DeviceStateRetrievalResponse::RESTORE_MODE_REENROLLMENT_REQUESTED:
return kDeviceStateRestoreModeReEnrollmentRequested;
case em::DeviceStateRetrievalResponse::RESTORE_MODE_REENROLLMENT_ENFORCED:
return kDeviceStateRestoreModeReEnrollmentEnforced;
case em::DeviceStateRetrievalResponse::RESTORE_MODE_DISABLED:
return kDeviceStateModeDisabled;
case em::DeviceStateRetrievalResponse::RESTORE_MODE_REENROLLMENT_ZERO_TOUCH:
return kDeviceStateRestoreModeReEnrollmentZeroTouch;
}
// Return is required to avoid compiler warning.
NOTREACHED() << "Bad restore_mode=" << restore_mode << ".";
return std::string();
}
// Converts an initial enrollment mode enum value from the DM protocol for
// initial enrollment into the corresponding prefs string constant.
std::string ConvertInitialEnrollmentMode(
em::DeviceInitialEnrollmentStateResponse::InitialEnrollmentMode
initial_enrollment_mode) {
switch (initial_enrollment_mode) {
case em::DeviceInitialEnrollmentStateResponse::INITIAL_ENROLLMENT_MODE_NONE:
return std::string();
case em::DeviceInitialEnrollmentStateResponse::
INITIAL_ENROLLMENT_MODE_ENROLLMENT_ENFORCED:
return kDeviceStateInitialModeEnrollmentEnforced;
case em::DeviceInitialEnrollmentStateResponse::
INITIAL_ENROLLMENT_MODE_ZERO_TOUCH_ENFORCED:
return kDeviceStateInitialModeEnrollmentZeroTouch;
case em::DeviceInitialEnrollmentStateResponse::
INITIAL_ENROLLMENT_MODE_DISABLED:
return kDeviceStateModeDisabled;
}
}
} // namespace
psm_rlwe::RlwePlaintextId ConstructDeviceRlweId(
const std::string& device_serial_number,
const std::string& device_rlz_brand_code) {
psm_rlwe::RlwePlaintextId rlwe_id;
std::string rlz_brand_code_hex = base::HexEncode(
device_rlz_brand_code.data(), device_rlz_brand_code.size());
rlwe_id.set_sensitive_id(rlz_brand_code_hex + "/" + device_serial_number);
return rlwe_id;
}
// Subclasses of this class provide an identifier and specify the identifier
// set for the DeviceAutoEnrollmentRequest,
class AutoEnrollmentClientImpl::DeviceIdentifierProvider {
public:
virtual ~DeviceIdentifierProvider() {}
// Should return the EnrollmentCheckType to be used in the
// DeviceAutoEnrollmentRequest. This specifies the identifier set used on
// the server.
virtual enterprise_management::DeviceAutoEnrollmentRequest::
EnrollmentCheckType
GetEnrollmentCheckType() const = 0;
// Should return the hash of this device's identifier. The
// DeviceAutoEnrollmentRequest exchange will check if this hash is in the
// server-side identifier set specified by |GetEnrollmentCheckType()|
virtual const std::string& GetIdHash() const = 0;
};
// Subclasses of this class generate the request to download the device state
// (after determining that there is server-side device state) and parse the
// response.
class AutoEnrollmentClientImpl::StateDownloadMessageProcessor {
public:
virtual ~StateDownloadMessageProcessor() {}
// Parsed fields of DeviceManagementResponse.
struct ParsedResponse {
std::string restore_mode;
base::Optional<std::string> management_domain;
base::Optional<std::string> disabled_message;
base::Optional<bool> is_license_packaged_with_device;
};
// Returns the request job type. This must match the request filled in
// |FillRequest|.
virtual DeviceManagementService::JobConfiguration::JobType GetJobType()
const = 0;
// Fills the specific request type in |request|.
virtual void FillRequest(
enterprise_management::DeviceManagementRequest* request) = 0;
// Parses the |response|. If it is valid, returns a ParsedResponse struct
// instance. If it is invalid, returns nullopt.
virtual base::Optional<ParsedResponse> ParseResponse(
const enterprise_management::DeviceManagementResponse& response) = 0;
};
class PsmHelper {
public:
// Callback will be triggered after completing the protocol, in case of a
// successful determination or stopping due to an error. Also, the bool result
// is ignored.
using CompletionCallback = base::OnceCallback<bool()>;
// The PsmHelper doesn't take ownership of |device_management_service| and
// |local_state|. Also, both must not be nullptr. The
// |device_management_service| and |local_state| must outlive PsmHelper.
PsmHelper(DeviceManagementService* device_management_service,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
PrefService* local_state,
psm_rlwe::RlwePlaintextId psm_rlwe_id)
: random_device_id_(base::GenerateGUID()),
url_loader_factory_(url_loader_factory),
device_management_service_(device_management_service),
local_state_(local_state),
psm_rlwe_id_(std::move(psm_rlwe_id)) {
CHECK(device_management_service);
DCHECK(local_state_);
// Create PSM client for |psm_rlwe_id_| with use case as CROS_DEVICE_STATE.
std::vector<psm_rlwe::RlwePlaintextId> psm_ids = {psm_rlwe_id_};
auto status_or_client = psm_rlwe::PrivateMembershipRlweClient::Create(
psm_rlwe::RlweUseCase::CROS_DEVICE_STATE, psm_ids);
if (!status_or_client.ok()) {
// If the PSM RLWE client hasn't been created successfully, then report
// the error and don't run the protocol.
LOG(ERROR)
<< "PSM error: unexpected internal logic error during creating "
"PSM RLWE client";
has_psm_error_ = true;
return;
}
psm_rlwe_client_ = std::move(status_or_client).value();
}
// Disallow copy constructor and assignment operator.
PsmHelper(const PsmHelper&) = delete;
PsmHelper& operator=(const PsmHelper&) = delete;
// Cancels the ongoing PSM operation, if any (without calling the operation's
// callbacks).
~PsmHelper() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); }
// Determines the PSM for the |psm_rlwe_id_|. Then, will call |callback| upon
// completing the protocol, whether it finished with a successful
// determination or stopped in case of errors. Also, the |callback| has to be
// non-null. In case a request is already in progress, the callback is called
// immediately.
void CheckMembership(CompletionCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(callback);
// Ignore new calls and execute their completion |callback|, if any error
// occurred while running PSM previously, or in case the
// requests from previous call didn't finish yet.
if (has_psm_error_ || psm_request_job_) {
std::move(callback).Run();
return;
}
// Report the psm attempt and start the timer to measure successful private
// set membership requests.
base::UmaHistogramEnumeration(kUMAPsmRequestStatus, PsmStatus::kAttempt);
time_start_ = base::TimeTicks::Now();
on_completion_callback_ = std::move(callback);
// Start the protocol and its timeout timer.
psm_timeout_.Start(
FROM_HERE, kPsmTimeout,
base::BindOnce(&PsmHelper::OnTimeout, base::Unretained(this)));
SendPsmRlweOprfRequest();
}
// Sets the |psm_rlwe_client_| and |psm_rlwe_id_| for testing.
void SetRlweClientAndIdForTesting(
std::unique_ptr<psm_rlwe::PrivateMembershipRlweClient> psm_rlwe_client,
psm_rlwe::RlwePlaintextId psm_rlwe_id) {
psm_rlwe_client_ = std::move(psm_rlwe_client);
psm_rlwe_id_ = std::move(psm_rlwe_id);
}
// Tries to load the result of a previous execution of the PSM protocol from
// local state. Returns decision value if it has been made and is valid,
// otherwise nullopt.
base::Optional<bool> GetPsmCachedDecision() const {
const PrefService::Preference* has_psm_server_state_pref =
local_state_->FindPreference(prefs::kShouldRetrieveDeviceState);
if (!has_psm_server_state_pref ||
has_psm_server_state_pref->IsDefaultValue() ||
!has_psm_server_state_pref->GetValue()->is_bool()) {
return base::nullopt;
}
return has_psm_server_state_pref->GetValue()->GetBool();
}
// Indicate whether an error occurred while executing the PSM protocol.
bool HasPsmError() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return has_psm_error_;
}
// Returns true if the PSM protocol is still running,
// otherwise false.
bool IsCheckMembershipInProgress() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return psm_request_job_ != nullptr;
}
private:
void OnTimeout() {
base::UmaHistogramEnumeration(kUMAPsmRequestStatus, PsmStatus::kTimeout);
StoreErrorAndStop();
}
void StoreErrorAndStop() {
// Record the error. Note that a timeout is also recorded as error.
base::UmaHistogramEnumeration(kUMAPsmRequestStatus, PsmStatus::kError);
// Stop the PSM timer.
psm_timeout_.Stop();
// Stop the current |psm_request_job_|.
psm_request_job_.reset();
has_psm_error_ = true;
std::move(on_completion_callback_).Run();
}
// Constructs and sends the PSM RLWE OPRF request.
void SendPsmRlweOprfRequest() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Create RLWE OPRF request.
const auto status_or_oprf_request = psm_rlwe_client_->CreateOprfRequest();
if (!status_or_oprf_request.ok()) {
// If the RLWE OPRF request hasn't been created successfully, then report
// the error and stop the protocol.
LOG(ERROR)
<< "PSM error: unexpected internal logic error during creating "
"RLWE OPRF request";
StoreErrorAndStop();
return;
}
LOG(WARNING) << "PSM: prepare and send out the RLWE OPRF request";
// Prepare the RLWE OPRF request job.
// The passed callback will not be called if |psm_request_job_| is
// destroyed, so it's safe to use base::Unretained.
std::unique_ptr<DMServerJobConfiguration> config =
CreatePsmRequestJobConfiguration(base::BindOnce(
&PsmHelper::OnRlweOprfRequestCompletion, base::Unretained(this)));
em::DeviceManagementRequest* request = config->request();
em::PrivateSetMembershipRlweRequest* psm_rlwe_request =
request->mutable_private_set_membership_request()
->mutable_rlwe_request();
*psm_rlwe_request->mutable_oprf_request() = status_or_oprf_request.value();
psm_request_job_ = device_management_service_->CreateJob(std::move(config));
}
// If the completion was successful, then it makes another request to
// DMServer for performing phase two.
void OnRlweOprfRequestCompletion(
DeviceManagementService::Job* job,
DeviceManagementStatus status,
int net_error,
const em::DeviceManagementResponse& response) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
switch (status) {
case DM_STATUS_SUCCESS: {
// Check if the RLWE OPRF response is empty.
if (!response.private_set_membership_response().has_rlwe_response() ||
!response.private_set_membership_response()
.rlwe_response()
.has_oprf_response()) {
LOG(ERROR) << "PSM error: empty OPRF RLWE response";
StoreErrorAndStop();
return;
}
LOG(WARNING) << "PSM RLWE OPRF request completed successfully";
SendPsmRlweQueryRequest(response.private_set_membership_response());
return;
}
case DM_STATUS_REQUEST_FAILED: {
LOG(ERROR)
<< "PSM error: RLWE OPRF request failed due to connection error";
StoreErrorAndStop();
return;
}
default: {
LOG(ERROR) << "PSM error: RLWE OPRF request failed due to server error";
StoreErrorAndStop();
return;
}
}
}
// Constructs and sends the PSM RLWE Query request.
void SendPsmRlweQueryRequest(
const em::PrivateSetMembershipResponse& psm_response) {
// Extract the oprf_response from |psm_response|.
const psm_rlwe::PrivateMembershipRlweOprfResponse oprf_response =
psm_response.rlwe_response().oprf_response();
const auto status_or_query_request =
psm_rlwe_client_->CreateQueryRequest(oprf_response);
// Create RLWE query request.
if (!status_or_query_request.ok()) {
// If the RLWE query request hasn't been created successfully, then report
// the error and stop the protocol.
LOG(ERROR)
<< "PSM error: unexpected internal logic error during creating "
"RLWE query request";
StoreErrorAndStop();
return;
}
LOG(WARNING) << "PSM: prepare and send out the RLWE query request";
// Prepare the RLWE query request job.
std::unique_ptr<DMServerJobConfiguration> config =
CreatePsmRequestJobConfiguration(
base::BindOnce(&PsmHelper::OnRlweQueryRequestCompletion,
base::Unretained(this), oprf_response));
em::DeviceManagementRequest* request = config->request();
em::PrivateSetMembershipRlweRequest* psm_rlwe_request =
request->mutable_private_set_membership_request()
->mutable_rlwe_request();
*psm_rlwe_request->mutable_query_request() =
status_or_query_request.value();
psm_request_job_ = device_management_service_->CreateJob(std::move(config));
}
// If the completion was successful, then it will parse the result and call
// the |on_completion_callback_| for |psm_id_|.
void OnRlweQueryRequestCompletion(
const psm_rlwe::PrivateMembershipRlweOprfResponse& oprf_response,
DeviceManagementService::Job* job,
DeviceManagementStatus status,
int net_error,
const em::DeviceManagementResponse& response) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
switch (status) {
case DM_STATUS_SUCCESS: {
// Check if the RLWE query response is empty.
if (!response.private_set_membership_response().has_rlwe_response() ||
!response.private_set_membership_response()
.rlwe_response()
.has_query_response()) {
LOG(ERROR) << "PSM error: empty query RLWE response";
StoreErrorAndStop();
return;
}
const psm_rlwe::PrivateMembershipRlweQueryResponse query_response =
response.private_set_membership_response()
.rlwe_response()
.query_response();
auto status_or_responses =
psm_rlwe_client_->ProcessResponse(query_response);
if (!status_or_responses.ok()) {
// If the RLWE query response hasn't processed successfully, then
// report the error and stop the protocol.
LOG(ERROR) << "PSM error: unexpected internal logic error during "
"processing the "
"RLWE query response";
StoreErrorAndStop();
return;
}
LOG(WARNING) << "PSM query request completed successfully";
base::UmaHistogramEnumeration(kUMAPsmRequestStatus,
PsmStatus::kSuccessfulDetermination);
RecordPsmSuccessTimeHistogram();
// The RLWE query response has been processed successfully. Extract
// the membership response, and report the result.
psm_rlwe::MembershipResponseMap membership_responses_map =
std::move(status_or_responses).value();
private_membership::MembershipResponse membership_response =
membership_responses_map.Get(psm_rlwe_id_);
LOG(WARNING) << "PSM determination successful. Identifier "
<< (membership_response.is_member() ? "" : "not ")
<< "present on the server";
// Reset the |psm_request_job_| to allow another call to
// CheckMembership.
psm_request_job_.reset();
// Stop the PSM timer.
psm_timeout_.Stop();
// Cache the decision in local_state, so that it is reused in case
// the device reboots before completing OOBE.
local_state_->SetBoolean(prefs::kShouldRetrieveDeviceState,
membership_response.is_member());
local_state_->CommitPendingWrite();
std::move(on_completion_callback_).Run();
return;
}
case DM_STATUS_REQUEST_FAILED: {
LOG(ERROR)
<< "PSM error: RLWE query request failed due to connection error";
StoreErrorAndStop();
return;
}
default: {
LOG(ERROR)
<< "PSM error: RLWE query request failed due to server error";
StoreErrorAndStop();
return;
}
}
}
// Returns a job config that has TYPE_PSM_REQUEST as job type and |callback|
// will be executed on completion.
std::unique_ptr<DMServerJobConfiguration> CreatePsmRequestJobConfiguration(
DMServerJobConfiguration::Callback callback) {
return std::make_unique<DMServerJobConfiguration>(
device_management_service_,
DeviceManagementService::JobConfiguration::
TYPE_PSM_HAS_DEVICE_STATE_REQUEST,
random_device_id_,
/*critical=*/true, DMAuth::NoAuth(),
/*oauth_token=*/base::nullopt, url_loader_factory_,
std::move(callback));
}
// Record UMA histogram for timing of successful PSM request.
void RecordPsmSuccessTimeHistogram() {
// These values determine bucketing of the histogram, they should not be
// changed.
static const base::TimeDelta kMin = base::TimeDelta::FromMilliseconds(1);
static const base::TimeDelta kMax = base::TimeDelta::FromSeconds(25);
static const int kBuckets = 50;
base::TimeTicks now = base::TimeTicks::Now();
if (!time_start_.is_null()) {
base::TimeDelta delta = now - time_start_;
base::UmaHistogramCustomTimes(kUMAPsmSuccessTime, delta, kMin, kMax,
kBuckets);
}
}
// PSM RLWE client, used for preparing PSM requests and parsing PSM responses.
std::unique_ptr<psm_rlwe::PrivateMembershipRlweClient> psm_rlwe_client_;
// Randomly generated device id for the PSM requests.
std::string random_device_id_;
// The loader factory to use to perform PSM requests.
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
// Unowned by PsmHelper. Its used to communicate with the device management
// service.
DeviceManagementService* device_management_service_;
// Its being used for both PSM requests e.g. RLWE OPRF request and RLWE query
// request.
std::unique_ptr<DeviceManagementService::Job> psm_request_job_;
// Callback will be triggered upon completing of the protocol.
CompletionCallback on_completion_callback_;
// PrefService where the PSM protocol result is cached.
PrefService* const local_state_;
// PSM identifier, which is going to be used while preparing the PSM requests.
psm_rlwe::RlwePlaintextId psm_rlwe_id_;
// Indicates whether there was previously any error occurred while running
// PSM protocol.
bool has_psm_error_ = false;
// A timer that puts a hard limit on the maximum time to wait for PSM
// protocol.
base::OneShotTimer psm_timeout_;
// The time when the PSM request started.
base::TimeTicks time_start_;
// A sequence checker to prevent the race condition of having the possibility
// of the destructor being called and any of the callbacks.
SEQUENCE_CHECKER(sequence_checker_);
};
namespace {
// Provides device identifier for Forced Re-Enrollment (FRE), where the
// server-backed state key is used.
class DeviceIdentifierProviderFRE
: public AutoEnrollmentClientImpl::DeviceIdentifierProvider {
public:
explicit DeviceIdentifierProviderFRE(
const std::string& server_backed_state_key) {
CHECK(!server_backed_state_key.empty());
server_backed_state_key_hash_ =
crypto::SHA256HashString(server_backed_state_key);
}
EnrollmentCheckType GetEnrollmentCheckType() const override {
return em::DeviceAutoEnrollmentRequest::ENROLLMENT_CHECK_TYPE_FRE;
}
const std::string& GetIdHash() const override {
return server_backed_state_key_hash_;
}
private:
// SHA-256 digest of the stable identifier.
std::string server_backed_state_key_hash_;
};
// Provides device identifier for Forced Initial Enrollment, where the brand
// code and serial number is used.
class DeviceIdentifierProviderInitialEnrollment
: public AutoEnrollmentClientImpl::DeviceIdentifierProvider {
public:
DeviceIdentifierProviderInitialEnrollment(
const std::string& device_serial_number,
const std::string& device_brand_code) {
CHECK(!device_serial_number.empty());
CHECK(!device_brand_code.empty());
// The hash for initial enrollment is the first 8 bytes of
// SHA256(<brnad_code>_<serial_number>).
id_hash_ =
crypto::SHA256HashString(device_brand_code + "_" + device_serial_number)
.substr(0, 8);
}
EnrollmentCheckType GetEnrollmentCheckType() const override {
return em::DeviceAutoEnrollmentRequest::
ENROLLMENT_CHECK_TYPE_FORCED_ENROLLMENT;
}
const std::string& GetIdHash() const override { return id_hash_; }
private:
// 8-byte Hash built from serial number and brand code passed to the
// constructor.
std::string id_hash_;
};
// Handles DeviceInitialEnrollmentStateRequest /
// DeviceInitialEnrollmentStateResponse for Forced Initial Enrollment.
class StateDownloadMessageProcessorInitialEnrollment
: public AutoEnrollmentClientImpl::StateDownloadMessageProcessor {
public:
StateDownloadMessageProcessorInitialEnrollment(
const std::string& device_serial_number,
const std::string& device_brand_code)
: device_serial_number_(device_serial_number),
device_brand_code_(device_brand_code) {}
DeviceManagementService::JobConfiguration::JobType GetJobType()
const override {
return DeviceManagementService::JobConfiguration::
TYPE_INITIAL_ENROLLMENT_STATE_RETRIEVAL;
}
void FillRequest(em::DeviceManagementRequest* request) override {
auto* inner_request =
request->mutable_device_initial_enrollment_state_request();
inner_request->set_brand_code(device_brand_code_);
inner_request->set_serial_number(device_serial_number_);
}
base::Optional<ParsedResponse> ParseResponse(
const em::DeviceManagementResponse& response) override {
if (!response.has_device_initial_enrollment_state_response()) {
LOG(ERROR) << "Server failed to provide initial enrollment response.";
return base::nullopt;
}
return ParseInitialEnrollmentStateResponse(
response.device_initial_enrollment_state_response());
}
static base::Optional<ParsedResponse> ParseInitialEnrollmentStateResponse(
const em::DeviceInitialEnrollmentStateResponse& state_response) {
StateDownloadMessageProcessor::ParsedResponse parsed_response;
if (state_response.has_initial_enrollment_mode()) {
parsed_response.restore_mode = ConvertInitialEnrollmentMode(
state_response.initial_enrollment_mode());
} else {
// Unknown initial enrollment mode - treat as no enrollment.
parsed_response.restore_mode.clear();
}
if (state_response.has_management_domain())
parsed_response.management_domain = state_response.management_domain();
if (state_response.has_is_license_packaged_with_device()) {
parsed_response.is_license_packaged_with_device =
state_response.is_license_packaged_with_device();
}
if (state_response.has_disabled_state()) {
parsed_response.disabled_message =
state_response.disabled_state().message();
}
// Logging as "WARNING" to make sure it's preserved in the logs.
LOG(WARNING) << "Received initial_enrollment_mode="
<< state_response.initial_enrollment_mode() << " ("
<< parsed_response.restore_mode << "). "
<< (state_response.is_license_packaged_with_device()
? "Device has a packaged license for management."
: "No packaged license.");
return parsed_response;
}
private:
// Serial number of the device.
std::string device_serial_number_;
// 4-character brand code of the device.
std::string device_brand_code_;
};
// Handles DeviceStateRetrievalRequest / DeviceStateRetrievalResponse for
// Forced Re-Enrollment (FRE).
class StateDownloadMessageProcessorFRE
: public AutoEnrollmentClientImpl::StateDownloadMessageProcessor {
public:
explicit StateDownloadMessageProcessorFRE(
const std::string& server_backed_state_key)
: server_backed_state_key_(server_backed_state_key) {}
DeviceManagementService::JobConfiguration::JobType GetJobType()
const override {
return DeviceManagementService::JobConfiguration::
TYPE_DEVICE_STATE_RETRIEVAL;
}
void FillRequest(em::DeviceManagementRequest* request) override {
request->mutable_device_state_retrieval_request()
->set_server_backed_state_key(server_backed_state_key_);
}
base::Optional<ParsedResponse> ParseResponse(
const em::DeviceManagementResponse& response) override {
if (!response.has_device_state_retrieval_response()) {
LOG(ERROR) << "Server failed to provide auto-enrollment response.";
return base::nullopt;
}
const em::DeviceStateRetrievalResponse& state_response =
response.device_state_retrieval_response();
const auto restore_mode = state_response.restore_mode();
if (restore_mode == em::DeviceStateRetrievalResponse::RESTORE_MODE_NONE &&
state_response.has_initial_state_response()) {
// Logging as "WARNING" to make sure it's preserved in the logs.
LOG(WARNING) << "Received restore_mode=" << restore_mode << " ("
<< ConvertRestoreMode(restore_mode) << ")"
<< " . Parsing included initial state response.";
return StateDownloadMessageProcessorInitialEnrollment::
ParseInitialEnrollmentStateResponse(
state_response.initial_state_response());
} else {
StateDownloadMessageProcessor::ParsedResponse parsed_response;
parsed_response.restore_mode = ConvertRestoreMode(restore_mode);
if (state_response.has_management_domain())
parsed_response.management_domain = state_response.management_domain();
if (state_response.has_disabled_state()) {
parsed_response.disabled_message =
state_response.disabled_state().message();
}
// Package license is not available during the re-enrollment
parsed_response.is_license_packaged_with_device.reset();
// Logging as "WARNING" to make sure it's preserved in the logs.
LOG(WARNING) << "Received restore_mode=" << restore_mode << " ("
<< parsed_response.restore_mode << ").";
return parsed_response;
}
}
private:
// Stable state key.
std::string server_backed_state_key_;
};
} // namespace
AutoEnrollmentClientImpl::FactoryImpl::FactoryImpl() {}
AutoEnrollmentClientImpl::FactoryImpl::~FactoryImpl() {}
std::unique_ptr<AutoEnrollmentClient>
AutoEnrollmentClientImpl::FactoryImpl::CreateForFRE(
const ProgressCallback& progress_callback,
DeviceManagementService* device_management_service,
PrefService* local_state,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const std::string& server_backed_state_key,
int power_initial,
int power_limit) {
return base::WrapUnique(new AutoEnrollmentClientImpl(
progress_callback, device_management_service, local_state,
url_loader_factory,
std::make_unique<DeviceIdentifierProviderFRE>(server_backed_state_key),
std::make_unique<StateDownloadMessageProcessorFRE>(
server_backed_state_key),
power_initial, power_limit,
/*power_outdated_server_detect=*/base::nullopt, kUMAHashDanceSuffixFRE,
/*private_set_membership_helper=*/nullptr));
}
std::unique_ptr<AutoEnrollmentClient>
AutoEnrollmentClientImpl::FactoryImpl::CreateForInitialEnrollment(
const ProgressCallback& progress_callback,
DeviceManagementService* device_management_service,
PrefService* local_state,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const std::string& device_serial_number,
const std::string& device_brand_code,
int power_initial,
int power_limit,
int power_outdated_server_detect) {
return base::WrapUnique(new AutoEnrollmentClientImpl(
progress_callback, device_management_service, local_state,
url_loader_factory,
std::make_unique<DeviceIdentifierProviderInitialEnrollment>(
device_serial_number, device_brand_code),
std::make_unique<StateDownloadMessageProcessorInitialEnrollment>(
device_serial_number, device_brand_code),
power_initial, power_limit,
base::make_optional(power_outdated_server_detect),
kUMAHashDanceSuffixInitialEnrollment,
ash::AutoEnrollmentController::IsPsmEnabled()
? std::make_unique<PsmHelper>(
device_management_service, url_loader_factory, local_state,
ConstructDeviceRlweId(device_serial_number, device_brand_code))
: nullptr));
}
AutoEnrollmentClientImpl::~AutoEnrollmentClientImpl() {
content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(this);
}
// static
void AutoEnrollmentClientImpl::RegisterPrefs(PrefRegistrySimple* registry) {
registry->RegisterBooleanPref(prefs::kShouldAutoEnroll, false);
registry->RegisterIntegerPref(prefs::kAutoEnrollmentPowerLimit, -1);
registry->RegisterBooleanPref(prefs::kShouldRetrieveDeviceState, false);
}
void AutoEnrollmentClientImpl::Start() {
// (Re-)register the network change observer.
content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(this);
content::GetNetworkConnectionTracker()->AddNetworkConnectionObserver(this);
// Drop the previous job and reset state.
request_job_.reset();
hash_dance_time_start_ = base::TimeTicks();
state_ = AUTO_ENROLLMENT_STATE_PENDING;
modulus_updates_received_ = 0;
has_server_state_ = false;
device_state_available_ = false;
NextStep();
}
void AutoEnrollmentClientImpl::Retry() {
RetryStep();
}
void AutoEnrollmentClientImpl::CancelAndDeleteSoon() {
// Check if neither Hash dance request i.e. DeviceAutoEnrollmentRequest nor
// DeviceStateRetrievalRequest is in progress.
if (!request_job_) {
// 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::TimeTicks::Now();
progress_callback_.Reset();
}
}
std::string AutoEnrollmentClientImpl::device_id() const {
return device_id_;
}
AutoEnrollmentState AutoEnrollmentClientImpl::state() const {
return state_;
}
void AutoEnrollmentClientImpl::OnConnectionChanged(
network::mojom::ConnectionType type) {
if (type != network::mojom::ConnectionType::CONNECTION_NONE &&
!progress_callback_.is_null()) {
RetryStep();
}
}
AutoEnrollmentClientImpl::AutoEnrollmentClientImpl(
const ProgressCallback& callback,
DeviceManagementService* service,
PrefService* local_state,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
std::unique_ptr<DeviceIdentifierProvider> device_identifier_provider,
std::unique_ptr<StateDownloadMessageProcessor>
state_download_message_processor,
int power_initial,
int power_limit,
base::Optional<int> power_outdated_server_detect,
std::string uma_suffix,
std::unique_ptr<PsmHelper> private_set_membership_helper)
: progress_callback_(callback),
state_(AUTO_ENROLLMENT_STATE_IDLE),
has_server_state_(false),
device_state_available_(false),
device_id_(base::GenerateGUID()),
current_power_(power_initial),
power_limit_(power_limit),
power_outdated_server_detect_(power_outdated_server_detect),
modulus_updates_received_(0),
device_management_service_(service),
local_state_(local_state),
url_loader_factory_(url_loader_factory),
device_identifier_provider_(std::move(device_identifier_provider)),
state_download_message_processor_(
std::move(state_download_message_processor)),
psm_helper_(std::move(private_set_membership_helper)),
uma_suffix_(uma_suffix),
recorded_psm_hash_dance_comparison_(false) {
DCHECK_LE(current_power_, power_limit_);
DCHECK(!progress_callback_.is_null());
}
bool AutoEnrollmentClientImpl::GetCachedDecision() {
const PrefService::Preference* has_server_state_pref =
local_state_->FindPreference(prefs::kShouldAutoEnroll);
const PrefService::Preference* previous_limit_pref =
local_state_->FindPreference(prefs::kAutoEnrollmentPowerLimit);
if (!has_server_state_pref || has_server_state_pref->IsDefaultValue() ||
!has_server_state_pref->GetValue()->is_bool() || !previous_limit_pref ||
previous_limit_pref->IsDefaultValue() ||
!previous_limit_pref->GetValue()->is_int() ||
power_limit_ > previous_limit_pref->GetValue()->GetInt()) {
return false;
}
has_server_state_ = has_server_state_pref->GetValue()->GetBool();
return true;
}
bool AutoEnrollmentClientImpl::RetryStep() {
if (PsmRetryStep())
return true;
// If there is a pending request job, let it finish.
if (request_job_)
return true;
if (GetCachedDecision()) {
VLOG(1) << "Cached: has_state=" << has_server_state_;
// The bucket download check has completed already. If it came back
// positive, then device state should be (re-)downloaded.
if (has_server_state_) {
if (!device_state_available_) {
SendDeviceStateRequest();
return true;
}
}
} else {
// Start bucket download.
SendBucketDownloadRequest();
return true;
}
return false;
}
bool AutoEnrollmentClientImpl::PsmRetryStep() {
// Don't retry if the protocol is disabled, or an error occurred while
// executing the protocol.
if (!psm_helper_ || psm_helper_->HasPsmError()) {
return false;
}
// If the PSM protocol is in progress, signal to the caller
// that nothing else needs to be done.
if (psm_helper_->IsCheckMembershipInProgress())
return true;
const base::Optional<bool> private_set_membership_server_state =
psm_helper_->GetPsmCachedDecision();
if (private_set_membership_server_state.has_value()) {
LOG(WARNING) << "PSM Cached: psm_server_state="
<< private_set_membership_server_state.value();
return false;
} else {
psm_helper_->CheckMembership(base::BindOnce(
&AutoEnrollmentClientImpl::RetryStep, base::Unretained(this)));
return true;
}
}
void AutoEnrollmentClientImpl::SetPsmRlweClientForTesting(
std::unique_ptr<psm_rlwe::PrivateMembershipRlweClient> psm_rlwe_client,
const psm_rlwe::RlwePlaintextId& psm_rlwe_id) {
if (!psm_helper_)
return;
DCHECK(psm_rlwe_client);
psm_helper_->SetRlweClientAndIdForTesting(std::move(psm_rlwe_client),
std::move(psm_rlwe_id));
}
void AutoEnrollmentClientImpl::ReportProgress(AutoEnrollmentState state) {
state_ = state;
// If hash dance finished with an error or result, record comparison with PSM.
// Note that hash dance might be retried but for recording we only care about
// the first attempt. If |psm_helper_| is non-null, a PSM request has been
// made at this point because it is executed before hash dance.
const bool has_hash_dance_result = (state != AUTO_ENROLLMENT_STATE_IDLE &&
state != AUTO_ENROLLMENT_STATE_PENDING);
if (psm_helper_ && !recorded_psm_hash_dance_comparison_ &&
has_hash_dance_result) {
RecordPsmHashDanceComparison();
}
if (progress_callback_.is_null()) {
base::ThreadTaskRunnerHandle::Get()->DeleteSoon(FROM_HERE, this);
} else {
progress_callback_.Run(state_);
}
}
void AutoEnrollmentClientImpl::NextStep() {
if (RetryStep())
return;
// Protocol finished successfully, report result.
const DeviceStateMode device_state_mode = GetDeviceStateMode();
switch (device_state_mode) {
case RESTORE_MODE_NONE:
ReportProgress(AUTO_ENROLLMENT_STATE_NO_ENROLLMENT);
break;
case RESTORE_MODE_DISABLED:
ReportProgress(AUTO_ENROLLMENT_STATE_DISABLED);
break;
case RESTORE_MODE_REENROLLMENT_REQUESTED:
case RESTORE_MODE_REENROLLMENT_ENFORCED:
case INITIAL_MODE_ENROLLMENT_ENFORCED:
ReportProgress(AUTO_ENROLLMENT_STATE_TRIGGER_ENROLLMENT);
break;
case RESTORE_MODE_REENROLLMENT_ZERO_TOUCH:
case INITIAL_MODE_ENROLLMENT_ZERO_TOUCH:
ReportProgress(AUTO_ENROLLMENT_STATE_TRIGGER_ZERO_TOUCH);
break;
}
}
void AutoEnrollmentClientImpl::SendBucketDownloadRequest() {
// Start the Hash dance timer during the first attempt.
if (hash_dance_time_start_.is_null())
hash_dance_time_start_ = base::TimeTicks::Now();
std::string id_hash = device_identifier_provider_->GetIdHash();
// Currently AutoEnrollmentClientImpl supports working with hashes that are at
// least 8 bytes long. If this is reduced, the computation of the remainder
// must also be adapted to handle the case of a shorter hash gracefully.
DCHECK_GE(id_hash.size(), 8u);
uint64_t remainder = 0;
const size_t last_byte_index = id_hash.size() - 1;
for (int i = 0; 8 * i < current_power_; ++i) {
uint64_t byte = id_hash[last_byte_index - i] & 0xff;
remainder = remainder | (byte << (8 * i));
}
remainder = remainder & ((UINT64_C(1) << current_power_) - 1);
ReportProgress(AUTO_ENROLLMENT_STATE_PENDING);
// Record the time when the bucket download request is started. Note that the
// time may be set multiple times. This is fine, only the last request is the
// one where the hash bucket is actually downloaded.
time_start_bucket_download_ = base::TimeTicks::Now();
VLOG(1) << "Request bucket #" << remainder;
std::unique_ptr<DMServerJobConfiguration> config = std::make_unique<
DMServerJobConfiguration>(
device_management_service_,
policy::DeviceManagementService::JobConfiguration::TYPE_AUTO_ENROLLMENT,
device_id_,
/*critical=*/false, DMAuth::NoAuth(),
/*oauth_token=*/base::nullopt, url_loader_factory_,
base::BindOnce(
&AutoEnrollmentClientImpl::HandleRequestCompletion,
base::Unretained(this),
&AutoEnrollmentClientImpl::OnBucketDownloadRequestCompletion));
em::DeviceAutoEnrollmentRequest* request =
config->request()->mutable_auto_enrollment_request();
request->set_remainder(remainder);
request->set_modulus(INT64_C(1) << current_power_);
request->set_enrollment_check_type(
device_identifier_provider_->GetEnrollmentCheckType());
request_job_ = device_management_service_->CreateJob(std::move(config));
}
void AutoEnrollmentClientImpl::SendDeviceStateRequest() {
ReportProgress(AUTO_ENROLLMENT_STATE_PENDING);
std::unique_ptr<DMServerJobConfiguration> config =
std::make_unique<DMServerJobConfiguration>(
device_management_service_,
state_download_message_processor_->GetJobType(), device_id_,
/*critical=*/false, DMAuth::NoAuth(),
/*oauth_token=*/base::nullopt, url_loader_factory_,
base::BindRepeating(
&AutoEnrollmentClientImpl::HandleRequestCompletion,
base::Unretained(this),
&AutoEnrollmentClientImpl::OnDeviceStateRequestCompletion));
state_download_message_processor_->FillRequest(config->request());
request_job_ = device_management_service_->CreateJob(std::move(config));
}
void AutoEnrollmentClientImpl::HandleRequestCompletion(
RequestCompletionHandler handler,
policy::DeviceManagementService::Job* job,
DeviceManagementStatus status,
int net_error,
const em::DeviceManagementResponse& response) {
base::UmaHistogramSparse(kUMAHashDanceRequestStatus + uma_suffix_, status);
if (status != DM_STATUS_SUCCESS) {
LOG(ERROR) << "Auto enrollment error: " << status;
if (status == DM_STATUS_REQUEST_FAILED)
base::UmaHistogramSparse(kUMAHashDanceNetworkErrorCode + uma_suffix_,
-net_error);
request_job_.reset();
// Abort if CancelAndDeleteSoon has been called meanwhile.
if (progress_callback_.is_null()) {
base::ThreadTaskRunnerHandle::Get()->DeleteSoon(FROM_HERE, this);
} else {
ReportProgress(status == DM_STATUS_REQUEST_FAILED
? AUTO_ENROLLMENT_STATE_CONNECTION_ERROR
: AUTO_ENROLLMENT_STATE_SERVER_ERROR);
}
return;
}
bool progress =
(this->*handler)(request_job_.get(), status, net_error, response);
request_job_.reset();
if (progress)
NextStep();
else
ReportProgress(AUTO_ENROLLMENT_STATE_SERVER_ERROR);
}
bool AutoEnrollmentClientImpl::OnBucketDownloadRequestCompletion(
policy::DeviceManagementService::Job* job,
DeviceManagementStatus status,
int net_error,
const em::DeviceManagementResponse& response) {
bool progress = false;
const em::DeviceAutoEnrollmentResponse& enrollment_response =
response.auto_enrollment_response();
if (!response.has_auto_enrollment_response()) {
LOG(ERROR) << "Server failed to provide auto-enrollment response.";
} else if (enrollment_response.has_expected_modulus()) {
// Server is asking us to retry with a different modulus.
modulus_updates_received_++;
int64_t modulus = enrollment_response.expected_modulus();
int power = NextPowerOf2(modulus);
if ((INT64_C(1) << power) != modulus) {
LOG(ERROR) << "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 (modulus_updates_received_ >= 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_outdated_server_detect_.has_value() &&
power >= power_outdated_server_detect_.value()) {
LOG(ERROR) << "Skipping auto enrollment: The server was detected as "
<< "outdated (power=" << power
<< ", power_outdated_server_detect="
<< power_outdated_server_detect_.value() << ").";
has_server_state_ = false;
// Cache the decision in local_state, so that it is reused in case
// the device reboots before completing OOBE. Note that this does not
// disable Forced Re-Enrollment for this device, because local state will
// be empty after the device is wiped.
local_state_->SetBoolean(prefs::kShouldAutoEnroll, false);
local_state_->SetInteger(prefs::kAutoEnrollmentPowerLimit, power_limit_);
local_state_->CommitPendingWrite();
return true;
} 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 <= current_power_) {
LOG(WARNING) << "Auto enrollment: the server asked to use a modulus ("
<< power << ") that isn't larger than the first used ("
<< current_power_ << "). Retrying anyway.";
}
// Remember this value, so that eventual retries start with the correct
// modulus.
current_power_ = power;
return true;
}
} else {
// Server should have sent down a list of hashes to try.
has_server_state_ = IsIdHashInProtobuf(enrollment_response.hashes());
// Cache the current decision in local_state, so that it is reused in case
// the device reboots before enrolling.
local_state_->SetBoolean(prefs::kShouldAutoEnroll, has_server_state_);
local_state_->SetInteger(prefs::kAutoEnrollmentPowerLimit, power_limit_);
local_state_->CommitPendingWrite();
VLOG(1) << "Received has_state=" << has_server_state_;
progress = true;
// Report timing if hash dance finished successfully and if the caller is
// still interested in the result.
if (!progress_callback_.is_null())
RecordHashDanceSuccessTimeHistogram();
}
// Bucket download done, update UMA.
UpdateBucketDownloadTimingHistograms();
return progress;
}
bool AutoEnrollmentClientImpl::OnDeviceStateRequestCompletion(
policy::DeviceManagementService::Job* job,
DeviceManagementStatus status,
int net_error,
const em::DeviceManagementResponse& response) {
base::Optional<StateDownloadMessageProcessor::ParsedResponse>
parsed_response_opt;
parsed_response_opt =
state_download_message_processor_->ParseResponse(response);
if (!parsed_response_opt)
return false;
StateDownloadMessageProcessor::ParsedResponse parsed_response =
std::move(parsed_response_opt.value());
{
DictionaryPrefUpdate dict(local_state_, prefs::kServerBackedDeviceState);
UpdateDict(dict.Get(), kDeviceStateManagementDomain,
parsed_response.management_domain.has_value(),
std::make_unique<base::Value>(
parsed_response.management_domain.value_or(std::string())));
UpdateDict(dict.Get(), kDeviceStateMode,
!parsed_response.restore_mode.empty(),
std::make_unique<base::Value>(parsed_response.restore_mode));
UpdateDict(dict.Get(), kDeviceStateDisabledMessage,
parsed_response.disabled_message.has_value(),
std::make_unique<base::Value>(
parsed_response.disabled_message.value_or(std::string())));
UpdateDict(
dict.Get(), kDeviceStatePackagedLicense,
parsed_response.is_license_packaged_with_device.has_value(),
std::make_unique<base::Value>(
parsed_response.is_license_packaged_with_device.value_or(false)));
}
local_state_->CommitPendingWrite();
device_state_available_ = true;
return true;
}
bool AutoEnrollmentClientImpl::IsIdHashInProtobuf(
const google::protobuf::RepeatedPtrField<std::string>& hashes) {
std::string id_hash = device_identifier_provider_->GetIdHash();
for (int i = 0; i < hashes.size(); ++i) {
if (hashes.Get(i) == id_hash)
return true;
}
return false;
}
void AutoEnrollmentClientImpl::UpdateBucketDownloadTimingHistograms() {
// These values determine bucketing of the histogram, they should not be
// changed.
// The minimum 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::TimeTicks now = base::TimeTicks::Now();
if (!hash_dance_time_start_.is_null()) {
base::TimeDelta delta = now - hash_dance_time_start_;
base::UmaHistogramCustomTimes(kUMAHashDanceProtocolTime + uma_suffix_,
delta, kMin, kMax, kBuckets);
}
if (!time_start_bucket_download_.is_null()) {
base::TimeDelta delta = now - time_start_bucket_download_;
base::UmaHistogramCustomTimes(kUMAHashDanceBucketDownloadTime + uma_suffix_,
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.
base::UmaHistogramCustomTimes(kUMAHashDanceExtraTime + uma_suffix_, delta,
kMin, kMax, kBuckets);
}
void AutoEnrollmentClientImpl::RecordHashDanceSuccessTimeHistogram() {
// These values determine bucketing of the histogram, they should not be
// changed.
static const base::TimeDelta kMin = base::TimeDelta::FromMilliseconds(1);
static const base::TimeDelta kMax = base::TimeDelta::FromSeconds(25);
static const int kBuckets = 50;
base::TimeTicks now = base::TimeTicks::Now();
if (!hash_dance_time_start_.is_null()) {
base::TimeDelta delta = now - hash_dance_time_start_;
base::UmaHistogramCustomTimes(kUMAHashDanceSuccessTime + uma_suffix_, delta,
kMin, kMax, kBuckets);
}
}
void AutoEnrollmentClientImpl::RecordPsmHashDanceComparison() {
// PSM timeout is enforced in the helper class. This method should only be
// called after PSM request finished or ran into timeout.
DCHECK(psm_helper_);
DCHECK(!psm_helper_->IsCheckMembershipInProgress());
// Make sure to only record once per instance.
recorded_psm_hash_dance_comparison_ = true;
bool psm_error = psm_helper_->HasPsmError();
bool hash_dance_decision = has_server_state_;
bool hash_dance_error = false;
switch (state_) {
case AUTO_ENROLLMENT_STATE_TRIGGER_ENROLLMENT:
case AUTO_ENROLLMENT_STATE_NO_ENROLLMENT:
case AUTO_ENROLLMENT_STATE_TRIGGER_ZERO_TOUCH:
case AUTO_ENROLLMENT_STATE_DISABLED:
hash_dance_error = false;
break;
case AUTO_ENROLLMENT_STATE_CONNECTION_ERROR:
case AUTO_ENROLLMENT_STATE_SERVER_ERROR:
hash_dance_error = true;
break;
// This method should only be called if hash dance finished.
case AUTO_ENROLLMENT_STATE_IDLE:
case AUTO_ENROLLMENT_STATE_PENDING:
default:
NOTREACHED();
}
auto comparison = PsmHashDanceComparison::kEqualResults;
if (!hash_dance_error && !psm_error) {
base::Optional<bool> psm_decision = psm_helper_->GetPsmCachedDecision();
// There was no error and this function is only invoked after PSM has been
// performed, so there must be a decision.
DCHECK(psm_decision.has_value());
comparison = (hash_dance_decision == psm_decision.value())
? PsmHashDanceComparison::kEqualResults
: PsmHashDanceComparison::kDifferentResults;
if (hash_dance_decision != psm_decision.value()) {
// Reports the different values of the protocols, after both have finished
// executing successfully.
auto different_protocols_results_comparison =
(psm_decision.value()
? PsmHashDanceDifferentResultsComparison::kPsmTrueHashDanceFalse
: PsmHashDanceDifferentResultsComparison::
kHashDanceTruePsmFalse);
base::UmaHistogramEnumeration(kUMAPsmHashDanceDifferentResultsComparison,
different_protocols_results_comparison);
}
} else if (hash_dance_error && !psm_error) {
comparison = PsmHashDanceComparison::kPSMSuccessHashDanceError;
} else if (!hash_dance_error && psm_error) {
comparison = PsmHashDanceComparison::kPSMErrorHashDanceSuccess;
} else {
comparison = PsmHashDanceComparison::kBothError;
}
base::UmaHistogramEnumeration(kUMAPsmHashDanceComparison, comparison);
}
} // namespace policy