// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/login/security_token_session_controller.h"

#include <string>
#include <string_view>
#include <vector>

#include "ash/constants/ash_login_pref_names.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/notreached.h"
#include "base/strings/utf_string_conversions.h"
#include "base/syslog_logging.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "base/trace_event/trace_event.h"
#include "chrome/browser/ash/certificate_provider/certificate_provider_service.h"
#include "chrome/browser/ash/certificate_provider/certificate_provider_service_factory.h"
#include "chrome/browser/ash/login/lock/screen_locker_controller.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/enterprise/util/managed_browser_utils.h"
#include "chrome/browser/extensions/forced_extensions/force_installed_tracker.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/security_token_restriction/security_token_session_restriction_view.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_types.h"
#include "chromeos/ash/components/login/auth/challenge_response/known_user_pref_utils.h"
#include "chromeos/ash/components/login/auth/public/challenge_response_key.h"
#include "chromeos/components/certificate_provider/certificate_info.h"
#include "chromeos/components/certificate_provider/certificate_provider.h"
#include "chromeos/ui/vector_icons/vector_icons.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/session_manager/core/session_manager.h"
#include "components/session_manager/session_manager_types.h"
#include "components/user_manager/known_user.h"
#include "components/user_manager/user.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/extension_id.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "net/cert/asn1_util.h"
#include "net/cert/x509_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/chromeos/devicetype_utils.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/dialog_delegate.h"
#include "url/gurl.h"
#include "url/url_constants.h"

