blob: f5fa652984b60125c862c765d541e223c1041273 [file] [log] [blame]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cryptohome/auth_session_manager.h"
#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <base/check.h>
#include <base/functional/bind.h>
#include <base/location.h>
#include <base/notreached.h>
#include <base/time/default_clock.h>
#include <base/time/time.h>
#include <cryptohome/proto_bindings/UserDataAuth.pb.h>
#include <libhwsec/status.h>
#include "cryptohome/error/location_utils.h"
#include "cryptohome/platform.h"
namespace cryptohome {
namespace {
using ::cryptohome::error::CryptohomeError;
using ::cryptohome::error::ErrorActionSet;
using ::cryptohome::error::PossibleAction;
using ::cryptohome::error::PrimaryAction;
using ::hwsec_foundation::status::MakeStatus;
using ::hwsec_foundation::status::OkStatus;
} // namespace
AuthSessionManager::AuthSessionManager(AuthSession::BackingApis backing_apis)
: backing_apis_(backing_apis), clock_(base::DefaultClock::GetInstance()) {
CHECK(backing_apis.crypto);
CHECK(backing_apis.platform);
CHECK(backing_apis.user_session_map);
CHECK(backing_apis.keyset_management);
CHECK(backing_apis.auth_block_utility);
CHECK(backing_apis.auth_factor_driver_manager);
CHECK(backing_apis.auth_factor_manager);
CHECK(backing_apis.user_secret_stash_storage);
CHECK(backing_apis.user_metadata_reader);
CHECK(backing_apis.features);
}
CryptohomeStatusOr<InUseAuthSession> AuthSessionManager::CreateAuthSession(
const Username& account_id, uint32_t flags, AuthIntent auth_intent) {
// Assumption here is that keyset_management_ will outlive this AuthSession.
std::unique_ptr<AuthSession> auth_session =
AuthSession::Create(account_id, flags, auth_intent, backing_apis_);
return AddAuthSession(std::move(auth_session));
}
InUseAuthSession AuthSessionManager::AddAuthSession(
std::unique_ptr<AuthSession> auth_session) {
// We should never, ever, be able to get a token collision.
const auto& token = auth_session->token();
auto iter = auth_sessions_.lower_bound(token);
CHECK(iter == auth_sessions_.end() || iter->first != token)
<< "AuthSession token collision";
// Add an entry to the session map. Note that we're deliberately initializing
// things into an in-use state by only adding a blank entry in the map.
auth_sessions_.emplace_hint(iter, token, AuthSessionMapEntry{});
InUseAuthSession in_use(*this, /*is_session_active=*/true,
std::move(auth_session));
// Add an expiration entry for the session set to the end of time.
base::Time expiration_time = base::Time::Max();
expiration_map_.emplace(expiration_time, token);
ResetExpirationTimer();
// Attach the OnAuth handler to the AuthSession. It's important that we do
// this after creating the map entries and in_use object because the callback
// may immediately fire.
//
// Note that it is safe for use to use |Unretained| here because the manager
// should always outlive all of the sessions it owns.
in_use->AddOnAuthCallback(
base::BindOnce(&AuthSessionManager::SessionOnAuthCallback,
base::Unretained(this), token));
// Set the AuthFactorStatusUpdate signal handler to the auth session.
if (auth_factor_status_update_callback_) {
in_use->SetAuthFactorStatusUpdateCallback(
base::BindRepeating(auth_factor_status_update_callback_));
in_use->SendAuthFactorStatusUpdateSignal();
}
return in_use;
}
void AuthSessionManager::RemoveAllAuthSessions() {
auth_sessions_.clear();
expiration_map_.clear();
}
bool AuthSessionManager::RemoveAuthSession(
const base::UnguessableToken& token) {
// Remove the session from the expiration map. If we don't find an entry we
// ignore this and rely on the session map removal step to catch the error.
for (auto iter = expiration_map_.begin(); iter != expiration_map_.end();
++iter) {
if (iter->second == token) {
expiration_map_.erase(iter);
break;
}
}
// Remove the session from the session map.
return auth_sessions_.erase(token) == 1;
}
bool AuthSessionManager::RemoveAuthSession(
const std::string& serialized_token) {
std::optional<base::UnguessableToken> token =
AuthSession::GetTokenFromSerializedString(serialized_token);
if (!token.has_value()) {
LOG(ERROR) << "Unparsable AuthSession token for removal";
return false;
}
return RemoveAuthSession(token.value());
}
InUseAuthSession AuthSessionManager::FindAuthSession(
const std::string& serialized_token) {
std::optional<base::UnguessableToken> token =
AuthSession::GetTokenFromSerializedString(serialized_token);
if (!token.has_value()) {
LOG(ERROR) << "Unparsable AuthSession token for find";
return InUseAuthSession(*this, /*is_session_active=*/false, nullptr);
}
return FindAuthSession(token.value());
}
InUseAuthSession AuthSessionManager::FindAuthSession(
const base::UnguessableToken& token) {
auto it = auth_sessions_.find(token);
if (it == auth_sessions_.end()) {
return InUseAuthSession(*this, /*is_session_active=*/false, nullptr);
}
// If the AuthSessionManager doesn't own the AuthSession unique_ptr,
// then the AuthSession is actively in use for another dbus operation.
if (!it->second.session) {
return InUseAuthSession(*this, /*is_session_active=*/true, nullptr);
} else {
// By giving ownership of the unique_ptr we are marking
// the AuthSession as in active use.
return InUseAuthSession(*this, /*is_session_active=*/false,
std::move(it->second.session));
}
}
void AuthSessionManager::ResetExpirationTimer() {
if (expiration_map_.empty()) {
expiration_timer_.Stop();
} else {
expiration_timer_.Start(
FROM_HERE, expiration_map_.cbegin()->first,
base::BindOnce(&AuthSessionManager::ExpireAuthSessions,
base::Unretained(this)));
}
}
void AuthSessionManager::SessionOnAuthCallback(
const base::UnguessableToken& token) {
// Find the existing expiration time of the session.
auto iter = expiration_map_.begin();
while (iter != expiration_map_.end() && iter->second != token) {
++iter;
}
// If we couldn't find a session something really went wrong, but there's not
// much we can do about it.
if (iter == expiration_map_.end()) {
LOG(ERROR) << "AuthSessionManager received an OnAuth event for a session "
"which it is not managing";
return;
}
// Remove the existing expiration entry and add a new one that triggers
// starting now.
base::Time new_time = clock_->Now() + kAuthTimeout;
expiration_map_.erase(iter);
expiration_map_.emplace(new_time, token);
ResetExpirationTimer();
}
void AuthSessionManager::ExpireAuthSessions() {
base::Time now = clock_->Now();
// Go through the map, removing all of the sessions until we find one with an
// expiration time after now (or reach the end).
//
// This will always remove the first element of the map even if its expiration
// time is later than now. This is because it's possible for the timer to be
// triggered slightly early and we don't want this callback to turn into a
// busy-wait where it runs over and over as a no-op.
auto iter = expiration_map_.begin();
bool first_entry = true;
while (iter != expiration_map_.end() && (first_entry || iter->first <= now)) {
if (auth_sessions_.erase(iter->second) == 0) {
LOG(FATAL) << "AuthSessionManager expired a session it is not managing";
}
++iter;
first_entry = false;
}
// Erase all of the entries from the map that were just removed.
iter = expiration_map_.erase(expiration_map_.begin(), iter);
// Reset the expiration timer to run again based on what's left in the map.
ResetExpirationTimer();
}
void AuthSessionManager::MarkNotInUse(std::unique_ptr<AuthSession> session) {
// If the session token still exists in the session map then return ownership
// of the session back to the manager.
auto it = auth_sessions_.find(session->token());
if (it == auth_sessions_.end()) {
// If it doesn't exist then that means the session has been removed and so
// just return and allow the object to be destroyed.
return;
}
if (!it->second.pending_callbacks.IsEmpty()) {
it->second.pending_callbacks.Pop().Run(InUseAuthSession(
*this, /*is_session_active=*/false, std::move(session)));
return;
}
it->second.session = std::move(session);
}
void AuthSessionManager::SetAuthFactorStatusUpdateCallback(
const AuthFactorStatusUpdateCallback& callback) {
auth_factor_status_update_callback_ = callback;
}
void AuthSessionManager::RunWhenAvailable(
const std::string& serialized_token,
base::OnceCallback<void(InUseAuthSession)> callback) {
std::optional<base::UnguessableToken> token =
AuthSession::GetTokenFromSerializedString(serialized_token);
if (!token.has_value()) {
LOG(ERROR) << "Unparsable AuthSession token for find";
std::move(callback).Run(
InUseAuthSession(*this, /*is_session_active=*/false, nullptr));
return;
}
RunWhenAvailable(token.value(), std::move(callback));
}
void AuthSessionManager::RunWhenAvailable(
const base::UnguessableToken& token,
base::OnceCallback<void(InUseAuthSession)> callback) {
auto it = auth_sessions_.find(token);
if (it == auth_sessions_.end()) {
std::move(callback).Run(
InUseAuthSession(*this, /*is_session_active=*/false, nullptr));
return;
}
// If the AuthSessionManager doesn't own the AuthSession unique_ptr,
// then the AuthSession is actively in use for another dbus operation. Put the
// callback into the pending_callbacks queue.
if (!it->second.session) {
it->second.pending_callbacks.Push(std::move(callback));
return;
}
// By giving ownership of the unique_ptr we are marking
// the AuthSession as in active use.
std::move(callback).Run(InUseAuthSession(*this, /*is_session_active=*/false,
std::move(it->second.session)));
}
AuthSessionManager::PendingCallbacksQueue::~PendingCallbacksQueue() {
while (!IsEmpty()) {
Pop().Run(InUseAuthSession());
}
}
bool AuthSessionManager::PendingCallbacksQueue::IsEmpty() {
return callbacks_.empty();
}
void AuthSessionManager::PendingCallbacksQueue::Push(
base::OnceCallback<void(InUseAuthSession)> callback) {
callbacks_.push(std::move(callback));
}
base::OnceCallback<void(InUseAuthSession)>
AuthSessionManager::PendingCallbacksQueue::Pop() {
base::OnceCallback<void(InUseAuthSession)> callback =
std::move(callbacks_.front());
callbacks_.pop();
return callback;
}
InUseAuthSession::InUseAuthSession()
: manager_(nullptr), is_session_active_(false), session_(nullptr) {}
InUseAuthSession::InUseAuthSession(AuthSessionManager& manager,
bool is_session_active,
std::unique_ptr<AuthSession> session)
: manager_(&manager),
is_session_active_(is_session_active),
session_(std::move(session)) {}
InUseAuthSession::InUseAuthSession(InUseAuthSession&& auth_session)
: manager_(auth_session.manager_),
is_session_active_(auth_session.is_session_active_),
session_(std::move(auth_session.session_)) {}
InUseAuthSession& InUseAuthSession::operator=(InUseAuthSession&& auth_session) {
manager_ = auth_session.manager_;
is_session_active_ = auth_session.is_session_active_;
session_ = std::move(auth_session.session_);
return *this;
}
InUseAuthSession::~InUseAuthSession() {
if (session_ && manager_) {
manager_->MarkNotInUse(std::move(session_));
}
}
CryptohomeStatus InUseAuthSession::AuthSessionStatus() const {
if (!session_) {
// InUseAuthSession wasn't made with a valid AuthSession unique_ptr
if (is_session_active_) {
LOG(ERROR) << "Existing AuthSession is locked in a previous opertaion.";
return MakeStatus<CryptohomeError>(
CRYPTOHOME_ERR_LOC(kLocAuthSessionManagerAuthSessionActive),
ErrorActionSet({PossibleAction::kReboot}),
user_data_auth::CryptohomeErrorCode::
CRYPTOHOME_INVALID_AUTH_SESSION_TOKEN);
} else {
LOG(ERROR) << "Invalid AuthSession token provided.";
return MakeStatus<CryptohomeError>(
CRYPTOHOME_ERR_LOC(kLocAuthSessionManagerAuthSessionNotFound),
ErrorActionSet({PossibleAction::kReboot}),
user_data_auth::CryptohomeErrorCode::
CRYPTOHOME_INVALID_AUTH_SESSION_TOKEN);
}
} else {
return OkStatus<CryptohomeError>();
}
}
base::TimeDelta InUseAuthSession::GetRemainingTime() const {
// Find the expiration time of the session. If it doesn't have one then its
// expiration is pending the object no longer being in use, which we report as
// zero remaining time.
std::optional<base::Time> expiration_time;
for (const auto& [time, token] : manager_->expiration_map_) {
if (token == session_->token()) {
expiration_time = time;
break;
}
}
if (!expiration_time) {
return base::TimeDelta();
}
// If the expiration time is the end of time, then report the max duration.
if (expiration_time->is_max()) {
return base::TimeDelta::Max();
}
// Given the (finite) expiration time we now have, compute the remaining time.
// If the expiration time is in the past (e.g. because the expiration timer
// hasn't fired yet) then we clamp the time to zero.
base::TimeDelta time_left = *expiration_time - manager_->clock_->Now();
return time_left.is_negative() ? base::TimeDelta() : time_left;
}
CryptohomeStatus InUseAuthSession::ExtendTimeout(base::TimeDelta extension) {
// Find the existing expiration time of the session. If it doesn't have one
// then the session has already been expired pending the session no longer
// being in use. This cannot be reverted and so the extend fails.
auto iter = manager_->expiration_map_.begin();
while (iter != manager_->expiration_map_.end() &&
iter->second != session_->token()) {
++iter;
}
if (iter == manager_->expiration_map_.end()) {
return MakeStatus<CryptohomeError>(
CRYPTOHOME_ERR_LOC(kLocAuthSessionTimedOutInExtend),
ErrorActionSet({PossibleAction::kReboot, PossibleAction::kRetry,
PossibleAction::kDevCheckUnexpectedState}),
user_data_auth::CRYPTOHOME_INVALID_AUTH_SESSION_TOKEN);
}
// Remove the existing expiration entry and add a new one with the new time.
base::Time new_time =
std::max(iter->first, manager_->clock_->Now() + extension);
manager_->expiration_map_.erase(iter);
manager_->expiration_map_.emplace(new_time, session_->token());
manager_->ResetExpirationTimer();
return OkStatus<CryptohomeError>();
}
} // namespace cryptohome