blob: 00215f70cdc8359de3057177fd6b54ed127e382b [file] [log] [blame]
// 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/notifier_catalogs.h"
#include "ash/public/cpp/notification_utils.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/login/lock/screen_locker.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/certificate_provider/certificate_provider.h"
#include "chrome/browser/certificate_provider/certificate_provider_service.h"
#include "chrome/browser/certificate_provider/certificate_provider_service_factory.h"
#include "chrome/browser/enterprise/util/managed_browser_utils.h"
#include "chrome/browser/extensions/forced_extensions/force_installed_tracker.h"
#include "chrome/browser/lifetime/application_lifetime.h"
#include "chrome/browser/notifications/system_notification_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/security_token_restriction/security_token_session_restriction_view.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.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/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/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 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.host();
return true;
}
void DisplayNotification(const std::u16string& title,
const std::u16string& text) {
message_center::Notification notification = CreateSystemNotification(
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();
SystemNotificationHelper::GetInstance()->Display(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::Value::List 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 (!extension_to_spkis->contains(challenge_response_key.extension_id)) {
(*extension_to_spkis)[challenge_response_key.extension_id] = {};
}
if (!challenge_response_key.public_key_spki_der.empty()) {
(*extension_to_spkis)[challenge_response_key.extension_id].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_(ProfileHelper::IsPrimaryProfile(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(prefs::kSecurityTokenSessionBehavior,
behavior_pref_changed_callback);
pref_change_registrar_.Add(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;
if (extension_to_spkis_[extension_id].empty())
return;
bool extension_provides_all_required_certificates = true;
std::vector<std::string> provided_spki_vector;
for (auto certificate_info : certificate_infos) {
provided_spki_vector.emplace_back(
GetSubjectPublicKeyInfo(*certificate_info.certificate.get()));
}
base::flat_set<std::string> provided_spkis(provided_spki_vector.begin(),
provided_spki_vector.end());
auto& expected_spkis = extension_to_spkis_[extension_id];
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(prefs::kSecurityTokenSessionBehavior,
kIgnorePrefValue);
registry->RegisterIntegerPref(prefs::kSecurityTokenSessionNotificationSeconds,
0);
// Prefs that contain state that needs to be persisted across Chrome restarts.
registry->RegisterStringPref(
prefs::kSecurityTokenSessionNotificationScheduledDomain, "");
}
// static
void SecurityTokenSessionController::MaybeDisplayLoginScreenNotification() {
PrefService* local_state = g_browser_process->local_state();
const PrefService::Preference* scheduled_notification_domain =
local_state->FindPreference(
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(
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(
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(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:
ScreenLocker::Show();
AddLockNotification();
return;
case Behavior::kLogout:
chrome::AttemptExit();
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(
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