blob: c15f5365d8d8bbdfd4af3911583dd47032bb9187 [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "kerberos/account_manager.h"
#include <limits>
#include <utility>
#include <base/base64.h>
#include <base/check.h>
#include <base/files/file_util.h>
#include <base/logging.h>
#include <base/stl_util.h>
#include <base/strings/string_util.h>
#include <libpasswordprovider/password.h>
#include <libpasswordprovider/password_provider.h>
#include "kerberos/error_strings.h"
#include "kerberos/kerberos_metrics.h"
#include "kerberos/krb5_interface.h"
#include "kerberos/krb5_jail_wrapper.h"
namespace kerberos {
namespace {
constexpr int kInvalidIndex = -1;
constexpr int kFileMode_rw =
base::FILE_PERMISSION_READ_BY_USER | base::FILE_PERMISSION_WRITE_BY_USER;
constexpr int kFileMode_rwxrwx =
base::FILE_PERMISSION_USER_MASK | base::FILE_PERMISSION_GROUP_MASK;
// Kerberos config files are stored as storage_dir/account_dir/this.
constexpr char kKrb5ConfFilePart[] = "krb5.conf";
// Kerberos credential caches are stored as storage_dir/account_dir/this.
constexpr char kKrb5CCFilePart[] = "krb5cc";
// Passwords are stored as storage_dir/account_dir/this.
constexpr char kPasswordFilePart[] = "password";
// Account data is stored as storage_dir + this.
constexpr char kAccountsFile[] = "accounts";
// Size limit for file (1 MB).
constexpr size_t kFileSizeLimit = 1024 * 1024;
// Returns the base64 encoded |principal_name|. This is used to create safe
// filenames while at the same time allowing easy debugging.
std::string GetSafeFilename(const std::string& principal_name) {
std::string encoded_principal;
base::Base64Encode(principal_name, &encoded_principal);
return encoded_principal;
}
// Reads the file at |path| into |data|. Returns |ERROR_LOCAL_IO| if the file
// could not be read.
WARN_UNUSED_RESULT ErrorType LoadFile(const base::FilePath& path,
std::string* data) {
data->clear();
if (!base::ReadFileToStringWithMaxSize(path, data, kFileSizeLimit)) {
PLOG(ERROR) << "Failed to read " << path.value();
data->clear();
return ERROR_LOCAL_IO;
}
return ERROR_NONE;
}
// Writes |data| to the file at |path|. Returns |ERROR_LOCAL_IO| if the file
// could not be written.
WARN_UNUSED_RESULT ErrorType SaveFile(const base::FilePath& path,
const std::string& data) {
const int data_size = static_cast<int>(data.size());
if (base::WriteFile(path, data.data(), data_size) != data_size) {
LOG(ERROR) << "Failed to write '" << path.value() << "'";
return ERROR_LOCAL_IO;
}
return ERROR_NONE;
}
// Sets file permissions for a given |path|. Returns ERROR_LOCAL_IO on error.
WARN_UNUSED_RESULT ErrorType SetFilePermissions(const base::FilePath& path,
int mode) {
if (!base::SetPosixFilePermissions(path, mode)) {
LOG(ERROR) << "Failed to set permissions on '" << path.value() << "'";
return ERROR_LOCAL_IO;
}
return ERROR_NONE;
}
} // namespace
AccountManager::AccountManager(
base::FilePath storage_dir,
KerberosFilesChangedCallback kerberos_files_changed,
KerberosTicketExpiringCallback kerberos_ticket_expiring,
std::unique_ptr<Krb5Interface> krb5,
std::unique_ptr<password_provider::PasswordProviderInterface>
password_provider,
KerberosMetrics* metrics)
: storage_dir_(std::move(storage_dir)),
accounts_path_(storage_dir_.Append(kAccountsFile)),
kerberos_files_changed_(std::move(kerberos_files_changed)),
kerberos_ticket_expiring_(std::move(kerberos_ticket_expiring)),
krb5_(std::move(krb5)),
password_provider_(std::move(password_provider)),
metrics_(metrics) {
DCHECK(kerberos_files_changed_);
DCHECK(kerberos_ticket_expiring_);
}
AccountManager::~AccountManager() = default;
ErrorType AccountManager::SaveAccounts() const {
// Copy |accounts_| into proto message.
AccountDataList storage_accounts;
for (const auto& account : accounts_)
*storage_accounts.add_accounts() = account.data;
// Store serialized proto message on disk.
std::string accounts_blob;
if (!storage_accounts.SerializeToString(&accounts_blob)) {
LOG(ERROR) << "Failed to serialize accounts list to string";
return ERROR_LOCAL_IO;
}
ErrorType error = SaveFile(accounts_path_, accounts_blob);
if (error != ERROR_NONE)
return error;
// Remove group and other read access. This prevents kerberosd-exec from
// reading it (it's none of its business).
return SetFilePermissions(accounts_path_, kFileMode_rw);
}
ErrorType AccountManager::LoadAccounts() {
accounts_.clear();
// A missing file counts as a file with empty data.
if (!base::PathExists(accounts_path_))
return ERROR_NONE;
// Load serialized proto blob.
std::string accounts_blob;
ErrorType error = LoadFile(accounts_path_, &accounts_blob);
if (error != ERROR_NONE)
return error;
// Parse blob into proto message.
AccountDataList storage_accounts;
if (!storage_accounts.ParseFromString(accounts_blob)) {
LOG(ERROR) << "Failed to parse accounts list from string";
return ERROR_LOCAL_IO;
}
// Copy data into |accounts_|.
accounts_.reserve(storage_accounts.accounts_size());
for (int n = 0; n < storage_accounts.accounts_size(); ++n) {
accounts_.emplace_back(std::move(*storage_accounts.mutable_accounts(n)),
this);
}
return ERROR_NONE;
}
ErrorType AccountManager::AddAccount(const std::string& principal_name,
bool is_managed) {
int index = GetAccountIndex(principal_name);
if (index != kInvalidIndex) {
// Policy should overwrite user-added accounts, but user-added accounts
// should not overwrite policy accounts.
if (!accounts_[index].data.is_managed() && is_managed) {
DeleteAllFilesFor(principal_name);
accounts_[index].data.set_is_managed(is_managed);
SaveAccounts();
}
return ERROR_DUPLICATE_PRINCIPAL_NAME;
}
// Create the account directory.
const base::FilePath account_dir = GetAccountDir(principal_name);
base::File::Error ferror;
if (!base::CreateDirectoryAndGetError(account_dir, &ferror)) {
LOG(ERROR) << "Failed to create directory '" << account_dir.value()
<< "': " << base::File::ErrorToString(ferror);
return ERROR_LOCAL_IO;
}
// The account directory needs to be group accessible since kinit runs as
// kerberosd-exec user and wants to write krbcc into that directory.
ErrorType error = SetFilePermissions(account_dir, kFileMode_rwxrwx);
if (error != ERROR_NONE) {
base::DeletePathRecursively(account_dir);
return error;
}
// Create account record.
AccountData data;
data.set_principal_name(principal_name);
data.set_is_managed(is_managed);
accounts_.emplace_back(std::move(data), this);
SaveAccounts();
return ERROR_NONE;
}
ErrorType AccountManager::RemoveAccount(const std::string& principal_name) {
int index = GetAccountIndex(principal_name);
if (index == kInvalidIndex)
return ERROR_UNKNOWN_PRINCIPAL_NAME;
DeleteAllFilesFor(principal_name);
accounts_.erase(accounts_.begin() + index);
SaveAccounts();
return ERROR_NONE;
}
void AccountManager::DeleteAllFilesFor(const std::string& principal_name) {
const bool krb5cc_existed = base::PathExists(GetKrb5CCPath(principal_name));
CHECK(base::DeletePathRecursively(GetAccountDir(principal_name)));
if (krb5cc_existed)
TriggerKerberosFilesChanged(principal_name);
}
ErrorType AccountManager::ClearAccounts(
ClearMode mode, std::unordered_set<std::string> keep_list) {
// Early out.
if (accounts_.size() == 0)
return ERROR_NONE;
for (auto it = accounts_.begin(); it != accounts_.end(); /* empty */) {
if (base::Contains(keep_list, it->data.principal_name())) {
++it;
continue;
}
switch (DetermineWhatToRemove(mode, *it)) {
case WhatToRemove::kNothing:
++it;
continue;
case WhatToRemove::kPassword:
CHECK(base::DeleteFile(GetPasswordPath(it->data.principal_name())));
++it;
continue;
case WhatToRemove::kAccount:
DeleteAllFilesFor(it->data.principal_name());
it = accounts_.erase(it);
continue;
}
}
SaveAccounts();
return ERROR_NONE;
}
std::vector<Account> AccountManager::ListAccounts() const {
std::vector<Account> accounts;
for (const auto& it : accounts_) {
Account account;
account.set_principal_name(it.data.principal_name());
account.set_is_managed(it.data.is_managed());
account.set_password_was_remembered(
base::PathExists(GetPasswordPath(it.data.principal_name())));
account.set_use_login_password(it.data.use_login_password());
// TODO(https://crbug.com/952239): Set additional properties.
// Do a best effort reporting results, don't bail on the first error. If
// there's a broken account, the user is able to recover the situation
// this way (reauthenticate or remove account and add back).
// Check PathExists, so that no error is printed if the file doesn't exist.
std::string krb5conf;
const base::FilePath krb5conf_path =
GetKrb5ConfPath(it.data.principal_name());
if (base::PathExists(krb5conf_path) &&
LoadFile(krb5conf_path, &krb5conf) == ERROR_NONE) {
account.set_krb5conf(krb5conf);
}
// A missing krb5cc file just translates to an invalid ticket (lifetime 0).
Krb5Interface::TgtStatus tgt_status;
const base::FilePath krb5cc_path = GetKrb5CCPath(it.data.principal_name());
if (base::PathExists(krb5cc_path) &&
krb5_->GetTgtStatus(krb5cc_path, &tgt_status) == ERROR_NONE) {
account.set_tgt_validity_seconds(tgt_status.validity_seconds);
account.set_tgt_renewal_seconds(tgt_status.renewal_seconds);
}
accounts.push_back(std::move(account));
}
return accounts;
}
ErrorType AccountManager::SetConfig(const std::string& principal_name,
const std::string& krb5conf) const {
const InternalAccount* account = GetAccount(principal_name);
if (!account)
return ERROR_UNKNOWN_PRINCIPAL_NAME;
// Validate configuration before setting it to make sure it doesn't contain
// invalid options.
ConfigErrorInfo error_info;
ErrorType error = krb5_->ValidateConfig(krb5conf, &error_info);
if (error != ERROR_NONE)
return error;
error = SaveFile(GetKrb5ConfPath(principal_name), krb5conf);
// Triggering the signal is only necessary if the credential cache exists.
if (error == ERROR_NONE && base::PathExists(GetKrb5CCPath(principal_name)))
TriggerKerberosFilesChanged(principal_name);
return error;
}
ErrorType AccountManager::ValidateConfig(const std::string& krb5conf,
ConfigErrorInfo* error_info) const {
return krb5_->ValidateConfig(krb5conf, error_info);
}
ErrorType AccountManager::AcquireTgt(const std::string& principal_name,
std::string password,
bool remember_password,
bool use_login_password) {
InternalAccount* account = GetMutableAccount(principal_name);
if (!account)
return ERROR_UNKNOWN_PRINCIPAL_NAME;
// Remember whether to use the login password.
if (account->data.use_login_password() != use_login_password) {
account->data.set_use_login_password(use_login_password);
SaveAccounts();
}
ErrorType error = use_login_password
? UpdatePasswordFromLogin(principal_name, &password)
: UpdatePasswordFromSaved(principal_name,
remember_password, &password);
if (error != ERROR_NONE)
return error;
// Acquire a Kerberos ticket-granting-ticket.
error =
krb5_->AcquireTgt(principal_name, password, GetKrb5CCPath(principal_name),
GetKrb5ConfPath(principal_name));
if (error == ERROR_NONE) {
// Schedule task to automatically renew the ticket. If the ticket is invalid
// for whatever reason, don't notify expiration immediately. This might lead
// to an infinite loop when a password is stored and MaybeAutoAcquireTgt
// tries to acquire a new TGT immediately.
account->tgt_renewal_scheduler_->ScheduleRenewal(
false /* notify_expiration */);
// Assume the ticket changed if AcquireTgt() was successful.
TriggerKerberosFilesChanged(principal_name);
std::string krb5conf;
ErrorType load_config_error =
LoadFile(GetKrb5ConfPath(principal_name), &krb5conf);
if (load_config_error == ERROR_NONE) {
KerberosEncryptionTypes encryption_types;
bool success =
config_parser_.GetEncryptionTypes(krb5conf, &encryption_types);
if (success) {
metrics_->ReportKerberosEncryptionTypes(encryption_types);
}
}
}
// Trying to acquire a ticket qualifies this user as an active user, so report
// stats.
MaybeReportDailyUsageStats();
return error;
}
ErrorType AccountManager::GetKerberosFiles(const std::string& principal_name,
KerberosFiles* files) const {
// Trying to get Kerberos files qualifies this user as an active user, so
// report stats.
MaybeReportDailyUsageStats();
files->clear_krb5cc();
files->clear_krb5conf();
const InternalAccount* account = GetAccount(principal_name);
if (!account)
return ERROR_UNKNOWN_PRINCIPAL_NAME;
// By convention, no credential cache means no error.
const base::FilePath krb5cc_path = GetKrb5CCPath(principal_name);
if (!base::PathExists(krb5cc_path))
return ERROR_NONE;
std::string krb5cc;
ErrorType error = LoadFile(krb5cc_path, &krb5cc);
if (error != ERROR_NONE)
return error;
std::string krb5conf;
error = LoadFile(GetKrb5ConfPath(principal_name), &krb5conf);
if (error != ERROR_NONE)
return error;
files->mutable_krb5cc()->assign(krb5cc.begin(), krb5cc.end());
files->mutable_krb5conf()->assign(krb5conf.begin(), krb5conf.end());
return ERROR_NONE;
}
void AccountManager::StartObservingTickets() {
for (const auto& account : accounts_) {
const base::FilePath krb5cc_path =
GetKrb5CCPath(account.data.principal_name());
// Might happen for managed accounts (e.g. misconfigured password). Chrome
// only allows adding unmanaged accounts if a ticket can be acquired.
if (!base::PathExists(krb5cc_path))
continue;
// A ticket where GetTgtStatus fails is considered broken and hence invalid.
Krb5Interface::TgtStatus tgt_status;
if (krb5_->GetTgtStatus(krb5cc_path, &tgt_status) != ERROR_NONE ||
tgt_status.validity_seconds <= 0) {
NotifyTgtExpiration(account.data.principal_name(),
TgtRenewalScheduler::TgtExpiration::kExpired);
continue;
}
// Ticket is valid. Schedule task to automatically renew it.
account.tgt_renewal_scheduler_->ScheduleRenewal(
true /* notify_expiration */);
}
}
// static
std::string AccountManager::GetSafeFilenameForTesting(
const std::string& principal_name) {
return GetSafeFilename(principal_name);
}
void AccountManager::WrapKrb5ForTesting() {
krb5_ = std::make_unique<Krb5JailWrapper>(std::move(krb5_));
}
void AccountManager::TriggerKerberosFilesChanged(
const std::string& principal_name) const {
kerberos_files_changed_.Run(principal_name);
}
void AccountManager::TriggerKerberosTicketExpiring(
const std::string& principal_name) const {
kerberos_ticket_expiring_.Run(principal_name);
}
ErrorType AccountManager::GetTgtStatus(const std::string& principal_name,
Krb5Interface::TgtStatus* tgt_status) {
return krb5_->GetTgtStatus(GetKrb5CCPath(principal_name), tgt_status);
}
ErrorType AccountManager::RenewTgt(const std::string& principal_name) {
ErrorType error =
krb5_->RenewTgt(principal_name, GetKrb5CCPath(principal_name),
GetKrb5ConfPath(principal_name));
if (error != ERROR_NONE) {
VLOG(1) << "RenewTgt failed with " << GetErrorString(error);
// Renewal didn't work. See if we have a password stored and try to
// auto-renew.
MaybeAutoAcquireTgt(principal_name, &error);
}
last_renew_tgt_error_for_testing_ = error;
return error;
}
void AccountManager::NotifyTgtExpiration(
const std::string& principal_name,
TgtRenewalScheduler::TgtExpiration expiration) {
// First try to auto-acquire the TGT (usually works if password is stored).
// Only if that isn't possible or doesn't work, trigger the signal.
ErrorType error = ERROR_NONE;
if (!MaybeAutoAcquireTgt(principal_name, &error) || error != ERROR_NONE) {
// TODO(https://crbug.com/952245): Distinguish between "about to expire" and
// "expired" in the KerberosTicketExpiring signal and in the Chrome
// notification.
TriggerKerberosTicketExpiring(principal_name);
}
}
bool AccountManager::MaybeAutoAcquireTgt(const std::string& principal_name,
ErrorType* error) {
InternalAccount* account = GetMutableAccount(principal_name);
DCHECK(account);
// Check if |account| has access to the password.
const bool use_login_password = account->data.use_login_password();
const bool password_was_remembered =
base::PathExists(GetPasswordPath(principal_name));
if (!use_login_password && !password_was_remembered)
return false;
// Should not have remembered login password ourselves.
DCHECK(!(use_login_password && password_was_remembered));
VLOG(1) << "Auto-acquiring new TGT using "
<< (use_login_password ? "login" : "remembered") << " password";
*error = AcquireTgt(principal_name, std::string() /* password */,
password_was_remembered /* keep remembering */,
use_login_password);
if (*error != ERROR_NONE)
VLOG(1) << "Auto-acquiring TGT failed with " << GetErrorString(*error);
return true;
}
base::FilePath AccountManager::GetAccountDir(
const std::string& principal_name) const {
return storage_dir_.Append(GetSafeFilename(principal_name));
}
base::FilePath AccountManager::GetKrb5ConfPath(
const std::string& principal_name) const {
return GetAccountDir(principal_name).Append(kKrb5ConfFilePart);
}
base::FilePath AccountManager::GetKrb5CCPath(
const std::string& principal_name) const {
return GetAccountDir(principal_name).Append(kKrb5CCFilePart);
}
base::FilePath AccountManager::GetPasswordPath(
const std::string& principal_name) const {
return GetAccountDir(principal_name).Append(kPasswordFilePart);
}
ErrorType AccountManager::UpdatePasswordFromLogin(
const std::string& principal_name, std::string* password) {
// Erase a previously remembered password.
base::DeleteFile(GetPasswordPath(principal_name));
// Get login password from |password_provider_|.
std::unique_ptr<password_provider::Password> login_password =
password_provider_->GetPassword();
if (!login_password || login_password->size() == 0) {
password->clear();
LOG(WARNING) << "Unable to retrieve login password";
} else {
*password = std::string(login_password->GetRaw(), login_password->size());
}
return ERROR_NONE;
}
ErrorType AccountManager::UpdatePasswordFromSaved(
const std::string& principal_name,
bool remember_password,
std::string* password) {
// Decision table what to do with the password:
// pw empty / remember| false | true
// -------------------+----------------------------+------------------------
// false | use given, erase file | use given, save to file
// true | load from file, erase file | load from file
// Remember password (even if authentication is going to fail below).
const base::FilePath password_path = GetPasswordPath(principal_name);
if (!password->empty() && remember_password) {
ErrorType error = SaveFile(password_path, *password);
if (error != ERROR_NONE)
return error;
// Remove group and other read access, just keep kerberosd rw. This prevents
// kerberosd-exec from accessing the password.
error = SetFilePermissions(password_path, kFileMode_rw);
if (error != ERROR_NONE) {
// Do a best effort removing the password.
base::DeleteFile(password_path);
return error;
}
}
// Try to load a saved password if available and none is given.
if (password->empty() && base::PathExists(password_path)) {
ErrorType error = LoadFile(password_path, password);
if (error != ERROR_NONE)
return error;
}
// Erase a previously remembered password.
if (!remember_password)
base::DeleteFile(password_path);
return ERROR_NONE;
}
void AccountManager::MaybeReportDailyUsageStats() const {
// Did a day pass already?
if (!metrics_->ShouldReportDailyUsageStats())
return;
// Count different kinds of accounts.
int total_count = static_cast<int>(accounts_.size());
int managed_count = 0;
int unmanaged_count = 0;
int remembered_password_count = 0;
int use_login_password_count = 0;
for (const auto& account : accounts_) {
if (account.data.is_managed())
managed_count++;
else
unmanaged_count++;
if (base::PathExists(GetPasswordPath(account.data.principal_name())))
remembered_password_count++;
if (account.data.use_login_password())
use_login_password_count++;
}
// Report UMA stats.
metrics_->ReportDailyUsageStats(total_count, managed_count, unmanaged_count,
remembered_password_count,
use_login_password_count);
}
AccountManager::InternalAccount::InternalAccount(
AccountData&& _data, TgtRenewalScheduler::Delegate* delegate)
: data(std::move(_data)),
tgt_renewal_scheduler_(std::make_unique<TgtRenewalScheduler>(
data.principal_name(), delegate)) {}
int AccountManager::GetAccountIndex(const std::string& principal_name) const {
for (size_t n = 0; n < accounts_.size(); ++n) {
if (accounts_[n].data.principal_name() == principal_name) {
CHECK(n <= std::numeric_limits<int>::max());
return static_cast<int>(n);
}
}
return kInvalidIndex;
}
const AccountManager::InternalAccount* AccountManager::GetAccount(
const std::string& principal_name) const {
int index = GetAccountIndex(principal_name);
return index != kInvalidIndex ? &accounts_[index] : nullptr;
}
AccountManager::InternalAccount* AccountManager::GetMutableAccount(
const std::string& principal_name) {
int index = GetAccountIndex(principal_name);
return index != kInvalidIndex ? &accounts_[index] : nullptr;
}
AccountManager::WhatToRemove AccountManager::DetermineWhatToRemove(
ClearMode mode, const InternalAccount& account) {
switch (mode) {
case CLEAR_ALL:
return WhatToRemove::kAccount;
case CLEAR_ONLY_MANAGED_ACCOUNTS:
return account.data.is_managed() ? WhatToRemove::kAccount
: WhatToRemove::kNothing;
case CLEAR_ONLY_UNMANAGED_ACCOUNTS:
return !account.data.is_managed() ? WhatToRemove::kAccount
: WhatToRemove::kNothing;
case CLEAR_ONLY_UNMANAGED_REMEMBERED_PASSWORDS:
return !account.data.is_managed() ? WhatToRemove::kPassword
: WhatToRemove::kNothing;
}
return WhatToRemove::kNothing;
}
} // namespace kerberos