// Copyright 2015 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 "components/cryptauth/cryptauth_enrollment_manager.h"

#include <utility>

#include "base/base64url.h"
#include "base/memory/ptr_util.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "components/cryptauth/cryptauth_enroller.h"
#include "components/cryptauth/pref_names.h"
#include "components/cryptauth/secure_message_delegate.h"
#include "components/cryptauth/sync_scheduler_impl.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/proximity_auth/logging/logging.h"

namespace cryptauth {

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 base::MakeUnique<SyncSchedulerImpl>(
      delegate, base::TimeDelta::FromDays(kEnrollmentRefreshPeriodDays),
      base::TimeDelta::FromMinutes(kEnrollmentBaseRecoveryPeriodMinutes),
      kEnrollmentMaxJitterRatio, "CryptAuth Enrollment");
}

}  // namespace

CryptAuthEnrollmentManager::CryptAuthEnrollmentManager(
    std::unique_ptr<base::Clock> clock,
    std::unique_ptr<CryptAuthEnrollerFactory> enroller_factory,
    std::unique_ptr<SecureMessageDelegate> secure_message_delegate,
    const GcmDeviceInfo& device_info,
    CryptAuthGCMManager* gcm_manager,
    PrefService* pref_service)
    : clock_(std::move(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) {}

CryptAuthEnrollmentManager::~CryptAuthEnrollmentManager() {
  gcm_manager_->RemoveObserver(this);
}

// static
void CryptAuthEnrollmentManager::RegisterPrefs(PrefRegistrySimple* registry) {
  registry->RegisterBooleanPref(
      prefs::kCryptAuthEnrollmentIsRecoveringFromFailure, false);
  registry->RegisterDoublePref(
      prefs::kCryptAuthEnrollmentLastEnrollmentTimeSeconds, 0.0);
  registry->RegisterIntegerPref(prefs::kCryptAuthEnrollmentReason,
                                INVOCATION_REASON_UNKNOWN);
  registry->RegisterStringPref(prefs::kCryptAuthEnrollmentUserPublicKey,
                               std::string());
  registry->RegisterStringPref(prefs::kCryptAuthEnrollmentUserPrivateKey,
                               std::string());
}

void CryptAuthEnrollmentManager::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 CryptAuthEnrollmentManager::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void CryptAuthEnrollmentManager::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

void CryptAuthEnrollmentManager::ForceEnrollmentNow(
    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 INVOCATION_REASON_FAILURE_RECOVERY.
  pref_service_->SetInteger(prefs::kCryptAuthEnrollmentReason,
                            invocation_reason);
  scheduler_->ForceSync();
}

bool CryptAuthEnrollmentManager::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 CryptAuthEnrollmentManager::GetLastEnrollmentTime() const {
  return base::Time::FromDoubleT(pref_service_->GetDouble(
      prefs::kCryptAuthEnrollmentLastEnrollmentTimeSeconds));
}

base::TimeDelta CryptAuthEnrollmentManager::GetTimeToNextAttempt() const {
  return scheduler_->GetTimeToNextSync();
}

bool CryptAuthEnrollmentManager::IsEnrollmentInProgress() const {
  return scheduler_->GetSyncState() ==
         SyncScheduler::SyncState::SYNC_IN_PROGRESS;
}

bool CryptAuthEnrollmentManager::IsRecoveringFromFailure() const {
  return scheduler_->GetStrategy() ==
         SyncScheduler::Strategy::AGGRESSIVE_RECOVERY;
}

void CryptAuthEnrollmentManager::OnEnrollmentFinished(bool success) {
  if (success) {
    pref_service_->SetDouble(
        prefs::kCryptAuthEnrollmentLastEnrollmentTimeSeconds,
        clock_->Now().ToDoubleT());
    pref_service_->SetInteger(prefs::kCryptAuthEnrollmentReason,
                              INVOCATION_REASON_UNKNOWN);
  }

  pref_service_->SetBoolean(prefs::kCryptAuthEnrollmentIsRecoveringFromFailure,
                            !success);

  sync_request_->OnDidComplete(success);
  cryptauth_enroller_.reset();
  sync_request_.reset();
  for (auto& observer : observers_)
    observer.OnEnrollmentFinished(success);
}

std::string CryptAuthEnrollmentManager::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 CryptAuthEnrollmentManager::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 CryptAuthEnrollmentManager::SetSyncSchedulerForTest(
    std::unique_ptr<SyncScheduler> sync_scheduler) {
  scheduler_ = std::move(sync_scheduler);
}

void CryptAuthEnrollmentManager::OnGCMRegistrationResult(bool success) {
  if (!sync_request_)
    return;

  PA_LOG(INFO) << "GCM registration for CryptAuth Enrollment completed: "
               << success;
  if (success)
    DoCryptAuthEnrollment();
  else
    OnEnrollmentFinished(false);
}

void CryptAuthEnrollmentManager::OnKeyPairGenerated(
    const std::string& public_key,
    const std::string& private_key) {
  if (!public_key.empty() && !private_key.empty()) {
    PA_LOG(INFO) << "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 CryptAuthEnrollmentManager::OnReenrollMessage() {
  ForceEnrollmentNow(INVOCATION_REASON_SERVER_INITIATED);
}

void CryptAuthEnrollmentManager::OnSyncRequested(
    std::unique_ptr<SyncScheduler::SyncRequest> sync_request) {
  for (auto& observer : observers_)
    observer.OnEnrollmentStarted();

  sync_request_ = std::move(sync_request);
  if (gcm_manager_->GetRegistrationId().empty() ||
      pref_service_->GetInteger(prefs::kCryptAuthEnrollmentReason) ==
          INVOCATION_REASON_MANUAL) {
    gcm_manager_->RegisterWithGCM();
  } else {
    DoCryptAuthEnrollment();
  }
}

void CryptAuthEnrollmentManager::DoCryptAuthEnrollment() {
  if (GetUserPublicKey().empty() || GetUserPrivateKey().empty()) {
    secure_message_delegate_->GenerateKeyPair(
        base::Bind(&CryptAuthEnrollmentManager::OnKeyPairGenerated,
                   weak_ptr_factory_.GetWeakPtr()));
  } else {
    DoCryptAuthEnrollmentWithKeys();
  }
}

void CryptAuthEnrollmentManager::DoCryptAuthEnrollmentWithKeys() {
  DCHECK(sync_request_);
  InvocationReason invocation_reason =
      INVOCATION_REASON_UNKNOWN;

  int reason_stored_in_prefs =
      pref_service_->GetInteger(prefs::kCryptAuthEnrollmentReason);

  if (InvocationReason_IsValid(reason_stored_in_prefs) &&
      reason_stored_in_prefs != INVOCATION_REASON_UNKNOWN) {
    invocation_reason =
        static_cast<InvocationReason>(reason_stored_in_prefs);
  } else if (GetLastEnrollmentTime().is_null()) {
    invocation_reason = INVOCATION_REASON_INITIALIZATION;
  } else if (!IsEnrollmentValid()) {
    invocation_reason = INVOCATION_REASON_EXPIRATION;
  } else if (scheduler_->GetStrategy() ==
             SyncScheduler::Strategy::PERIODIC_REFRESH) {
    invocation_reason = INVOCATION_REASON_PERIODIC;
  } else if (scheduler_->GetStrategy() ==
             SyncScheduler::Strategy::AGGRESSIVE_RECOVERY) {
    invocation_reason = 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.
  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(INFO) << "Making enrollment:\n"
               << "  public_key: " << public_key_b64 << "\n"
               << "  invocation_reason: " << invocation_reason << "\n"
               << "  gcm_registration_id: "
               << device_info.gcm_registration_id();

  cryptauth_enroller_ = enroller_factory_->CreateInstance();
  cryptauth_enroller_->Enroll(
      GetUserPublicKey(), GetUserPrivateKey(), device_info, invocation_reason,
      base::Bind(&CryptAuthEnrollmentManager::OnEnrollmentFinished,
                 weak_ptr_factory_.GetWeakPtr()));
}

}  // namespace cryptauth
