keysmith/src/account/account_p.cpp

789 lines
27 KiB
C++

/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2020-2021 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
*/
#include "account_p.h"
#include "validation.h"
#include "../logging_p.h"
#include <QObject>
#include <QTimer>
KEYSMITH_LOGGER(logger, ".accounts.account_p")
namespace accounts
{
QUuid AccountPrivate::id(void) const
{
return m_id;
}
std::optional<uint> AccountPrivate::offset(void) const
{
return m_offset;
}
QString AccountPrivate::name(void) const
{
return m_name;
}
QString AccountPrivate::issuer(void) const
{
return m_issuer;
}
QString AccountPrivate::token(void) const
{
return m_token;
}
quint64 AccountPrivate::counter(void) const
{
return m_counter;
}
QDateTime AccountPrivate::epoch(void) const
{
return m_epoch;
}
uint AccountPrivate::timeStep(void) const
{
return m_timeStep;
}
Account::Hash AccountPrivate::hash(void) const
{
return m_hash;
}
Account::Algorithm AccountPrivate::algorithm(void) const
{
return m_algorithm;
}
int AccountPrivate::tokenLength(void) const
{
return m_tokenLength;
}
bool AccountPrivate::checksum(void) const
{
return m_checksum;
}
void AccountPrivate::setCounter(quint64 counter)
{
Q_Q(Account);
if (!m_is_still_alive) {
qCDebug(logger)
<< "Will not set counter for account:" << m_id
<< "Object marked for death";
return;
}
if (!m_storage->isStillOpen()) {
qCDebug(logger)
<< "Will not set counter for account:" << m_id
<< "Storage no longer open";
return;
}
if (m_algorithm != Account::Algorithm::Hotp) {
qCDebug(logger)
<< "Will not set counter for account:" << m_id
<< "Algorithm not applicable:" << m_algorithm;
return;
}
qCDebug(logger) << "Requesting to store updated details for account:" << m_id;
SaveHotp *job = new SaveHotp(m_storage->settings(),
m_id, m_name, m_issuer, m_secret, m_tokenLength,
counter, m_offset, m_checksum);
m_actions->queueAndProceed(job, [counter, job, q, this](void) -> void
{
new HandleCounterUpdate(this, m_storage, counter, job, q);
});
}
void AccountPrivate::acceptCounter(quint64 counter)
{
Q_Q(Account);
if (!m_is_still_alive) {
qCDebug(logger)
<< "Ignoring counter update for account:" << m_id
<< "Object marked for death";
return;
}
if (!m_storage->isStillOpen()) {
qCDebug(logger)
<< "Ignoring counter update for account:" << m_id
<< "Storage no longer open";
return;
}
if (m_algorithm != Account::Algorithm::Hotp) {
qCDebug(logger)
<< "Ignoring counter update for account:" << m_id
<< "Algorithm not applicable:" << m_algorithm;
return;
}
if (m_counter != counter) {
qCDebug(logger) << "Counter is updated for account:" << m_id;
m_counter = counter;
Q_EMIT q->updated();
recompute();
}
}
void AccountPrivate::acceptTotpTokens(const QString &token, const QString &nextToken,
const QDateTime &validFrom, const QDateTime &validUntil)
{
if (!m_is_still_alive) {
qCDebug(logger)
<< "Ignoring token update for account:" << m_id
<< "Object marked for death";
return;
}
if (!m_storage->isStillOpen()) {
qCDebug(logger)
<< "Ignoring token update for account:" << m_id
<< "Storage no longer open";
return;
}
m_nextToken = nextToken;
m_nextTotpValidFrom = validFrom;
m_nextTotpValidUntil = validUntil;
setToken(token);
}
void AccountPrivate::acceptHotpTokens(const QString &token, const QString &nextToken, quint64 nextCounter)
{
if (!m_is_still_alive) {
qCDebug(logger)
<< "Ignoring token update for account:" << m_id
<< "Object marked for death";
return;
}
if (!m_storage->isStillOpen()) {
qCDebug(logger)
<< "Ignoring token update for account:" << m_id
<< "Storage no longer open";
return;
}
m_nextToken = nextToken;
m_nextCounter = nextCounter;
setToken(token);
}
void AccountPrivate::setToken(const QString &token)
{
Q_Q(Account);
if (m_token != token) {
qCDebug(logger) << "Token is updated for account:" << m_id;
m_token = token;
Q_EMIT q->tokenChanged(m_token);
}
}
void AccountPrivate::shiftTokens(void)
{
if (m_nextToken.isEmpty()) {
qCDebug(logger)
<< "Not shifting to next token for account:" << m_id
<< "It is not yet available";
return;
}
QDateTime now = QDateTime::currentDateTime();
switch (m_algorithm) {
case Account::Algorithm::Hotp:
if (m_counter != m_nextCounter) {
qCDebug(logger)
<< "Not shifting to next token for account:" << m_id
<< "It is not valid anymore";
return;
}
break;
case Account::Algorithm::Totp:
if (now < m_nextTotpValidFrom || now > m_nextTotpValidUntil) {
qCDebug(logger)
<< "Not shifting to next token for account:" << m_id
<< "It is not valid at this time";
return;
}
break;
default:
Q_ASSERT_X(false, Q_FUNC_INFO, "unknown algorithm value");
return;
}
setToken(m_nextToken);
}
void AccountPrivate::recompute(void)
{
Q_Q(Account);
if (!m_is_still_alive) {
qCDebug(logger)
<< "Will not recompute token for account:" << m_id
<< "Object marked for death";
return;
}
if (!m_storage->isStillOpen()) {
qCDebug(logger)
<< "Will not recompute token for account:" << m_id
<< "Storage no longer open";
return;
}
shiftTokens();
qCDebug(logger) << "Requesting recomputed tokens for account:" << m_id;
ComputeHotp *hotpJob = nullptr;
ComputeTotp *totpJob = nullptr;
switch (m_algorithm) {
case Account::Algorithm::Hotp:
hotpJob = new ComputeHotp(m_storage->secret(), m_secret, m_tokenLength, m_counter, m_offset, m_checksum);
m_actions->queueAndProceed(hotpJob, [hotpJob, q, this](void) -> void {
new HandleTokenUpdate(this, hotpJob, q);
});
break;
case Account::Algorithm::Totp:
totpJob = new ComputeTotp(m_storage->secret(), m_secret, m_tokenLength, m_epoch, m_timeStep, m_hash);
m_actions->queueAndProceed(totpJob, [totpJob, q, this](void) -> void
{
new HandleTokenUpdate(this, totpJob, q);
});
break;
default:
Q_ASSERT_X(false, Q_FUNC_INFO, "unknown algorithm value");
break;
}
}
void AccountPrivate::remove(void)
{
if (!m_is_still_alive) {
qCDebug(logger)
<< "Will not remove account:" << m_id
<< "Object marked for death";
return;
}
if (!m_storage->isStillOpen()) {
qCDebug(logger)
<< "Will not remove account:" << m_id
<< "Storage no longer open";
return;
}
QSet<QString> self;
self.insert(AccountPrivate::toFullName(m_name, m_issuer));
m_storage->removeAccounts(self);
}
void AccountPrivate::markForRemoval(void)
{
m_is_still_alive = false;
}
bool AccountPrivate::isStillAlive(void) const
{
return m_is_still_alive;
}
AccountPrivate::AccountPrivate(const std::function<Account*(AccountPrivate*)> &account,
AccountStoragePrivate *storage, Dispatcher *dispatcher,
const QUuid id, const QString &name, const QString &issuer,
const secrets::EncryptedSecret &secret, uint tokenLength,
quint64 counter, const std::optional<uint> offset, bool addChecksum) :
q_ptr(account(this)), m_storage(storage), m_actions(dispatcher), m_is_still_alive(true),
m_algorithm(Account::Algorithm::Hotp), m_id(id), m_token(QString()), m_nextToken(QString()),
m_nextTotpValidFrom(QDateTime::fromMSecsSinceEpoch(0)), // not a totp token so does not really matter
m_nextTotpValidUntil(QDateTime::fromMSecsSinceEpoch(0)), // not a totp token so does not really matter
m_nextCounter(1ULL), m_name(name), m_issuer(issuer),
m_secret(secret), m_tokenLength(tokenLength),
m_counter(counter), m_offset(offset), m_checksum(addChecksum),
// not a totp token so these values don't really matter
m_epoch(QDateTime::fromMSecsSinceEpoch(0)), m_timeStep(30), m_hash(Account::Hash::Sha1)
{
}
AccountPrivate::AccountPrivate(const std::function<Account*(AccountPrivate*)> &account,
AccountStoragePrivate *storage, Dispatcher *dispatcher,
const QUuid id, const QString &name, const QString &issuer,
const secrets::EncryptedSecret &secret, uint tokenLength,
const QDateTime &epoch, uint timeStep, Account::Hash hash) :
q_ptr(account(this)), m_storage(storage), m_actions(dispatcher), m_is_still_alive(true),
m_algorithm(Account::Algorithm::Totp), m_id(id), m_token(QString()), m_nextToken(QString()),
m_nextTotpValidFrom(epoch), m_nextTotpValidUntil(epoch),
m_nextCounter(0ULL), // not a hotp token so does not really matter
m_name(name), m_issuer(issuer), m_secret(secret), m_tokenLength(tokenLength),
// not a hotp token so these values don't really matter
m_counter(0ULL), m_offset(std::nullopt), m_checksum(false),
m_epoch(epoch), m_timeStep(timeStep), m_hash(hash)
{
}
QVector<QString> AccountStoragePrivate::activeAccounts(void) const
{
QVector<QString> active;
if (!m_is_still_open) {
qCDebug(logger) << "Not returning accounts: account storage no longer open";
return active;
}
const QList<QString> all = m_names.keys();
for (const QString &account : all) {
const QUuid id = m_names[account];
if (m_accountsPrivate[id]->isStillAlive()) {
active.append(account);
} else {
qCDebug(logger)
<< "Not returning account:" << id
<< "Object marked for death";
}
}
return active;
}
bool AccountStoragePrivate::isStillOpen(void) const
{
return m_is_still_open;
}
bool AccountStoragePrivate::isAccountStillAvailable(const QString &account) const
{
if (!m_is_still_open) {
qCDebug(logger) << "Pretending no name is available: account storage no longer open";
return false;
}
return !m_names.contains(account);
}
QString AccountPrivate::toFullName(const QString &name, const QString &issuer)
{
return issuer.isEmpty() ? name : issuer + QLatin1Char(':') + name;
}
bool AccountStoragePrivate::contains(const QString &account) const
{
/*
* Pretend an account which is marked for removal is already fully purged
* This lets the behaviour of get() and contains() be mutually consistent
* without having to hand out pointers which may be about to go stale in get()
*/
if (!m_is_still_open) {
qCDebug(logger) << "Pretending no account exists: account storage no longer open";
return false;
}
if (!m_names.contains(account)) {
return false;
}
const QUuid id = m_names[account];
if (!m_accountsPrivate[id]->isStillAlive()) {
qCDebug(logger)
<< "Pretending account does not exist:" << id
<< "Object marked for death";
return false;
}
return true;
}
Account * AccountStoragePrivate::get(const QString &account) const
{
return contains(account) ? m_accounts[m_names[account]] : nullptr;
}
SettingsProvider AccountStoragePrivate::settings(void) const
{
return m_settings;
}
AccountSecret * AccountStoragePrivate::secret(void) const
{
return m_secret;
}
void AccountStoragePrivate::removeAccounts(const QSet<QString> &accountNames)
{
if (!m_is_still_open) {
qCDebug(logger) << "Not removing accounts: account storage no longer open";
return;
}
QSet<QUuid> ids;
for (const QString &accountName : accountNames) {
if (m_names.contains(accountName)) {
const QUuid id = m_names[accountName];
AccountPrivate *p = m_accountsPrivate[id];
/*
* Avoid doing anything with accounts which are already about to be removed from the maps
*/
if (p->isStillAlive()) {
p->markForRemoval();
ids.insert(id);
} else {
qCDebug(logger)
<< "Not removing account:" << id
<< "Object marked for death";
}
}
}
DeleteAccounts *job = new DeleteAccounts(m_settings, ids);
m_actions->queueAndProceed(job, [&ids, job, this](void) -> void
{
for (const QUuid &id : qAsConst(ids)) {
Account *account = m_accounts[id];
QObject::connect(job, &DeleteAccounts::finished, account, &Account::removed);
}
});
}
void AccountStoragePrivate::acceptAccountRemoval(const QString &accountName)
{
if (!m_names.contains(accountName)) {
qCDebug(logger) << "Not accepting account removal: account name unknown";
return;
}
const QUuid id = m_names[accountName];
qCDebug(logger) << "Handling account cleanup for account:" << id;
Account *account = m_accounts[id];
m_accounts.remove(id);
m_accountsPrivate.remove(id);
m_names.remove(accountName);
m_ids.remove(id);
QTimer::singleShot(0, account, &accounts::Account::deleteLater);
}
void AccountStoragePrivate::dispose(const std::function<void(Null*)> &handler)
{
if (!m_is_still_open) {
qCDebug(logger) << "Not disposing of storage: account storage no longer open";
return;
}
qCDebug(logger) << "Disposing of storage";
m_is_still_open = false;
Null *job = new Null();
m_secret->cancelRequests();
m_actions->queueAndProceed(job, [job, &handler](void) -> void
{
handler(job);
});
}
void AccountStoragePrivate::acceptDisposal(void)
{
Q_Q(AccountStorage);
if (m_is_still_open) {
qCDebug(logger) << "Ignoring disposal of storage: account storage is still open";
return;
}
qCDebug(logger) << "Handling storage disposal";
const auto &names = m_names.keys();
for (const QString &accountName : names) {
const QUuid id = m_names[accountName];
qCDebug(logger) << "Handling account cleanup for account:" << id;
Account *account = m_accounts[id];
m_accounts.remove(id);
m_accountsPrivate.remove(id);
m_names.remove(accountName);
m_ids.remove(id);
QTimer::singleShot(0, account, &accounts::Account::deleteLater);
}
QTimer::singleShot(0, m_secret, &accounts::AccountSecret::deleteLater);
Q_EMIT q->disposed();
}
QUuid AccountStoragePrivate::generateId(const QString &name) const
{
QUuid attempt = QUuid::createUuidV5(QUuid(), name);
while (attempt.isNull() || m_ids.contains(attempt)) {
attempt = QUuid::createUuid();
}
return attempt;
}
std::optional<secrets::EncryptedSecret> AccountStoragePrivate::encrypt(const QString &secret) const
{
if (!m_is_still_open) {
qCDebug(logger) << "Will not encrypt account secret: storage no longer open";
return std::nullopt;
}
if (!m_secret || !m_secret->key()) {
qCDebug(logger) << "Will not encrypt account secret: encryption key not available";
return std::nullopt;
}
QScopedPointer<secrets::SecureMemory> decoded(secrets::decodeBase32(secret));
if (!decoded) {
qCDebug(logger) << "Will not encrypt account secret: failed to decode base32";
return std::nullopt;
}
return m_secret->encrypt(decoded.data());
}
bool AccountStoragePrivate::validateGenericNewToken(const QString &name, const QString &issuer,
const QString &secret, uint tokenLength) const
{
return checkTokenLength(tokenLength) && checkName(name) && checkIssuer(issuer)
&& isAccountStillAvailable(AccountPrivate::toFullName(name, issuer)) && checkSecret(secret);
}
bool AccountStoragePrivate::addHotp(const std::function<void(SaveHotp*)> &handler,
const QString &name, const QString &issuer,
const QString &secret, uint tokenLength,
quint64 counter, const std::optional<uint> offset, bool checksum)
{
if (!m_is_still_open) {
qCDebug(logger) << "Will not add new HOTP account: storage no longer open";
return false;
}
if (!validateGenericNewToken(name, issuer, secret, tokenLength) ||
!checkOffset(offset, QCryptographicHash::Sha1)) {
qCDebug(logger) << "Will not add new HOTP account: invalid account details";
return false;
}
std::optional<secrets::EncryptedSecret> encryptedSecret = encrypt(secret);
if (!encryptedSecret) {
qCDebug(logger) << "Will not add new HOTP account: failed to encrypt secret";
return false;
}
QUuid id = generateId(AccountPrivate::toFullName(name, issuer));
qCDebug(logger) << "Requesting to store details for new HOTP account:" << id;
m_ids.insert(id);
SaveHotp *job = new SaveHotp(m_settings, id, name, issuer, *encryptedSecret, tokenLength,
counter, offset, checksum);
m_actions->queueAndProceed(job, [job, &handler](void) -> void
{
handler(job);
});
return true;
}
bool AccountStoragePrivate::addTotp(const std::function<void(SaveTotp*)> &handler,
const QString &name, const QString &issuer,
const QString &secret, uint tokenLength,
uint timeStep, const QDateTime &epoch, Account::Hash hash)
{
if (!m_is_still_open) {
qCDebug(logger) << "Will not add new TOTP account: storage no longer open";
return false;
}
if (!validateGenericNewToken(name, issuer, secret, tokenLength) ||
!checkTimeStep(timeStep) || !checkEpoch(epoch)) {
qCDebug(logger) << "Will not add new TOTP account: invalid account details";
return false;
}
std::optional<secrets::EncryptedSecret> encryptedSecret = encrypt(secret);
if (!encryptedSecret) {
qCDebug(logger) << "Will not add new TOTP account: failed to encrypt secret";
return false;
}
QUuid id = generateId(AccountPrivate::toFullName(name, issuer));
qCDebug(logger) << "Requesting to store details for new TOTP account:" << id;
m_ids.insert(id);
SaveTotp *job = new SaveTotp(m_settings, id, name, issuer, *encryptedSecret, tokenLength,
timeStep, epoch, hash);
m_actions->queueAndProceed(job, [job, &handler](void) -> void
{
handler(job);
});
return true;
}
void AccountStoragePrivate::unlock(const std::function<void(RequestAccountPassword*)> &handler)
{
if (!m_is_still_open) {
qCDebug(logger) << "Will not attempt to unlock accounts: storage no longer open";
return;
}
qCDebug(logger) << "Requesting to unlock account storage";
RequestAccountPassword *job = new RequestAccountPassword(m_settings, m_secret);
m_actions->queueAndProceed(job, [job, &handler](void) -> void
{
handler(job);
});
}
void AccountStoragePrivate::load(const std::function<void(LoadAccounts*)> &handler)
{
if (!m_is_still_open) {
qCDebug(logger) << "Will not load accounts: storage no longer open";
return;
}
LoadAccounts *job = new LoadAccounts(m_settings, m_secret);
m_actions->queueAndProceed(job, [job, &handler](void) -> void
{
handler(job);
});
}
Account * AccountStoragePrivate::acceptHotpAccount(const QUuid id, const QString &name, const QString &issuer,
const secrets::EncryptedSecret &secret, uint tokenLength,
quint64 counter, const std::optional<uint> offset, bool checksum)
{
Q_Q(AccountStorage);
qCDebug(logger) << "Registering HOTP account:" << id;
const std::function<Account*(AccountPrivate*)> registration([this, q, &id](AccountPrivate *p) -> Account *
{
Account *account = new Account(p, q);
m_accounts.insert(id, account);
return account;
});
m_ids.insert(id);
m_names.insert(AccountPrivate::toFullName(name, issuer), id);
const auto p = new AccountPrivate(registration, this, m_actions,
id, name, issuer, secret, tokenLength, counter, offset, checksum);
m_accountsPrivate.insert(id, p);
Q_ASSERT_X(m_accounts.contains(id), Q_FUNC_INFO, "account should have been registered");
return m_accounts[id];
}
Account * AccountStoragePrivate::acceptTotpAccount(const QUuid id, const QString &name, const QString &issuer,
const secrets::EncryptedSecret &secret, uint tokenLength,
uint timeStep, const QDateTime &epoch, Account::Hash hash)
{
Q_Q(AccountStorage);
qCDebug(logger) << "Registering TOTP account:" << id;
const std::function<Account*(AccountPrivate*)> registration([this, q, &id](AccountPrivate *p) -> Account *
{
Account *account = new Account(p, q);
m_accounts.insert(id, account);
return account;
});
m_ids.insert(id);
m_names.insert(AccountPrivate::toFullName(name, issuer), id);
const auto p = new AccountPrivate(registration, this, m_actions,
id, name, issuer, secret, tokenLength, epoch, timeStep, hash);
m_accountsPrivate.insert(id, p);
Q_ASSERT_X(m_accounts.contains(id), Q_FUNC_INFO, "account should have been registered");
return m_accounts[id];
}
bool AccountStoragePrivate::isLoaded(void) const
{
return m_is_loaded;
}
void AccountStoragePrivate::notifyLoaded(void)
{
Q_Q(AccountStorage);
m_is_loaded = true;
Q_EMIT q->loaded();
}
void AccountStoragePrivate::notifyError(void)
{
Q_Q(AccountStorage);
m_has_error = true;
Q_EMIT q->error();
}
void AccountStoragePrivate::clearError(void)
{
m_has_error = false;
}
bool AccountStoragePrivate::hasError(void) const
{
return m_has_error;
}
AccountStoragePrivate::AccountStoragePrivate(const SettingsProvider &settings,
AccountSecret *secret, AccountStorage *storage, Dispatcher *dispatcher) :
q_ptr(storage), m_is_loaded(false), m_has_error(false), m_is_still_open(true),
m_actions(dispatcher), m_settings(settings), m_secret(secret)
{
}
HandleCounterUpdate::HandleCounterUpdate(AccountPrivate *account, AccountStoragePrivate *storage,
quint64 counter, SaveHotp *job, QObject *parent) :
QObject(parent), m_accept_on_finish(true), m_counter(counter), m_account(account), m_storage(storage)
{
QObject::connect(job, &SaveHotp::invalid, this, &HandleCounterUpdate::rejected);
QObject::connect(job, &SaveHotp::finished, this, &HandleCounterUpdate::finished);
}
void HandleCounterUpdate::rejected(void)
{
m_accept_on_finish = false;
m_storage->notifyError();
}
void HandleCounterUpdate::finished(void)
{
if (m_accept_on_finish) {
m_account->acceptCounter(m_counter);
} else {
qCWarning(logger)
<< "Rejecting counter update for account:" << m_account->id()
<< "Failed to save the updated account details to storage";
}
deleteLater();
}
HandleTokenUpdate::HandleTokenUpdate(AccountPrivate *account, ComputeHotp *job, QObject *parent) :
QObject(parent), m_account(account)
{
QObject::connect(job, &ComputeHotp::otp, this, &HandleTokenUpdate::hotp);
QObject::connect(job, &ComputeHotp::finished, this, &HandleTokenUpdate::deleteLater);
}
HandleTokenUpdate::HandleTokenUpdate(AccountPrivate *account, ComputeTotp *job, QObject *parent) :
QObject(parent), m_account(account)
{
QObject::connect(job, &ComputeTotp::otp, this, &HandleTokenUpdate::totp);
QObject::connect(job, &ComputeTotp::finished, this, &HandleTokenUpdate::deleteLater);
}
void HandleTokenUpdate::totp(const QString &otp, const QString &nextOtp, const QDateTime &validFrom,
const QDateTime &validUntil)
{
m_account->acceptTotpTokens(otp, nextOtp, validFrom, validUntil);
}
void HandleTokenUpdate::hotp(const QString &otp, const QString &nextOtp, quint64 validUntil)
{
m_account->acceptHotpTokens(otp, nextOtp, validUntil);
}
}