// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/auth/active_session_auth_controller_impl.h"
#include <memory>
#include <string>
#include "ash/auth/views/active_session_auth_view.h"
#include "ash/auth/views/auth_common.h"
#include "ash/auth/views/auth_view_utils.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/auth/active_session_auth_controller.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/i18n/time_formatting.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chromeos/ash/components/cryptohome/auth_factor_conversions.h"
#include "chromeos/ash/components/cryptohome/constants.h"
#include "chromeos/ash/components/dbus/userdataauth/userdataauth_client.h"
#include "chromeos/ash/components/login/auth/auth_performer.h"
#include "chromeos/ash/components/login/auth/public/auth_session_intent.h"
#include "chromeos/ash/components/login/auth/public/session_auth_factors.h"
#include "chromeos/ash/components/login/auth/public/user_context.h"
#include "chromeos/ash/components/osauth/impl/auth_surface_registry.h"
#include "chromeos/ash/components/osauth/public/auth_session_storage.h"
#include "components/account_id/account_id.h"
#include "components/user_manager/known_user.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/base/ui_base_types.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/size.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
namespace ash {
namespace {
// Read the salt from local state.
std::string GetUserSalt(const AccountId& account_id) {
user_manager::KnownUser known_user(Shell::Get()->local_state());
if (const std::string* salt =
known_user.FindStringPath(account_id, prefs::kQuickUnlockPinSalt)) {
return *salt;
}
return {};
}
std::unique_ptr<views::Widget> CreateAuthDialogWidget(
std::unique_ptr<views::View> contents_view) {
views::Widget::InitParams params(
views::Widget::InitParams::CLIENT_OWNS_WIDGET,
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
params.delegate = new views::WidgetDelegate();
params.show_state = ui::SHOW_STATE_NORMAL;
CHECK_EQ(Shell::Get()->session_controller()->GetSessionState(),
session_manager::SessionState::ACTIVE);
params.parent = Shell::GetPrimaryRootWindow()->GetChildById(
kShellWindowId_SystemModalContainer);
params.autosize = true;
params.name = "AuthDialogWidget";
params.delegate->SetInitiallyFocusedView(contents_view.get());
params.delegate->SetModalType(ui::mojom::ModalType::kSystem);
params.delegate->SetOwnedByWidget(true);
std::unique_ptr<views::Widget> widget = std::make_unique<views::Widget>();
widget->Init(std::move(params));
widget->SetVisibilityAnimationTransition(views::Widget::ANIMATE_NONE);
widget->SetContentsView(std::move(contents_view));
return widget;
}
const char* ReasonToString(AuthRequest::Reason reason) {
switch (reason) {
case AuthRequest::Reason::kPasswordManager:
return "PasswordManager";
case AuthRequest::Reason::kSettings:
return "Settings";
case AuthRequest::Reason::kWebAuthN:
return "WebAuthN";
}
NOTREACHED();
}
const char* ActiveSessionAuthStateToString(
ActiveSessionAuthControllerImpl::ActiveSessionAuthState state) {
switch (state) {
case ActiveSessionAuthControllerImpl::ActiveSessionAuthState::kWaitForInit:
return "WaitForInit";
case ActiveSessionAuthControllerImpl::ActiveSessionAuthState::kInitialized:
return "Initialized";
case ActiveSessionAuthControllerImpl::ActiveSessionAuthState::
kPasswordAuthStarted:
return "PasswordAuthStarted";
case ActiveSessionAuthControllerImpl::ActiveSessionAuthState::
kPasswordAuthSucceeded:
return "PasswordAuthSucceeded";
case ActiveSessionAuthControllerImpl::ActiveSessionAuthState::
kPinAuthStarted:
return "PinAuthStarted";
case ActiveSessionAuthControllerImpl::ActiveSessionAuthState::
kPinAuthSucceeded:
return "PinAuthSucceeded";
}
NOTREACHED();
}
std::u16string BuildPinStatusMessage(const cryptohome::PinStatus& pin_status) {
if (!pin_status.IsLockedFactor()) {
return u"";
}
if (pin_status.AvailableAt() == base::Time::Max()) {
return l10n_util::GetStringUTF16(
IDS_ASH_IN_SESSION_AUTH_PIN_TOO_MANY_ATTEMPTS);
} else {
base::TimeDelta delta = pin_status.AvailableAt() - base::Time::Now();
std::u16string time_left_message;
if (base::TimeDurationCompactFormatWithSeconds(
delta, base::DurationFormatWidth::DURATION_WIDTH_WIDE,
&time_left_message)) {
return l10n_util::GetStringFUTF16(
IDS_ASH_IN_SESSION_AUTH_PIN_DELAY_REQUIRED, time_left_message);
} else {
return l10n_util::GetStringUTF16(
IDS_ASH_IN_SESSION_AUTH_PIN_TOO_MANY_ATTEMPTS);
}
}
}
} // namespace
ActiveSessionAuthControllerImpl::TestApi::TestApi(
ActiveSessionAuthControllerImpl* controller)
: controller_(controller) {}
ActiveSessionAuthControllerImpl::TestApi::~TestApi() = default;
AuthFactorSet ActiveSessionAuthControllerImpl::TestApi::GetAvailableFactors()
const {
return controller_->available_factors_;
}
void ActiveSessionAuthControllerImpl::TestApi::SubmitPassword(
const std::string& password) {
controller_->OnPasswordSubmit(base::UTF8ToUTF16(password));
}
void ActiveSessionAuthControllerImpl::TestApi::SubmitPin(
const std::string& pin) {
controller_->OnPinSubmit(base::UTF8ToUTF16(pin));
}
void ActiveSessionAuthControllerImpl::TestApi::DisplayPinStatusMessage(
const cryptohome::PinStatus pin_status) {
controller_->DisplayPinStatusMessage(pin_status);
}
const std::u16string&
ActiveSessionAuthControllerImpl::TestApi::GetPinStatusMessage() const {
return controller_->pin_status_message_;
}
void ActiveSessionAuthControllerImpl::TestApi::Close() {
controller_->Close();
}
ActiveSessionAuthControllerImpl::ActiveSessionAuthControllerImpl() = default;
ActiveSessionAuthControllerImpl::~ActiveSessionAuthControllerImpl() = default;
bool ActiveSessionAuthControllerImpl::ShowAuthDialog(
std::unique_ptr<AuthRequest> auth_request) {
CHECK(auth_request);
LOG(WARNING) << "Show is requested with reason: "
<< ReasonToString(auth_request->GetAuthReason());
if (IsShown()) {
LOG(ERROR) << "ActiveSessionAuthController widget is already exists.";
auth_request->NotifyAuthFailure();
return false;
}
CHECK(!auth_request_);
auth_request_ = std::move(auth_request);
title_ = l10n_util::GetStringUTF16(IDS_ASH_IN_SESSION_AUTH_TITLE);
description_ = auth_request_->GetDescription();
auth_factor_editor_ =
std::make_unique<AuthFactorEditor>(UserDataAuthClient::Get());
auth_performer_ = std::make_unique<AuthPerformer>(UserDataAuthClient::Get());
account_id_ = Shell::Get()->session_controller()->GetActiveAccountId();
user_manager::User* active_user =
user_manager::UserManager::Get()->GetActiveUser();
auto user_context = std::make_unique<UserContext>(*active_user);
const bool ephemeral =
user_manager::UserManager::Get()->IsUserCryptohomeDataEphemeral(
account_id_);
auth_performer_->StartAuthSession(
std::move(user_context), ephemeral, auth_request_->GetAuthSessionIntent(),
base::BindOnce(&ActiveSessionAuthControllerImpl::OnAuthSessionStarted,
weak_ptr_factory_.GetWeakPtr()));
return true;
}
bool ActiveSessionAuthControllerImpl::IsShown() const {
return widget_ != nullptr;
}
void ActiveSessionAuthControllerImpl::OnAuthSessionStarted(
bool user_exists,
std::unique_ptr<UserContext> user_context,
std::optional<AuthenticationError> authentication_error) {
if (!user_exists || authentication_error.has_value()) {
LOG(ERROR) << "Failed to start auth session, code "
<< authentication_error->get_cryptohome_code();
Close();
return;
}
uma_recorder_.RecordShow(auth_request_->GetAuthReason());
UserDataAuthClient::Get()->AddAuthFactorStatusUpdateObserver(this);
available_factors_.Clear();
user_context_ = std::move(user_context);
const auto& auth_factors = user_context_->GetAuthFactorsData();
if (auth_factors.FindAnyPasswordFactor()) {
available_factors_.Put(AuthInputType::kPassword);
}
if (auth_factors.FindPinFactor() && !IsPinLocked()) {
available_factors_.Put(AuthInputType::kPin);
}
InitUi();
}
void ActiveSessionAuthControllerImpl::InitUi() {
auto contents_view = std::make_unique<ActiveSessionAuthView>(
account_id_, title_, description_, available_factors_);
contents_view_ = contents_view.get();
widget_ = CreateAuthDialogWidget(std::move(contents_view));
contents_view_observer_.Observe(contents_view_);
contents_view_->AddObserver(this);
SetState(ActiveSessionAuthState::kInitialized);
MoveToTheCenter();
widget_->Show();
ash::AuthParts::Get()
->GetAuthSurfaceRegistry()
->NotifyInSessionAuthDialogShown();
}
void ActiveSessionAuthControllerImpl::Close() {
LOG(WARNING) << "Close with : " << ActiveSessionAuthStateToString(state_)
<< " state.";
uma_recorder_.RecordClose();
contents_view_observer_.Reset();
CHECK(contents_view_);
contents_view_->RemoveObserver(this);
contents_view_ = nullptr;
SetState(ActiveSessionAuthState::kWaitForInit);
pending_pin_factor_status_update_.reset();
UserDataAuthClient::Get()->RemoveAuthFactorStatusUpdateObserver(this);
if (auth_performer_) {
auth_performer_->InvalidateCurrentAttempts();
auth_performer_.reset();
}
auth_factor_editor_.reset();
title_.clear();
description_.clear();
widget_.reset();
if (auth_request_) {
auth_request_->NotifyAuthFailure();
auth_request_.reset();
}
if (user_context_) {
user_context_.reset();
}
available_factors_.Clear();
}
void ActiveSessionAuthControllerImpl::OnViewPreferredSizeChanged(
views::View* observed_view) {
MoveToTheCenter();
}
void ActiveSessionAuthControllerImpl::MoveToTheCenter() {
widget_->CenterWindow(widget_->GetContentsView()->GetPreferredSize());
}
void ActiveSessionAuthControllerImpl::OnPasswordSubmit(
const std::u16string& password) {
SetState(ActiveSessionAuthState::kPasswordAuthStarted);
uma_recorder_.RecordAuthStarted(AuthInputType::kPassword);
CHECK(user_context_);
const auto* password_factor =
user_context_->GetAuthFactorsData().FindAnyPasswordFactor();
CHECK(password_factor);
const cryptohome::KeyLabel key_label = password_factor->ref().label();
auth_performer_->AuthenticateWithPassword(
key_label.value(), base::UTF16ToUTF8(password), std::move(user_context_),
base::BindOnce(&ActiveSessionAuthControllerImpl::OnAuthComplete,
weak_ptr_factory_.GetWeakPtr(), AuthInputType::kPassword));
}
void ActiveSessionAuthControllerImpl::OnPinSubmit(const std::u16string& pin) {
SetState(ActiveSessionAuthState::kPinAuthStarted);
uma_recorder_.RecordAuthStarted(AuthInputType::kPin);
CHECK(user_context_);
user_manager::KnownUser known_user(Shell::Get()->local_state());
const std::string salt = GetUserSalt(account_id_);
auth_performer_->AuthenticateWithPin(
base::UTF16ToUTF8(pin), salt, std::move(user_context_),
base::BindOnce(&ActiveSessionAuthControllerImpl::OnAuthComplete,
weak_ptr_factory_.GetWeakPtr(), AuthInputType::kPin));
}
void ActiveSessionAuthControllerImpl::OnAuthComplete(
AuthInputType input_type,
std::unique_ptr<UserContext> user_context,
std::optional<AuthenticationError> authentication_error) {
if (authentication_error.has_value()) {
uma_recorder_.RecordAuthFailed(input_type);
user_context_ = std::move(user_context);
if (pending_pin_factor_status_update_.has_value()) {
ProcessAuthFactorStatusUpdate(pending_pin_factor_status_update_.value());
pending_pin_factor_status_update_.reset();
}
contents_view_->SetErrorTitle(l10n_util::GetStringUTF16(
input_type == AuthInputType::kPassword
? IDS_ASH_IN_SESSION_AUTH_PASSWORD_INCORRECT
: IDS_ASH_IN_SESSION_AUTH_PIN_INCORRECT));
SetState(ActiveSessionAuthState::kInitialized);
} else {
uma_recorder_.RecordAuthSucceeded(input_type);
SetState(input_type == AuthInputType::kPassword
? ActiveSessionAuthState::kPasswordAuthSucceeded
: ActiveSessionAuthState::kPinAuthSucceeded);
auth_request_->NotifyAuthSuccess(std::move(user_context));
auth_request_.reset();
Close();
}
}
void ActiveSessionAuthControllerImpl::OnClose() {
Close();
}
bool ActiveSessionAuthControllerImpl::IsPinLocked() const {
CHECK(user_context_);
const auto& auth_factors = user_context_->GetAuthFactorsData();
auto* pin_factor = auth_factors.FindPinFactor();
CHECK(pin_factor);
return pin_factor->GetPinStatus().IsLockedFactor();
}
void ActiveSessionAuthControllerImpl::SetState(ActiveSessionAuthState state) {
LOG(WARNING) << "SetState is requested from: "
<< ActiveSessionAuthStateToString(state_)
<< " state to : " << ActiveSessionAuthStateToString(state)
<< " state.";
switch (state) {
case ActiveSessionAuthState::kWaitForInit:
break;
case ActiveSessionAuthState::kInitialized:
CHECK(state_ == ActiveSessionAuthState::kWaitForInit ||
state_ == ActiveSessionAuthState::kPasswordAuthStarted ||
state_ == ActiveSessionAuthState::kPinAuthStarted);
contents_view_->SetInputEnabled(true);
break;
case ActiveSessionAuthState::kPasswordAuthStarted:
// Disable the UI while we are waiting for the response, except the close
// button.
CHECK_EQ(state_, ActiveSessionAuthState::kInitialized);
contents_view_->SetInputEnabled(false);
break;
case ActiveSessionAuthState::kPasswordAuthSucceeded:
CHECK_EQ(state_, ActiveSessionAuthState::kPasswordAuthStarted);
break;
case ActiveSessionAuthState::kPinAuthStarted:
CHECK_EQ(state_, ActiveSessionAuthState::kInitialized);
contents_view_->SetInputEnabled(false);
break;
case ActiveSessionAuthState::kPinAuthSucceeded:
CHECK_EQ(state_, ActiveSessionAuthState::kPinAuthStarted);
break;
}
state_ = state;
}
void ActiveSessionAuthControllerImpl::OnAuthFactorStatusUpdate(
const user_data_auth::AuthFactorStatusUpdate& update) {
if (update.auth_factor_with_status().auth_factor().type() ==
user_data_auth::AUTH_FACTOR_TYPE_PIN) {
if (user_context_) {
ProcessAuthFactorStatusUpdate(update);
} else {
if (pending_pin_factor_status_update_.has_value()) {
LOG(WARNING) << "Overwrite pending pin status update.";
}
pending_pin_factor_status_update_ = update;
}
}
}
void ActiveSessionAuthControllerImpl::ProcessAuthFactorStatusUpdate(
const user_data_auth::AuthFactorStatusUpdate& update) {
CHECK(user_context_);
// Broadcast id is a public id of an ongoing auth session.
// Generally it is unlikely to have two active auth session running
// in parallel, but we need to make sure we only react to signals
// mapped to this session.
if (user_context_->GetBroadcastId() == update.broadcast_id()) {
auto auth_factor = cryptohome::DeserializeAuthFactor(
update.auth_factor_with_status(),
/*fallback_type=*/cryptohome::AuthFactorType::kPassword);
if (auth_factor.ref().type() == cryptohome::AuthFactorType::kPin) {
CHECK(contents_view_);
auto pin_status = auth_factor.GetPinStatus();
bool pin_enabled = !pin_status.IsLockedFactor();
// Only need to update the auth view because |available_factors_| is only
// used to initialize the view.
contents_view_->SetHasPin(pin_enabled);
DisplayPinStatusMessage(pin_status);
}
}
}
void ActiveSessionAuthControllerImpl::DisplayPinStatusMessage(
const cryptohome::PinStatus pin_status) {
pin_status_message_ = BuildPinStatusMessage(pin_status);
contents_view_->SetPinStatus(pin_status_message_);
}
} // namespace ash