blob: 62b273d9c00131936b9325116b60e8dd7763635b [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 "chromeos/services/device_sync/cryptauth_enrollment_manager_impl.h"
#include <memory>
#include <sstream>
#include <utility>
#include "base/base64url.h"
#include "base/memory/ptr_util.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "chromeos/components/multidevice/secure_message_delegate.h"
#include "chromeos/components/proximity_auth/logging/logging.h"
#include "chromeos/services/device_sync/cryptauth_enroller.h"
#include "chromeos/services/device_sync/pref_names.h"
#include "chromeos/services/device_sync/proto/enum_util.h"
#include "chromeos/services/device_sync/sync_scheduler_impl.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
namespace chromeos {
namespace device_sync {
namespace {
// The number of days that an enrollment is valid. Note that we try to refresh
// the enrollment well before this time elapses.
const int kValidEnrollmentPeriodDays = 45;
// The normal period between successful enrollments in days.
const int kEnrollmentRefreshPeriodDays = 30;
// A more aggressive period between enrollments to recover when the last
// enrollment fails, in minutes. This is a base time that increases for each
// subsequent failure.
const int kEnrollmentBaseRecoveryPeriodMinutes = 10;
// The bound on the amount to jitter the period between enrollments.
const double kEnrollmentMaxJitterRatio = 0.2;
// The value of the device_software_package field in the device info uploaded
// during enrollment. This value must be the same as the app id used for GCM
// registration.
const char kDeviceSoftwarePackage[] = "com.google.chrome.cryptauth";
std::unique_ptr<SyncScheduler> CreateSyncScheduler(
SyncScheduler::Delegate* delegate) {
return std::make_unique<SyncSchedulerImpl>(
delegate, base::TimeDelta::FromDays(kEnrollmentRefreshPeriodDays),
base::TimeDelta::FromMinutes(kEnrollmentBaseRecoveryPeriodMinutes),
kEnrollmentMaxJitterRatio, "CryptAuth Enrollment");
}
std::string GenerateSupportedFeaturesString(
const cryptauth::GcmDeviceInfo& info) {
std::stringstream ss;
ss << "[";
bool logged_feature = false;
for (int i = 0; i < info.supported_software_features_size(); ++i) {
logged_feature = true;
ss << info.supported_software_features(i) << ", ";
}
if (logged_feature)
ss.seekp(-2, ss.cur); // Remove last ", " from the stream.
ss << "]";
return ss.str();
}
} // namespace
// static
CryptAuthEnrollmentManagerImpl::Factory*
CryptAuthEnrollmentManagerImpl::Factory::factory_instance_ = nullptr;
// static
std::unique_ptr<CryptAuthEnrollmentManager>
CryptAuthEnrollmentManagerImpl::Factory::NewInstance(
base::Clock* clock,
std::unique_ptr<CryptAuthEnrollerFactory> enroller_factory,
std::unique_ptr<multidevice::SecureMessageDelegate> secure_message_delegate,
const cryptauth::GcmDeviceInfo& device_info,
CryptAuthGCMManager* gcm_manager,
PrefService* pref_service) {
if (!factory_instance_)
factory_instance_ = new Factory();
return factory_instance_->BuildInstance(
clock, std::move(enroller_factory), std::move(secure_message_delegate),
device_info, gcm_manager, pref_service);
}
// static
void CryptAuthEnrollmentManagerImpl::Factory::SetInstanceForTesting(
Factory* factory) {
factory_instance_ = factory;
}
CryptAuthEnrollmentManagerImpl::Factory::~Factory() = default;
std::unique_ptr<CryptAuthEnrollmentManager>
CryptAuthEnrollmentManagerImpl::Factory::BuildInstance(
base::Clock* clock,
std::unique_ptr<CryptAuthEnrollerFactory> enroller_factory,
std::unique_ptr<multidevice::SecureMessageDelegate> secure_message_delegate,
const cryptauth::GcmDeviceInfo& device_info,
CryptAuthGCMManager* gcm_manager,
PrefService* pref_service) {
return base::WrapUnique(new CryptAuthEnrollmentManagerImpl(
clock, std::move(enroller_factory), std::move(secure_message_delegate),
device_info, gcm_manager, pref_service));
}
CryptAuthEnrollmentManagerImpl::CryptAuthEnrollmentManagerImpl(
base::Clock* clock,
std::unique_ptr<CryptAuthEnrollerFactory> enroller_factory,
std::unique_ptr<multidevice::SecureMessageDelegate> secure_message_delegate,
const cryptauth::GcmDeviceInfo& device_info,
CryptAuthGCMManager* gcm_manager,
PrefService* pref_service)
: clock_(clock),
enroller_factory_(std::move(enroller_factory)),
secure_message_delegate_(std::move(secure_message_delegate)),
device_info_(device_info),
gcm_manager_(gcm_manager),
pref_service_(pref_service),
scheduler_(CreateSyncScheduler(this /* delegate */)),
weak_ptr_factory_(this) {}
CryptAuthEnrollmentManagerImpl::~CryptAuthEnrollmentManagerImpl() {
gcm_manager_->RemoveObserver(this);
}
void CryptAuthEnrollmentManagerImpl::Start() {
gcm_manager_->AddObserver(this);
bool is_recovering_from_failure =
pref_service_->GetBoolean(
prefs::kCryptAuthEnrollmentIsRecoveringFromFailure) ||
!IsEnrollmentValid();
base::Time last_successful_enrollment = GetLastEnrollmentTime();
base::TimeDelta elapsed_time_since_last_sync =
clock_->Now() - last_successful_enrollment;
scheduler_->Start(elapsed_time_since_last_sync,
is_recovering_from_failure
? SyncScheduler::Strategy::AGGRESSIVE_RECOVERY
: SyncScheduler::Strategy::PERIODIC_REFRESH);
}
void CryptAuthEnrollmentManagerImpl::ForceEnrollmentNow(
cryptauth::InvocationReason invocation_reason) {
// We store the invocation reason in a preference so that it can persist
// across browser restarts. If the sync fails, the next retry should still use
// this original reason instead of
// cryptauth::INVOCATION_REASON_FAILURE_RECOVERY.
pref_service_->SetInteger(prefs::kCryptAuthEnrollmentReason,
invocation_reason);
scheduler_->ForceSync();
}
bool CryptAuthEnrollmentManagerImpl::IsEnrollmentValid() const {
base::Time last_enrollment_time = GetLastEnrollmentTime();
return !last_enrollment_time.is_null() &&
(clock_->Now() - last_enrollment_time) <
base::TimeDelta::FromDays(kValidEnrollmentPeriodDays);
}
base::Time CryptAuthEnrollmentManagerImpl::GetLastEnrollmentTime() const {
return base::Time::FromDoubleT(pref_service_->GetDouble(
prefs::kCryptAuthEnrollmentLastEnrollmentTimeSeconds));
}
base::TimeDelta CryptAuthEnrollmentManagerImpl::GetTimeToNextAttempt() const {
return scheduler_->GetTimeToNextSync();
}
bool CryptAuthEnrollmentManagerImpl::IsEnrollmentInProgress() const {
return scheduler_->GetSyncState() ==
SyncScheduler::SyncState::SYNC_IN_PROGRESS;
}
bool CryptAuthEnrollmentManagerImpl::IsRecoveringFromFailure() const {
return scheduler_->GetStrategy() ==
SyncScheduler::Strategy::AGGRESSIVE_RECOVERY;
}
void CryptAuthEnrollmentManagerImpl::OnEnrollmentFinished(bool success) {
if (success) {
pref_service_->SetDouble(
prefs::kCryptAuthEnrollmentLastEnrollmentTimeSeconds,
clock_->Now().ToDoubleT());
pref_service_->SetInteger(prefs::kCryptAuthEnrollmentReason,
cryptauth::INVOCATION_REASON_UNKNOWN);
}
pref_service_->SetBoolean(prefs::kCryptAuthEnrollmentIsRecoveringFromFailure,
!success);
sync_request_->OnDidComplete(success);
cryptauth_enroller_.reset();
sync_request_.reset();
NotifyEnrollmentFinished(success);
}
std::string CryptAuthEnrollmentManagerImpl::GetUserPublicKey() const {
std::string public_key;
if (!base::Base64UrlDecode(
pref_service_->GetString(prefs::kCryptAuthEnrollmentUserPublicKey),
base::Base64UrlDecodePolicy::REQUIRE_PADDING, &public_key)) {
PA_LOG(ERROR) << "Invalid public key stored in user prefs.";
return std::string();
}
return public_key;
}
std::string CryptAuthEnrollmentManagerImpl::GetUserPrivateKey() const {
std::string private_key;
if (!base::Base64UrlDecode(
pref_service_->GetString(prefs::kCryptAuthEnrollmentUserPrivateKey),
base::Base64UrlDecodePolicy::REQUIRE_PADDING, &private_key)) {
PA_LOG(ERROR) << "Invalid private key stored in user prefs.";
return std::string();
}
return private_key;
}
void CryptAuthEnrollmentManagerImpl::SetSyncSchedulerForTest(
std::unique_ptr<SyncScheduler> sync_scheduler) {
scheduler_ = std::move(sync_scheduler);
}
void CryptAuthEnrollmentManagerImpl::OnGCMRegistrationResult(bool success) {
if (!sync_request_)
return;
PA_LOG(VERBOSE) << "GCM registration for CryptAuth Enrollment completed: "
<< success;
if (success)
DoCryptAuthEnrollment();
else
OnEnrollmentFinished(false);
}
void CryptAuthEnrollmentManagerImpl::OnKeyPairGenerated(
const std::string& public_key,
const std::string& private_key) {
if (!public_key.empty() && !private_key.empty()) {
PA_LOG(VERBOSE) << "Key pair generated for CryptAuth enrollment";
// Store the keypair in Base64 format because pref values require readable
// string values.
std::string public_key_b64, private_key_b64;
base::Base64UrlEncode(public_key,
base::Base64UrlEncodePolicy::INCLUDE_PADDING,
&public_key_b64);
base::Base64UrlEncode(private_key,
base::Base64UrlEncodePolicy::INCLUDE_PADDING,
&private_key_b64);
pref_service_->SetString(prefs::kCryptAuthEnrollmentUserPublicKey,
public_key_b64);
pref_service_->SetString(prefs::kCryptAuthEnrollmentUserPrivateKey,
private_key_b64);
DoCryptAuthEnrollment();
} else {
OnEnrollmentFinished(false);
}
}
void CryptAuthEnrollmentManagerImpl::OnReenrollMessage() {
ForceEnrollmentNow(cryptauth::INVOCATION_REASON_SERVER_INITIATED);
}
void CryptAuthEnrollmentManagerImpl::OnSyncRequested(
std::unique_ptr<SyncScheduler::SyncRequest> sync_request) {
NotifyEnrollmentStarted();
sync_request_ = std::move(sync_request);
if (gcm_manager_->GetRegistrationId().empty() ||
pref_service_->GetInteger(prefs::kCryptAuthEnrollmentReason) ==
cryptauth::INVOCATION_REASON_MANUAL) {
gcm_manager_->RegisterWithGCM();
} else {
DoCryptAuthEnrollment();
}
}
void CryptAuthEnrollmentManagerImpl::DoCryptAuthEnrollment() {
if (GetUserPublicKey().empty() || GetUserPrivateKey().empty()) {
secure_message_delegate_->GenerateKeyPair(
base::Bind(&CryptAuthEnrollmentManagerImpl::OnKeyPairGenerated,
weak_ptr_factory_.GetWeakPtr()));
} else {
DoCryptAuthEnrollmentWithKeys();
}
}
void CryptAuthEnrollmentManagerImpl::DoCryptAuthEnrollmentWithKeys() {
DCHECK(sync_request_);
cryptauth::InvocationReason invocation_reason =
cryptauth::INVOCATION_REASON_UNKNOWN;
int reason_stored_in_prefs =
pref_service_->GetInteger(prefs::kCryptAuthEnrollmentReason);
if (cryptauth::InvocationReason_IsValid(reason_stored_in_prefs) &&
reason_stored_in_prefs != cryptauth::INVOCATION_REASON_UNKNOWN) {
invocation_reason =
static_cast<cryptauth::InvocationReason>(reason_stored_in_prefs);
} else if (GetLastEnrollmentTime().is_null()) {
invocation_reason = cryptauth::INVOCATION_REASON_INITIALIZATION;
} else if (!IsEnrollmentValid()) {
invocation_reason = cryptauth::INVOCATION_REASON_EXPIRATION;
} else if (scheduler_->GetStrategy() ==
SyncScheduler::Strategy::PERIODIC_REFRESH) {
invocation_reason = cryptauth::INVOCATION_REASON_PERIODIC;
} else if (scheduler_->GetStrategy() ==
SyncScheduler::Strategy::AGGRESSIVE_RECOVERY) {
invocation_reason = cryptauth::INVOCATION_REASON_FAILURE_RECOVERY;
}
// Fill in the current GCM registration id before enrolling, and explicitly
// make sure that the software package is the same as the GCM app id.
cryptauth::GcmDeviceInfo device_info(device_info_);
device_info.set_gcm_registration_id(gcm_manager_->GetRegistrationId());
device_info.set_device_software_package(kDeviceSoftwarePackage);
std::string public_key_b64;
base::Base64UrlEncode(GetUserPublicKey(),
base::Base64UrlEncodePolicy::INCLUDE_PADDING,
&public_key_b64);
PA_LOG(VERBOSE) << "Making enrollment:\n"
<< " public_key: " << public_key_b64 << "\n"
<< " invocation_reason: " << invocation_reason << "\n"
<< " gcm_registration_id: "
<< device_info.gcm_registration_id()
<< " supported features: "
<< GenerateSupportedFeaturesString(device_info);
cryptauth_enroller_ = enroller_factory_->CreateInstance();
cryptauth_enroller_->Enroll(
GetUserPublicKey(), GetUserPrivateKey(), device_info, invocation_reason,
base::Bind(&CryptAuthEnrollmentManagerImpl::OnEnrollmentFinished,
weak_ptr_factory_.GetWeakPtr()));
}
} // namespace device_sync
} // namespace chromeos