blob: 3c76614289b7b7bd21abf73faa5f2ebae4910796 [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 <climits>
#include <cstddef>
#include <iterator>
#include <memory>
#include <optional>
#include <set>
#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 <base/unguessable_token.h>
#include <cryptohome/proto_bindings/UserDataAuth.pb.h>
#include <libhwsec/status.h>
#include <libstorage/platform/platform.h>
#include "cryptohome/error/location_utils.h"
#include "cryptohome/username.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.features);
}
base::UnguessableToken AuthSessionManager::CreateAuthSession(
const Username& account_id, uint32_t flags, AuthIntent auth_intent) {
std::unique_ptr<AuthSession> auth_session =
AuthSession::Create(account_id, flags, auth_intent, backing_apis_);
return AddAuthSession(std::move(auth_session));
}
base::UnguessableToken AuthSessionManager::CreateAuthSession(
AuthSession::Params auth_session_params) {
return AddAuthSession(std::make_unique<AuthSession>(
std::move(auth_session_params), backing_apis_));
}
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 still try to remove the underlying session.
bool auth_session_removed = false;
{
auto iter = expiration_map_.begin();
while (iter != expiration_map_.end()) {
if (iter->second == token) {
expiration_map_.erase(iter);
auth_session_removed = true;
break;
}
++iter;
}
}
// If the entry wasn't in the expiration map it might be in the expiring soon
// map, so check it as well.
if (!auth_session_removed) {
auto expiring_iter = auth_session_expiring_soon_map_.begin();
while (expiring_iter != auth_session_expiring_soon_map_.end()) {
if (expiring_iter->second == token) {
auth_session_expiring_soon_map_.erase(expiring_iter);
auth_session_removed = true;
break;
}
++expiring_iter;
}
}
if (!auth_session_removed) {
return false;
}
// In case anything is removed, reset the timer. If nothing is removed, it'll
// just reset the timer and would essentially be a no-op.
ResetExpirationTimer();
// Find entries for the token in the token and user
// maps. If any of the lookups fail we report an error.
auto token_iter = token_to_user_.find(token);
if (token_iter == token_to_user_.end()) {
return false;
}
auto user_iter = user_auth_sessions_.find(token_iter->second);
if (user_iter == user_auth_sessions_.end()) {
return false;
}
auto session_iter = user_iter->second.auth_sessions.find(token);
if (session_iter == user_iter->second.auth_sessions.end()) {
return false;
}
// If we get here we found all the entries for this session, remove them all
// and report success. If the session is in use, also mark is as the zombie
// session so that we know the user is still busy.
if (session_iter->second == nullptr) {
user_iter->second.zombie_session = token;
}
user_iter->second.auth_sessions.erase(session_iter);
if (!user_iter->second.zombie_session.has_value() &&
user_iter->second.auth_sessions.empty()) {
user_auth_sessions_.erase(user_iter);
}
token_to_user_.erase(token_iter);
return true;
}
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());
}
void AuthSessionManager::RemoveUserAuthSessions(
const ObfuscatedUsername& username) {
std::set<base::UnguessableToken> tokens_being_removed;
for (auto iter = token_to_user_.begin(); iter != token_to_user_.end();) {
if (iter->second == username) {
tokens_being_removed.insert(iter->first);
iter = token_to_user_.erase(iter);
} else {
++iter;
}
}
user_auth_sessions_.erase(username);
for (auto iter = expiration_map_.begin(); iter != expiration_map_.end();) {
if (tokens_being_removed.contains(iter->second)) {
iter = expiration_map_.erase(iter);
} else {
++iter;
}
}
for (auto iter = auth_session_expiring_soon_map_.begin();
iter != auth_session_expiring_soon_map_.end();) {
if (tokens_being_removed.contains(iter->second)) {
iter = auth_session_expiring_soon_map_.erase(iter);
} else {
++iter;
}
}
ResetExpirationTimer();
}
void AuthSessionManager::RemoveAllAuthSessions() {
token_to_user_.clear();
user_auth_sessions_.clear();
expiration_map_.clear();
auth_session_expiring_soon_map_.clear();
ResetExpirationTimer();
}
void AuthSessionManager::RunWhenAvailable(
const base::UnguessableToken& token,
base::OnceCallback<void(InUseAuthSession)> callback) {
PendingWork work(token, std::move(callback));
// Look up the user sessions instance for the given token. If it doesn't exist
// just execute the callback immediately with an invalid InUse object.
auto token_iter = token_to_user_.find(token);
if (token_iter == token_to_user_.end()) {
return;
}
auto user_iter = user_auth_sessions_.find(token_iter->second);
if (user_iter == user_auth_sessions_.end()) {
return;
}
// Check if the user is busy, i.e. if they have any sessions that are
// currently in use. If they are, add an item to the pending work queue.
if (user_iter->second.zombie_session.has_value()) {
user_iter->second.work_queue.push(std::move(work));
return;
}
for (const auto& [unused_token, session] : user_iter->second.auth_sessions) {
if (!session) {
user_iter->second.work_queue.push(std::move(work));
return;
}
}
// If we get here then the user is not busy, execute the callback immediately.
auto session_iter = user_iter->second.auth_sessions.find(token);
if (session_iter == user_iter->second.auth_sessions.end()) {
return;
}
std::move(work).Run(InUseAuthSession(*this, std::move(session_iter->second)));
}
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());
return;
}
RunWhenAvailable(token.value(), std::move(callback));
}
base::UnguessableToken AuthSessionManager::AddAuthSession(
std::unique_ptr<AuthSession> auth_session) {
// Find the insertion location in the token->user map. We should never, ever,
// be able to get a token collision.
const auto token = auth_session->token();
const ObfuscatedUsername username = auth_session->obfuscated_username();
auto token_iter = token_to_user_.lower_bound(token);
CHECK(token_iter == token_to_user_.end() || token_iter->first != token)
<< "AuthSession token collision";
// Find the insertion location in the user->session map. This may create a new
// map implicitly if this is the first session for this user. Again, we should
// never, ever be able to get a token collision.
auto& user_entry = user_auth_sessions_[username];
auto session_iter = user_entry.auth_sessions.lower_bound(token);
CHECK(session_iter == user_entry.auth_sessions.end() ||
session_iter->first != token)
<< "AuthSession token collision";
// Add entries to both maps.
token_to_user_.emplace_hint(token_iter, token, username);
session_iter = user_entry.auth_sessions.emplace_hint(session_iter, token,
std::move(auth_session));
AuthSession& added_session = *session_iter->second;
// 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();
// Trigger a status update for the newly added session.
added_session.SendAuthFactorStatusUpdateSignal();
// Attach the OnAuth handler to the AuthSession. It's important that we do
// this after creating the map entries because the callback may immediately
// fire. We should also avoid touching the session after this because it's
// technically possible for it to have been destroyed.
added_session.AddOnAuthCallback(
base::BindOnce(&AuthSessionManager::SessionOnAuthCallback,
weak_factory_.GetWeakPtr(), token));
return token;
}
void AuthSessionManager::MoveAuthSessionsToExpiringSoon() {
auto iter = expiration_map_.begin();
bool need_moving = false;
while (iter != expiration_map_.end() &&
(iter->first - clock_->Now()) <= kAuthTimeoutWarning) {
auto token_iter = token_to_user_.find(iter->second);
if (token_iter == token_to_user_.end()) {
continue;
}
// If the sending the signal fails or is not sent because of authsession not
// found, that's fine for now since this is informational.
if (user_auth_sessions_.find(token_to_user_[iter->second]) !=
user_auth_sessions_.end() &&
backing_apis_.signalling) {
user_data_auth::AuthSessionExpiring expiring_proto;
auto broadcast_id =
user_auth_sessions_.find(token_to_user_[iter->second])
->second.auth_sessions[iter->second]
->serialized_public_token();
expiring_proto.set_broadcast_id(broadcast_id);
expiring_proto.set_time_left((iter->first - clock_->Now()).InSeconds());
backing_apis_.signalling->SendAuthSessionExpiring(expiring_proto);
}
++iter;
need_moving = true;
}
if (need_moving) {
std::copy(std::make_move_iterator(expiration_map_.begin()),
std::make_move_iterator(iter),
std::inserter(auth_session_expiring_soon_map_,
auth_session_expiring_soon_map_.begin()));
// Clearing it explicitly in order to avoid a undefined state.
expiration_map_.erase(expiration_map_.cbegin(), iter);
}
ResetExpirationTimer();
}
void AuthSessionManager::ResetExpirationTimer() {
if (auth_session_expiring_soon_map_.empty() && expiration_map_.empty()) {
expiration_timer_.Stop();
return;
}
if (auth_session_expiring_soon_map_.empty() ||
(!expiration_map_.empty() &&
((expiration_map_.cbegin()->first - kAuthTimeoutWarning) <
auth_session_expiring_soon_map_.cbegin()->first))) {
expiration_timer_.Start(
FROM_HERE, expiration_map_.cbegin()->first - kAuthTimeoutWarning,
base::BindOnce(&AuthSessionManager::MoveAuthSessionsToExpiringSoon,
weak_factory_.GetWeakPtr()));
return;
}
expiration_timer_.Start(
FROM_HERE, auth_session_expiring_soon_map_.cbegin()->first,
base::BindOnce(&AuthSessionManager::ExpireAuthSessions,
weak_factory_.GetWeakPtr()));
}
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 then check the expiring soon map.
if (iter == expiration_map_.end()) {
CheckExpiringSoonMap(token);
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::CheckExpiringSoonMap(
const base::UnguessableToken& token) {
// Find the existing expiration time of the session.
auto iter = auth_session_expiring_soon_map_.begin();
while (iter != auth_session_expiring_soon_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 == auth_session_expiring_soon_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;
auth_session_expiring_soon_map_.erase(iter);
// We add it to the new map.
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 = auth_session_expiring_soon_map_.begin();
bool first_entry = true;
while (iter != auth_session_expiring_soon_map_.end() &&
(first_entry || iter->first <= now)) {
auto token_iter = token_to_user_.find(iter->second);
if (token_iter == token_to_user_.end()) {
LOG(FATAL) << "token_iter: AuthSessionManager expired a session it is "
"not managing";
}
auto user_iter = user_auth_sessions_.find(token_iter->second);
if (user_iter == user_auth_sessions_.end()) {
LOG(FATAL) << "user_iter:AuthSessionManager expired a session it "
"is not managing";
}
auto session_iter = user_iter->second.auth_sessions.find(iter->second);
if (session_iter == user_iter->second.auth_sessions.end()) {
LOG(FATAL) << "session_iter:AuthSessionManager expired a session it is "
"not managing";
}
if (session_iter->second == nullptr) {
user_iter->second.zombie_session = iter->second;
}
user_iter->second.auth_sessions.erase(session_iter);
token_to_user_.erase(token_iter);
if (!user_iter->second.zombie_session.has_value() &&
user_iter->second.auth_sessions.empty()) {
user_auth_sessions_.erase(user_iter);
}
++iter;
first_entry = false;
}
// Erase all of the entries from the map that were just removed.
iter = auth_session_expiring_soon_map_.erase(
auth_session_expiring_soon_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) {
// Find the session map for this session's user. If no such map exists then
// this session has been removed and there are no sessions (or work) left for
// this user. Just return and let |session| be destroyed.
auto user_iter = user_auth_sessions_.find(session->obfuscated_username());
if (user_iter == user_auth_sessions_.end()) {
return;
}
// The user is still active. Return this session to the session map. If its
// entry no longer exists then the session has been removed and we can destroy
// |session|, but we still need to kick off any pending work the user has.
auto& session_map = user_iter->second.auth_sessions;
auto session_iter = session_map.find(session->token());
if (session_iter == session_map.end()) {
CHECK_EQ(*user_iter->second.zombie_session, session->token());
user_iter->second.zombie_session = std::nullopt;
session = nullptr;
} else {
session_iter->second = std::move(session);
}
// Run the next item in the work queue. Note that if the next element was
// scheduled against a session that no longer exists, we need to keep going
// until we find work that can actually run (or until the queue is empty).
auto& work_queue = user_iter->second.work_queue;
while (!work_queue.empty()) {
PendingWork work = std::move(work_queue.front());
work_queue.pop();
session_iter = session_map.find(work.session_token());
if (session_iter != session_map.end()) {
std::move(work).Run(
InUseAuthSession(*this, std::move(session_iter->second)));
return;
}
}
}
AuthSessionManager::PendingWork::PendingWork(
base::UnguessableToken session_token, Callback work_callback)
: session_token_(std::move(session_token)),
work_callback_(std::move(work_callback)) {}
AuthSessionManager::PendingWork::PendingWork(PendingWork&& other)
: session_token_(std::move(other.session_token_)),
work_callback_(std::move(other.work_callback_)) {
other.work_callback_ = std::nullopt;
}
AuthSessionManager::PendingWork& AuthSessionManager::PendingWork::operator=(
PendingWork&& other) {
session_token_ = std::move(other.session_token_);
work_callback_ = std::move(other.work_callback_);
other.work_callback_ = std::nullopt;
return *this;
}
AuthSessionManager::PendingWork::~PendingWork() {
if (work_callback_) {
std::move(*work_callback_).Run(InUseAuthSession());
}
}
void AuthSessionManager::PendingWork::Run(InUseAuthSession session) && {
if (!work_callback_) {
LOG(FATAL) << "Attempting to run work multiple times";
}
Callback work = std::move(*work_callback_);
work_callback_ = std::nullopt;
std::move(work).Run(std::move(session));
}
InUseAuthSession::InUseAuthSession() : manager_(nullptr), session_(nullptr) {}
InUseAuthSession::InUseAuthSession(AuthSessionManager& manager,
std::unique_ptr<AuthSession> session)
: manager_(&manager), session_(std::move(session)) {}
InUseAuthSession::InUseAuthSession(InUseAuthSession&& auth_session)
: manager_(auth_session.manager_),
session_(std::move(auth_session.session_)) {}
InUseAuthSession& InUseAuthSession::operator=(InUseAuthSession&& auth_session) {
if (session_ && manager_) {
manager_->MarkNotInUse(std::move(session_));
}
manager_ = auth_session.manager_;
session_ = std::move(auth_session.session_);
return *this;
}
InUseAuthSession::~InUseAuthSession() {
if (session_ && manager_) {
manager_->MarkNotInUse(std::move(session_));
}
}
CryptohomeStatus InUseAuthSession::AuthSessionStatus() const {
if (session_ && manager_) {
return OkStatus<CryptohomeError>();
}
return MakeStatus<CryptohomeError>(
CRYPTOHOME_ERR_LOC(kLocAuthSessionManagerAuthSessionNotFound),
ErrorActionSet({PossibleAction::kReboot}),
user_data_auth::CryptohomeErrorCode::
CRYPTOHOME_INVALID_AUTH_SESSION_TOKEN);
}
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) {
for (const auto& [time, token] :
manager_->auth_session_expiring_soon_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 we check the expring soon map.
auto iter = manager_->expiration_map_.begin();
while (iter != manager_->expiration_map_.end() &&
iter->second != session_->token()) {
++iter;
}
if (iter == manager_->expiration_map_.end()) {
return ExtendExpiringSoonTimeout(extension);
}
// 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>();
}
std::unique_ptr<BoundAuthSession> InUseAuthSession::BindForCallback() && {
return std::make_unique<BoundAuthSession>(std::move(*this));
}
void InUseAuthSession::Release() && {
*this = InUseAuthSession();
}
BoundAuthSession::BoundAuthSession(InUseAuthSession auth_session)
: session_(std::move(auth_session)) {
// Setup the initial timeout, unless the session this is bound to is already
// invalid and so releasing it would be redundant.
if (session_.AuthSessionStatus().ok()) {
ScheduleReleaseCheck(kTimeout);
}
}
InUseAuthSession BoundAuthSession::Take() && {
timeout_timer_.Stop();
return std::move(session_);
}
void BoundAuthSession::ReleaseSessionIfBlocking() {
// If the session is already gone, nothing to do.
if (!session_.AuthSessionStatus().ok()) {
return;
}
// If the session is blocking any pending work, release it.
const auto& session_map = session_.manager_->user_auth_sessions_;
auto sessions_iter = session_map.find(session_->obfuscated_username());
if (sessions_iter != session_map.end() &&
!sessions_iter->second.work_queue.empty()) {
LOG(WARNING)
<< "Timeout on bound auth session, releasing back to session manager";
session_->CancelAllOutstandingAsyncCallbacks();
session_ = InUseAuthSession();
return;
}
// If we get here the session is still live but isn't blocking anything so
// reset the timer to check again.
ScheduleReleaseCheck(kShortTimeout);
}
void BoundAuthSession::ScheduleReleaseCheck(base::TimeDelta delay) {
// It's safe to use Unretained here because if |this| is destroyed then the
// timer will be destroyed and the callback cancelled.
timeout_timer_.Start(
FROM_HERE, session_.manager_->clock_->Now() + delay,
base::BindOnce(&BoundAuthSession::ReleaseSessionIfBlocking,
base::Unretained(this)));
}
CryptohomeStatus InUseAuthSession::ExtendExpiringSoonTimeout(
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_->auth_session_expiring_soon_map_.begin();
while (iter != manager_->auth_session_expiring_soon_map_.end() &&
iter->second != session_->token()) {
++iter;
}
if (iter == manager_->auth_session_expiring_soon_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_->auth_session_expiring_soon_map_.erase(iter);
manager_->expiration_map_.emplace(new_time, session_->token());
manager_->ResetExpirationTimer();
return OkStatus<CryptohomeError>();
}
} // namespace cryptohome