namespace ash {
namespace login {
namespace {

// Possible values of ash::prefs::kSecurityTokenSessionBehavior. This needs to
// match the values of the SecurityTokenSessionBehavior policy defined in
// policy_templates.json.
constexpr char kIgnorePrefValue[] = "IGNORE";
constexpr char kLogoutPrefValue[] = "LOGOUT";
constexpr char kLockPrefValue[] = "LOCK";

constexpr char kNotifierSecurityTokenSession[] =
    "ash.security_token_session_controller";
constexpr char kNotificationId[] =
    "security_token_session_controller_notification";

// How long we allow before smart card middleware extensions start reporting
// the user's certificates after all extensions become installed and ready
// during login or unlock process. This is needed because of the time it takes
// because USB devices can temporarily remain occupied by the login/lock-screen
// extensions.
constexpr base::TimeDelta kSessionActivationTimeout = base::Seconds(20);

SecurityTokenSessionController::Behavior ParseBehaviorPrefValue(
    const std::string& behavior) {
  if (behavior == kIgnorePrefValue)
    return SecurityTokenSessionController::Behavior::kIgnore;
  if (behavior == kLogoutPrefValue)
    return SecurityTokenSessionController::Behavior::kLogout;
  if (behavior == kLockPrefValue)
    return SecurityTokenSessionController::Behavior::kLock;

  return SecurityTokenSessionController::Behavior::kIgnore;
}

std::string SerializeBehaviorValue(
    const SecurityTokenSessionController::Behavior& behavior) {
  switch (behavior) {
    case SecurityTokenSessionController::Behavior::kIgnore:
      return std::string(kIgnorePrefValue);
    case SecurityTokenSessionController::Behavior::kLogout:
      return std::string(kLogoutPrefValue);
    case SecurityTokenSessionController::Behavior::kLock:
      return std::string(kLockPrefValue);
  }
  NOTREACHED();
}

// Checks if `domain` represents a valid domain. Returns false if `domain` is
// malformed. Returns the host part, which should be displayed to the user, in
// `sanitized_domain`.
bool SanitizeDomain(const std::string& domain, std::string& sanitized_domain) {
  // Add "http://" to the url. Otherwise, "example.com" would be rejected,
  // even though it has the format that is expected for `domain`.
  GURL url(std::string(url::kHttpScheme) +
           std::string(url::kStandardSchemeSeparator) + domain);
  if (!url.is_valid())
    return false;
  if (!url.has_host())
    return false;
  sanitized_domain = url.GetHost();
  return true;
}

void DisplayNotification(const std::u16string& title,
                         const std::u16string& text) {
  auto notification = CreateSystemNotificationPtr(
      message_center::NOTIFICATION_TYPE_SIMPLE, kNotificationId, title, text,
      /*display_source=*/std::u16string(), /*origin_url=*/GURL(),
      message_center::NotifierId(message_center::NotifierType::SYSTEM_COMPONENT,
                                 kNotifierSecurityTokenSession,
                                 NotificationCatalogName::kSecurityToken),
      /*optional_fields=*/{},
      new message_center::HandleNotificationClickDelegate(
          base::DoNothingAs<void()>()),
      chromeos::kEnterpriseIcon,
      message_center::SystemNotificationWarningLevel::NORMAL);
  notification->set_fullscreen_visibility(
      message_center::FullscreenVisibility::OVER_USER);
  notification->SetSystemPriority();
  message_center::MessageCenter::Get()->AddNotification(
      std::move(notification));
}

// Loads the persistently stored information about the challenge-response keys
// that can be used for authenticating the user.
void LoadStoredChallengeResponseSpkiKeysForUser(
    PrefService* local_state,
    const AccountId& account_id,
    base::flat_map<std::string, std::vector<std::string>>* extension_to_spkis,
    base::flat_set<std::string>* extension_ids) {
  // TODO(crbug.com/1164373) This approach does not work for ephemeral users.
  // Instead, only get the certificate that was actually used on the last login.
  const base::ListValue known_user_value =
      user_manager::KnownUser(local_state).GetChallengeResponseKeys(account_id);
  std::vector<DeserializedChallengeResponseKey>
      deserialized_challenge_response_keys;
  DeserializeChallengeResponseKeyFromKnownUser(
      known_user_value, &deserialized_challenge_response_keys);
  for (const DeserializedChallengeResponseKey& challenge_response_key :
       deserialized_challenge_response_keys) {
    if (challenge_response_key.extension_id.empty())
      continue;

    extension_ids->insert(challenge_response_key.extension_id);
    if (auto it =
            extension_to_spkis->try_emplace(challenge_response_key.extension_id)
                .first;
        !challenge_response_key.public_key_spki_der.empty()) {
      it->second.push_back(challenge_response_key.public_key_spki_der);
    }
  }
}

std::string GetSubjectPublicKeyInfo(const net::X509Certificate& certificate) {
  std::string_view spki_bytes;
  if (!net::asn1::ExtractSPKIFromDERCert(
          net::x509_util::CryptoBufferAsStringPiece(certificate.cert_buffer()),
          &spki_bytes)) {
    return {};
  }
  return std::string(spki_bytes);
}

}  // namespace

const char* const
    SecurityTokenSessionController::kNotificationDisplayedKnownUserKey =
        "security_token_session_notification_displayed";

SecurityTokenSessionController::SecurityTokenSessionController(
    Profile* profile,
    PrefService* local_state,
    const user_manager::User* primary_user,
    chromeos::CertificateProviderService* certificate_provider_service)
    : is_user_profile_(IsUserBrowserContext(profile)),
      local_state_(local_state),
      primary_user_(primary_user),
      certificate_provider_service_(certificate_provider_service),
      extensions_tracker_(extensions::ExtensionRegistry::Get(profile), profile),
      session_manager_(session_manager::SessionManager::Get()),
      session_activation_seconds_(kSessionActivationTimeout) {
  DCHECK(local_state_);
  DCHECK(primary_user_);
  DCHECK(certificate_provider_service_);
  session_manager_observation_.Observe(session_manager_.get());
  certificate_provider_ =
      certificate_provider_service_->CreateCertificateProvider();
  LoadStoredChallengeResponseSpkiKeysForUser(
      local_state_, primary_user_->GetAccountId(), &extension_to_spkis_,
      &observed_extensions_);
  UpdateNotificationPref();
  behavior_ = GetBehaviorFromPrefAndSessionState();
  pref_change_registrar_.Init(local_state_);
  base::RepeatingClosure behavior_pref_changed_callback =
      base::BindRepeating(&SecurityTokenSessionController::UpdateBehavior,
                          weak_ptr_factory_.GetWeakPtr());
  base::RepeatingClosure notification_pref_changed_callback =
      base::BindRepeating(
          &SecurityTokenSessionController::UpdateNotificationPref,
          weak_ptr_factory_.GetWeakPtr());
  pref_change_registrar_.Add(ash::prefs::kSecurityTokenSessionBehavior,
                             behavior_pref_changed_callback);
  pref_change_registrar_.Add(
      ash::prefs::kSecurityTokenSessionNotificationSeconds,
      notification_pref_changed_callback);
  certificate_provider_service_->AddObserver(this);
  extensions_tracker_.AddObserver(this);
}

SecurityTokenSessionController::~SecurityTokenSessionController() {
  extensions_tracker_.RemoveObserver(this);
  certificate_provider_service_->RemoveObserver(this);
}

void SecurityTokenSessionController::Shutdown() {
  pref_change_registrar_.RemoveAll();
}

void SecurityTokenSessionController::OnChallengeResponseKeysUpdated() {
  extension_to_spkis_.clear();
  observed_extensions_.clear();
  LoadStoredChallengeResponseSpkiKeysForUser(
      local_state_, primary_user_->GetAccountId(), &extension_to_spkis_,
      &observed_extensions_);
}

void SecurityTokenSessionController::OnCertificatesUpdated(
    const std::string& extension_id,
    const std::vector<chromeos::certificate_provider::CertificateInfo>&
        certificate_infos) {
  if (behavior_ == Behavior::kIgnore)
    return;

  if (!observed_extensions_.contains(extension_id))
    return;

  auto& expected_spkis = extension_to_spkis_[extension_id];
  if (expected_spkis.empty()) {
    return;
  }

  bool extension_provides_all_required_certificates = true;

  std::vector<std::string> provided_spki_vector;
  provided_spki_vector.reserve(certificate_infos.size());
  for (auto certificate_info : certificate_infos) {
    provided_spki_vector.emplace_back(
        GetSubjectPublicKeyInfo(*certificate_info.certificate.get()));
  }
  base::flat_set<std::string> provided_spkis(std::move(provided_spki_vector));
  for (const auto& expected_spki : expected_spkis) {
    if (!provided_spkis.contains(expected_spki)) {
      extension_provides_all_required_certificates = false;
      break;
    }
  }

  if (extension_provides_all_required_certificates) {
    ExtensionProvidesAllRequiredCertificates(extension_id);
  } else {
    extensions_missing_required_certificates_.insert(extension_id);
    ExtensionStopsProvidingCertificate();
  }
}

void SecurityTokenSessionController::OnForceInstalledExtensionsReady() {
  if (session_manager_->session_state() !=
          session_manager::SessionState::ACTIVE ||
      is_session_activation_complete_ ||
      session_activation_timer_.IsRunning()) {
    return;
  }
  StartSessionActivation();
}

void SecurityTokenSessionController::OnSessionStateChanged() {
  TRACE_EVENT0("login",
               "SecurityTokenSessionController::OnSessionStateChanged");
  if (session_manager_->session_state() ==
      session_manager::SessionState::LOCKED) {
    had_lock_screen_transition_ = true;
  }

  is_session_activation_complete_ = false;
  // Reset the flag, so that after the certificates are collected from all
  // extensions we know whether the absence of some should be tolerated.
  all_required_certificates_were_observed_ = false;

  UpdateBehavior();

  // In case kInstallForceList preference wouldn't load yet, it would still
  // mean that all extensions are installed and ready. However, IsComplete()
  // call also ensures there is at least one extension that is installed and
  // ready. That will always be the case because reading smartcards depends on
  // Smart Card Connector App being installed.
  if (session_manager_->session_state() ==
          session_manager::SessionState::ACTIVE &&
      extensions_tracker_.IsComplete()) {
    StartSessionActivation();
  }
}

void SecurityTokenSessionController::SetSessionActivationTimeoutForTest(
    base::TimeDelta session_activation_seconds) {
  session_activation_seconds_ = session_activation_seconds;
}

void SecurityTokenSessionController::TriggerSessionActivationTimeoutForTest() {
  if (session_activation_timer_.IsRunning()) {
    session_activation_timer_.FireNow();
  }
}

// static
void SecurityTokenSessionController::RegisterLocalStatePrefs(
    PrefRegistrySimple* registry) {
  // Prefs that contain policy values. We use the Local State for these, so that
  // the values are available for the controller regardless of the profile it's
  // attached to (the policy stack has code to automatically copy the primary
  // profile's policies into the Local State).
  registry->RegisterStringPref(ash::prefs::kSecurityTokenSessionBehavior,
                               kIgnorePrefValue);
  registry->RegisterIntegerPref(
      ash::prefs::kSecurityTokenSessionNotificationSeconds, 0);
  // Prefs that contain state that needs to be persisted across Chrome restarts.
  registry->RegisterStringPref(
      ash::prefs::kSecurityTokenSessionNotificationScheduledDomain, "");
}

// static
void SecurityTokenSessionController::MaybeDisplayLoginScreenNotification(
    PrefService& local_state) {
  const PrefService::Preference* scheduled_notification_domain =
      local_state.FindPreference(
          ash::prefs::kSecurityTokenSessionNotificationScheduledDomain);
  if (!scheduled_notification_domain ||
      scheduled_notification_domain->IsDefaultValue() ||
      !scheduled_notification_domain->GetValue()->is_string()) {
    // No notification is scheduled.
    return;
  }
  // Sanitize `scheduled_notification_domain`, as values coming from local state
  // are not trusted.
  std::string domain = scheduled_notification_domain->GetValue()->GetString();
  local_state.ClearPref(
      ash::prefs::kSecurityTokenSessionNotificationScheduledDomain);
  std::string sanitized_domain;
  if (!SanitizeDomain(domain, sanitized_domain)) {
    // The pref value is invalid.
    return;
  }
  DisplayNotification(
      l10n_util::GetStringUTF16(
          IDS_SECURITY_TOKEN_SESSION_LOGOUT_MESSAGE_TITLE),
      l10n_util::GetStringFUTF16(IDS_SECURITY_TOKEN_SESSION_LOGOUT_MESSAGE_BODY,
                                 base::UTF8ToUTF16(sanitized_domain)));
}

void SecurityTokenSessionController::UpdateBehavior() {
  Behavior previous_behavior = behavior_;
  behavior_ = GetBehaviorFromPrefAndSessionState();
  if (behavior_ == Behavior::kIgnore) {
    Reset();
  } else if (previous_behavior == Behavior::kIgnore) {
    // Request all available certificates to ensure that all required
    // certificates are still present.
    certificate_provider_->GetCertificates(base::DoNothing());
  }
}

void SecurityTokenSessionController::UpdateNotificationPref() {
  notification_seconds_ = base::Seconds(local_state_->GetInteger(
      ash::prefs::kSecurityTokenSessionNotificationSeconds));
}

bool SecurityTokenSessionController::ShouldApplyPolicyInCurrentSessionState()
    const {
  switch (session_manager_->session_state()) {
    case session_manager::SessionState::UNKNOWN:
    case session_manager::SessionState::OOBE:
    case session_manager::SessionState::LOGIN_PRIMARY:
    case session_manager::SessionState::LOGGED_IN_NOT_ACTIVE:
    case session_manager::SessionState::LOGIN_SECONDARY:
    case session_manager::SessionState::RMA:
      return false;
    case session_manager::SessionState::ACTIVE:
      if (!is_user_profile_) {
        // Inside the user session, only the controller that's tied to the user
        // profile should work.
        return false;
      }
      return true;
    case session_manager::SessionState::LOCKED:
      if (is_user_profile_) {
        // On the lock screen, only the controller that's tied to the sign-in
        // profile should work.
        return false;
      }
      return true;
  }
  NOTREACHED();
}

SecurityTokenSessionController::Behavior
SecurityTokenSessionController::GetBehaviorFromPrefAndSessionState() const {
  // First determine if we're in a session state in which our instance should do
  // nothing (ignore the policy).
  if (!ShouldApplyPolicyInCurrentSessionState())
    return Behavior::kIgnore;
  // After passing the session state checks, use the policy value as the desired
  // behavior.
  return ParseBehaviorPrefValue(
      local_state_->GetString(ash::prefs::kSecurityTokenSessionBehavior));
}

void SecurityTokenSessionController::TriggerAction() {
  if (fullscreen_notification_ && !fullscreen_notification_->IsClosed()) {
    fullscreen_notification_->CloseWithReason(
        views::Widget::ClosedReason::kAcceptButtonClicked);
  }
  Reset();
  switch (behavior_) {
    case Behavior::kIgnore:
      return;
    case Behavior::kLock:
      ScreenLockerController::Get().ShowLockScreen();
      AddLockNotification();
      return;
    case Behavior::kLogout:
      session_manager::SessionManager::Get()->RequestSignOut();
      ScheduleLogoutNotification();
      return;
  }
  NOTREACHED();
}

void SecurityTokenSessionController::StartSessionActivation() {
  session_activation_timer_.Start(
      FROM_HERE, session_activation_seconds_,
      base::BindOnce(&SecurityTokenSessionController::CompleteSessionActivation,
                     weak_ptr_factory_.GetWeakPtr()));
}

void SecurityTokenSessionController::CompleteSessionActivation() {
  is_session_activation_complete_ = true;
  if (!all_required_certificates_were_observed_) {
    ExtensionStopsProvidingCertificate();
  }
}

void SecurityTokenSessionController::ExtensionProvidesAllRequiredCertificates(
    const extensions::ExtensionId& extension_id) {
  extensions_missing_required_certificates_.erase(extension_id);
  if (extensions_missing_required_certificates_.empty()) {
    all_required_certificates_were_observed_ = true;
    Reset();
  }
}

void SecurityTokenSessionController::ExtensionStopsProvidingCertificate() {
  if (!all_required_certificates_were_observed_ &&
      had_lock_screen_transition_) {
    // When transitioning to/from the Lock Screen, we delay applying the policy
    // until we saw the full list of the required certificates at least once.
    // This is needed because the extensions report a spuriously empty list of
    // certificates shortly after such session state transition, due to the USB
    // access conflicts between two profiles.
    return;
  }
  if (!all_required_certificates_were_observed_ &&
      session_manager_->session_state() ==
          session_manager::SessionState::ACTIVE &&
      !is_session_activation_complete_) {
    return;
  }

  if (fullscreen_notification_) {
    // There was already a security token missing.
    return;
  }

  if (behavior_ == Behavior::kIgnore) {
    return;
  }
  SYSLOG(WARNING) << "Missing certificate is about to trigger "
                  << SerializeBehaviorValue(behavior_)
                  << " action with a delay " << notification_seconds_ << ".";

  // Schedule session lock / logout.
  action_timer_.Start(
      FROM_HERE, notification_seconds_,
      base::BindOnce(&SecurityTokenSessionController::TriggerAction,
                     weak_ptr_factory_.GetWeakPtr()));

  if (!notification_seconds_.is_zero()) {
    fullscreen_notification_ = views::DialogDelegate::CreateDialogWidget(
        std::make_unique<SecurityTokenSessionRestrictionView>(
            notification_seconds_,
            base::BindOnce(&SecurityTokenSessionController::TriggerAction,
                           weak_ptr_factory_.GetWeakPtr()),
            behavior_,
            enterprise_util::GetDomainFromEmail(
                primary_user_->GetDisplayEmail())),
        nullptr, nullptr);
    fullscreen_notification_->Show();
  }
}

void SecurityTokenSessionController::AddLockNotification() {
  // A user should see the notification only the first time their session is
  // locked.
  if (GetNotificationDisplayedKnownUserFlag())
    return;
  SetNotificationDisplayedKnownUserFlag();

  std::string domain =
      enterprise_util::GetDomainFromEmail(primary_user_->GetDisplayEmail());
  DisplayNotification(
      l10n_util::GetStringFUTF16(IDS_SECURITY_TOKEN_SESSION_LOCK_MESSAGE_TITLE,
                                 ui::GetChromeOSDeviceName()),
      l10n_util::GetStringFUTF16(IDS_SECURITY_TOKEN_SESSION_LOGOUT_MESSAGE_BODY,
                                 base::UTF8ToUTF16(domain)));
}

void SecurityTokenSessionController::ScheduleLogoutNotification() {
  // The notification can not be created directly, since it will not persist
  // after the session is ended. Instead, use local state to schedule the
  // creation of a notification.
  if (GetNotificationDisplayedKnownUserFlag())
    return;
  SetNotificationDisplayedKnownUserFlag();

  local_state_->SetString(
      ash::prefs::kSecurityTokenSessionNotificationScheduledDomain,
      enterprise_util::GetDomainFromEmail(primary_user_->GetDisplayEmail()));
}

void SecurityTokenSessionController::Reset() {
  action_timer_.Stop();
  session_activation_timer_.Stop();
  extensions_missing_required_certificates_.clear();
  if (fullscreen_notification_) {
    if (!fullscreen_notification_->IsClosed()) {
      fullscreen_notification_->CloseWithReason(
          views::Widget::ClosedReason::kEscKeyPressed);
    }
    fullscreen_notification_ = nullptr;
  }
}

bool SecurityTokenSessionController::GetNotificationDisplayedKnownUserFlag()
    const {
  return user_manager::KnownUser(local_state_)
      .FindBoolPath(primary_user_->GetAccountId(),
                    kNotificationDisplayedKnownUserKey)
      .value_or(false);
}

void SecurityTokenSessionController::SetNotificationDisplayedKnownUserFlag() {
  // The reason we use `KnownUser` (i.e., the Local State) here is because the
  // flag needs to be readable/writable from the instance of our class that's
  // tied to the sign-in profile. There's no direct/safe way to access a
  // profile's pref service from a keyed service tied to a different profile.
  user_manager::KnownUser(local_state_)
      .SetBooleanPref(primary_user_->GetAccountId(),
                      kNotificationDisplayedKnownUserKey, true);
}

}  // namespace login
}  // namespace ash
