keysmith/src/account/actions_p.cpp

809 lines
32 KiB
C++

/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2020-2021 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
*/
#include "actions_p.h"
#include "validation.h"
#include "../base32/base32.h"
#include "../logging_p.h"
#include "../oath/oath.h"
#include <QMetaEnum>
#include <QScopedPointer>
#include <QTimer>
#include <limits>
KEYSMITH_LOGGER(logger, ".accounts.actions")
KEYSMITH_LOGGER(dispatcherLogger, ".accounts.dispatcher")
static const quint64 maxCounter = std::numeric_limits<quint64>::max();
static const int hashTypeId = qRegisterMetaType<accounts::Account::Hash>();
namespace accounts
{
AccountJob::AccountJob() :
QObject()
{
}
AccountJob::~AccountJob()
{
}
Null::Null() :
AccountJob()
{
}
void Null::run(void)
{
Q_EMIT finished();
}
void AccountJob::run(void)
{
Q_ASSERT_X(false, Q_FUNC_INFO, "should be overridden in derived classes!");
}
RequestAccountPassword::RequestAccountPassword(const SettingsProvider &settings, AccountSecret *secret) :
AccountJob(), m_settings(settings), m_secret(secret), m_failed(false), m_succeeded(false)
{
}
LoadAccounts::LoadAccounts(const SettingsProvider &settings, const AccountSecret *secret,
const std::function<qint64(void)> &clock) :
AccountJob(), m_settings(settings), m_secret(secret), m_clock(clock)
{
}
DeleteAccounts::DeleteAccounts(const SettingsProvider &settings, const QSet<QUuid> &ids) :
AccountJob(), m_settings(settings), m_ids(ids)
{
}
SaveHotp::SaveHotp(const SettingsProvider &settings,
const QUuid id, const QString &accountName, const QString &issuer,
const secrets::EncryptedSecret &secret, uint tokenLength,
quint64 counter, const std::optional<uint> offset, bool checksum) :
AccountJob(), m_settings(settings), m_id(id), m_accountName(accountName), m_issuer(issuer),
m_secret(secret), m_tokenLength(tokenLength), m_counter(counter), m_offset(offset), m_checksum(checksum)
{
}
SaveTotp::SaveTotp(const SettingsProvider &settings,
const QUuid id, const QString &accountName, const QString &issuer,
const secrets::EncryptedSecret &secret, uint tokenLength,
uint timeStep, const QDateTime &epoch, Account::Hash hash,
const std::function<qint64(void)> &clock) :
AccountJob(), m_settings(settings), m_id(id), m_accountName(accountName), m_issuer(issuer),
m_secret(secret), m_tokenLength(tokenLength), m_timeStep(timeStep), m_epoch(epoch), m_hash(hash), m_clock(clock)
{
}
void SaveHotp::run(void)
{
if (!checkId(m_id) || !checkName(m_accountName) || !checkIssuer(m_issuer) ||
!checkTokenLength(m_tokenLength) || !checkOffset(m_offset, QCryptographicHash::Sha1)) {
qCDebug(logger)
<< "Unable to save HOTP account:" << m_id
<< "Invalid account details";
Q_EMIT invalid();
Q_EMIT finished();
return;
}
const PersistenceAction act([this](QSettings &settings) -> void
{
if (!settings.isWritable()) {
qCWarning(logger)
<< "Unable to save HOTP account:" << m_id
<< "Storage not writable";
Q_EMIT invalid();
return;
}
qCInfo(logger) << "Saving HOTP account:" << m_id;
const QString group = m_id.toString();
settings.remove(group);
settings.beginGroup(group);
settings.setValue(QStringLiteral("account"), m_accountName);
if (!m_issuer.isNull()) {
settings.setValue(QStringLiteral("issuer"), m_issuer);
}
settings.setValue(QStringLiteral("type"), QStringLiteral("hotp"));
QString encodedNonce = QString::fromUtf8(m_secret.nonce().toBase64(QByteArray::Base64Encoding));
QString encodedSecret = QString::fromUtf8(m_secret.cryptText().toBase64(QByteArray::Base64Encoding));
settings.setValue(QStringLiteral("secret"), encodedSecret);
settings.setValue(QStringLiteral("nonce"), encodedNonce);
settings.setValue(QStringLiteral("counter"), m_counter);
settings.setValue(QStringLiteral("pinLength"), m_tokenLength);
if (m_offset) {
settings.setValue(QStringLiteral("offset"), *m_offset);
}
settings.setValue(QStringLiteral("checksum"), m_checksum);
settings.endGroup();
// Try to guarantee that data will have been written before claiming the account was actually saved
settings.sync();
Q_EMIT saved(m_id, m_accountName, m_issuer, m_secret.cryptText(), m_secret.nonce(), m_tokenLength,
m_counter, m_offset.has_value(), m_offset ? *m_offset : 0U, m_checksum);
});
m_settings(act);
Q_EMIT finished();
}
void SaveTotp::run(void)
{
if (!checkId(m_id) || !checkName(m_accountName) || !checkIssuer(m_issuer) ||
!checkTokenLength(m_tokenLength) || !checkTimeStep(m_timeStep) || !checkEpoch(m_epoch, m_clock)) {
qCDebug(logger)
<< "Unable to save TOTP account:" << m_id
<< "Invalid account details";
Q_EMIT invalid();
Q_EMIT finished();
return;
}
const PersistenceAction act([this](QSettings &settings) -> void
{
if (!settings.isWritable()) {
qCWarning(logger)
<< "Unable to save TOTP account:" << m_id
<< "Storage not writable";
Q_EMIT invalid();
return;
}
qCInfo(logger) << "Saving TOTP account:" << m_id;
const QString group = m_id.toString();
settings.remove(group);
settings.beginGroup(group);
settings.setValue(QStringLiteral("account"), m_accountName);
if (!m_issuer.isNull()) {
settings.setValue(QStringLiteral("issuer"), m_issuer);
}
settings.setValue(QStringLiteral("type"), QStringLiteral("totp"));
QString encodedNonce = QString::fromUtf8(m_secret.nonce().toBase64(QByteArray::Base64Encoding));
QString encodedSecret = QString::fromUtf8(m_secret.cryptText().toBase64(QByteArray::Base64Encoding));
settings.setValue(QStringLiteral("secret"), encodedSecret);
settings.setValue(QStringLiteral("nonce"), encodedNonce);
settings.setValue(QStringLiteral("timeStep"), m_timeStep);
settings.setValue(QStringLiteral("pinLength"), m_tokenLength);
settings.setValue(QStringLiteral("epoch"), m_epoch.toUTC().toString(Qt::ISODateWithMs));
settings.setValue(QStringLiteral("hash"), QVariant::fromValue<Account::Hash>(m_hash).toString());
settings.endGroup();
// Try to guarantee that data will have been written before claiming the account was actually saved
settings.sync();
Q_EMIT saved(m_id, m_accountName, m_issuer, m_secret.cryptText(), m_secret.nonce(), m_tokenLength,
m_timeStep, m_epoch, m_hash);
});
m_settings(act);
Q_EMIT finished();
}
void DeleteAccounts::run(void)
{
const PersistenceAction act([this](QSettings &settings) -> void
{
if (!settings.isWritable()) {
qCWarning(logger) << "Unable to delete accounts: storage not writable";
Q_EMIT invalid();
return;
}
qCInfo(logger) << "Deleting accounts";
for (const QUuid &id : m_ids) {
settings.remove(id.toString());
}
});
m_settings(act);
Q_EMIT finished();
}
void RequestAccountPassword::fail(void)
{
if (m_failed || m_succeeded) {
qCDebug(logger) << "Suppressing 'failure' in unlocking accounts: already handled";
return;
}
m_failed = true;
QObject::disconnect(m_secret, &AccountSecret::requestsCancelled, this, &RequestAccountPassword::fail);
QObject::disconnect(m_secret, &AccountSecret::passwordAvailable, this, &RequestAccountPassword::unlock);
QObject::disconnect(m_secret, &AccountSecret::keyAvailable, this, &RequestAccountPassword::finish);
Q_EMIT failed();
Q_EMIT finished();
}
void RequestAccountPassword::unlock(void)
{
secrets::SecureMasterKey * derived = m_secret->deriveKey();
std::optional<secrets::EncryptedSecret> challenge = m_secret->challenge();
if (derived && challenge) {
qCInfo(logger) << "Successfully derived key for storage";
return;
} else {
qCInfo(logger) << "Failed to unlock storage:"
<< "Unable to derive secret encryption/decryption key or generate its matching challenge";
}
}
void RequestAccountPassword::finish(void)
{
if (m_succeeded || m_failed) {
qCDebug(logger) << "Suppressing 'success' in unlocking accounts: already handled";
return;
}
QObject::disconnect(m_secret, &AccountSecret::requestsCancelled, this, &RequestAccountPassword::fail);
QObject::disconnect(m_secret, &AccountSecret::passwordAvailable, this, &RequestAccountPassword::unlock);
QObject::disconnect(m_secret, &AccountSecret::keyAvailable, this, &RequestAccountPassword::finish);
std::optional<secrets::EncryptedSecret> challenge = m_secret->challenge();
secrets::SecureMasterKey * derived = m_secret->key();
if (!derived) {
qCInfo(logger) << "Failed to finish unlocking storage: no secret encryption/decryption key";
m_failed = true;
Q_EMIT failed();
Q_EMIT finished();
return;
}
// sanity check: challenge should be available once key derivation has completed successfully
if (!challenge) {
qCInfo(logger) << "Failed to finish unlocking storage: no challenge for encryption/decryption key";
m_failed = true;
Q_EMIT failed();
Q_EMIT finished();
return;
}
bool ok = false;
m_settings([derived, &challenge, &ok](QSettings &settings) -> void
{
if (!settings.isWritable()) {
qCWarning(logger) << "Unable to save account secret key parameters: storage not writable";
return;
}
const secrets::KeyDerivationParameters params = derived->params();
QString encodedSalt = QString::fromUtf8(derived->salt().toBase64(QByteArray::Base64Encoding));
QString encodedChallenge = QString::fromUtf8(challenge->cryptText().toBase64(QByteArray::Base64Encoding));
QString encodedNonce = QString::fromUtf8(challenge->nonce().toBase64(QByteArray::Base64Encoding));
settings.beginGroup(QStringLiteral("master-key"));
settings.setValue(QStringLiteral("salt"), encodedSalt);
settings.setValue(QStringLiteral("cpu"), params.cpuCost());
settings.setValue(QStringLiteral("memory"), (quint64) params.memoryCost());
settings.setValue(QStringLiteral("algorithm"), params.algorithm());
settings.setValue(QStringLiteral("length"), params.keyLength());
settings.setValue(QStringLiteral("nonce"), encodedNonce);
settings.setValue(QStringLiteral("challenge"), encodedChallenge);
settings.endGroup();
ok = true;
});
if (ok) {
qCInfo(logger) << "Successfully unlocked storage";
m_succeeded = true;
Q_EMIT unlocked();
} else {
qCInfo(logger) << "Failed to finish unlocking storage: unable to store parameters";
m_failed = true;
Q_EMIT failed();
}
Q_EMIT finished();
}
void RequestAccountPassword::run(void)
{
if (!m_secret) {
qCDebug(logger) << "Unable to request accounts password: no account secret object";
m_failed = true;
Q_EMIT failed();
Q_EMIT finished();
return;
}
QObject::connect(m_secret, &AccountSecret::passwordAvailable, this, &RequestAccountPassword::unlock);
QObject::connect(m_secret, &AccountSecret::requestsCancelled, this, &RequestAccountPassword::fail);
QObject::connect(m_secret, &AccountSecret::keyAvailable, this, &RequestAccountPassword::finish);
if (!m_secret->isStillAlive()) {
qCDebug(logger) << "Unable to request accounts password: account secret marked for death";
fail();
return;
}
bool ok = false;
m_settings([this, &ok](QSettings &settings) -> void
{
if (!settings.isWritable()) {
qCWarning(logger) << "Unable to request password for accounts: storage not writable";
return;
}
QStringList groups = settings.childGroups();
if (!groups.contains(QStringLiteral("master-key"))) {
qCInfo(logger) << "No key derivation parameters found: requesting 'new' password for accounts";
ok = m_secret->requestNewPassword();
return;
}
settings.beginGroup(QStringLiteral("master-key"));
QByteArray salt;
QByteArray nonce;
QByteArray challenge;
quint64 cpuCost = 0ULL;
quint64 keyLength = 0ULL;
size_t memoryCost = 0ULL;
// HACK: disables challenge verification, remove at some point!
bool challengeAvailable = settings.contains(QStringLiteral("challenge"));
int algorithm = settings.value(QStringLiteral("algorithm")).toInt(&ok);
if (ok) {
ok = false;
keyLength = settings.value(QStringLiteral("length")).toULongLong(&ok);
}
if (ok) {
ok = false;
cpuCost = settings.value(QStringLiteral("cpu")).toULongLong(&ok);
}
if (ok) {
ok = false;
memoryCost = settings.value(QStringLiteral("memory")).toULongLong(&ok);
}
if (ok) {
QByteArray encodedSalt = settings.value(QStringLiteral("salt")).toString().toUtf8();
salt = QByteArray::fromBase64(encodedSalt, QByteArray::Base64Encoding);
ok = !salt.isEmpty() && secrets::SecureMasterKey::validate(salt);
}
// HACK: disables challenge verification, remove at some point!
if (challengeAvailable && ok) {
QByteArray encodedChallenge = settings.value(QStringLiteral("challenge")).toString().toUtf8();
challenge = QByteArray::fromBase64(encodedChallenge, QByteArray::Base64Encoding);
ok = !challenge.isEmpty();
}
// HACK: disables challenge verification, remove at some point!
if (challengeAvailable && ok) {
QByteArray encodedNonce = settings.value(QStringLiteral("nonce")).toString().toUtf8();
nonce = QByteArray::fromBase64(encodedNonce, QByteArray::Base64Encoding);
ok = !nonce.isEmpty();
}
settings.endGroup();
const auto params = secrets::KeyDerivationParameters::create(keyLength, algorithm, memoryCost, cpuCost);
const auto encryptedChallenge = secrets::EncryptedSecret::from(challenge, nonce);
// HACK: disables challenge verification, remove at some point!
if (!ok || !params || !secrets::SecureMasterKey::validate(*params) || (challengeAvailable && !encryptedChallenge)) {
qCDebug(logger) << "Unable to request 'existing' password: invalid challenge, nonce, salt or key derivation parameters";
return;
}
qCInfo(logger) << "Requesting 'existing' password for accounts";
ok = challengeAvailable
? m_secret->requestExistingPassword(*encryptedChallenge, salt, *params)
: m_secret->requestExistingPassword(salt, *params); // HACK: disables challenge verification, remove at some point!
});
if (!ok) {
qCInfo(logger) << "Unable to unlock storage: failed to request password for accounts";
fail();
}
}
void LoadAccounts::run(void)
{
if (!m_secret || !m_secret->key()) {
qCDebug(logger) << "Unable to load accounts: secret decryption key not available";
Q_EMIT finished();
return;
}
bool failed = false;
const PersistenceAction act([this, &failed](QSettings &settings) -> void
{
qCInfo(logger, "Loading accounts from storage");
const QStringList entries = settings.childGroups();
for (const QString &group : entries) {
if (group == QLatin1String("master-key")) {
continue;
}
const QUuid id(group);
if (id.isNull()) {
qCDebug(logger)
<< "Ignoring:" << group
<< "Not an account section";
failed = true;
continue;
}
settings.beginGroup(group);
const QString accountName = settings.value(QStringLiteral("account")).toString();
if (!checkName(accountName)) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid account name";
settings.endGroup();
continue;
}
const QString issuer = settings.value(QStringLiteral("issuer"), QString()).toString();
if (!checkIssuer(issuer)) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid account issuer";
settings.endGroup();
continue;
}
const QString type = settings.value(QStringLiteral("type")).toString();
if (type != QStringLiteral("hotp") && type != QStringLiteral("totp")) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid account type";
settings.endGroup();
failed = true;
continue;
}
bool ok = false;
const int tokenLength = settings.value(QStringLiteral("pinLength")).toInt(&ok);
if (!ok || !checkTokenLength(tokenLength)) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid token length";
settings.endGroup();
failed = true;
continue;
}
const QByteArray encodedNonce = settings.value(QStringLiteral("nonce")).toString().toUtf8();
const QByteArray encodedSecret = settings.value(QStringLiteral("secret")).toString().toUtf8();
const QByteArray nonce = QByteArray::fromBase64(encodedNonce, QByteArray::Base64Encoding);
const QByteArray secret = QByteArray::fromBase64(encodedSecret, QByteArray::Base64Encoding);
const auto encryptedSecret = secrets::EncryptedSecret::from(secret, nonce);
if (!encryptedSecret) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid token secret";
settings.endGroup();
failed = true;
continue;
}
QScopedPointer<secrets::SecureMemory> decrypted(m_secret->decrypt(*encryptedSecret));
if (!decrypted) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Unable to decrypt token secret";
settings.endGroup();
failed = true;
continue;
}
if (type == QStringLiteral("totp")) {
ok = false;
const uint timeStep = settings.value(QStringLiteral("timeStep")).toUInt(&ok);
if (!ok || !checkTimeStep(timeStep)) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid time step";
settings.endGroup();
failed = true;
continue;
}
const QDateTime epoch = settings.value(QStringLiteral("epoch"), QDateTime::fromMSecsSinceEpoch(0))
.toDateTime();
if (!checkEpoch(epoch, m_clock)) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid epoch";
settings.endGroup();
failed = true;
continue;
}
ok = false;
const auto hashEnum = QMetaEnum::fromType<accounts::Account::Hash>();
const auto hashDefault = QVariant::fromValue<accounts::Account::Hash>(accounts::Account::Sha1);
const QByteArray hashName = settings.value(QStringLiteral("hash"), hashDefault).toByteArray();
int hash = hashEnum.keyToValue(hashName.constData(), &ok);
if (!ok) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid hash";
settings.endGroup();
failed = true;
continue;
}
qCInfo(logger) << "Found valid TOTP account:" << id;
Q_EMIT foundTotp(id, accountName, issuer, secret, nonce, tokenLength,
timeStep, epoch, (Account::Hash) hash);
}
if (type == QStringLiteral("hotp")) {
ok = false;
const quint64 counter = settings.value(QStringLiteral("counter")).toULongLong(&ok);
if (!ok) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid counter";
settings.endGroup();
failed = true;
continue;
}
const QVariant offsetVariant = settings.value(QStringLiteral("offset"));
ok = offsetVariant.isNull();
std::optional<uint> offset = ok ? std::nullopt : std::optional<uint>(offsetVariant.toUInt(&ok));
if (!ok || !checkOffset(offset, QCryptographicHash::Sha1)) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid offset";
settings.endGroup();
failed = true;
continue;
}
const auto checkSumOff = QStringLiteral("false");
const auto checksum = settings.value(QStringLiteral("checksum"), checkSumOff).toString();
if (checksum != QStringLiteral("true") && checksum != checkSumOff) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid checksum";
settings.endGroup();
failed = true;
continue;
}
qCInfo(logger) << "Found valid HOTP account:" << id;
Q_EMIT foundHotp(id, accountName, issuer, secret, nonce, tokenLength,
counter, offset.has_value(), offset ? *offset : 0U,
checksum == QStringLiteral("true"));
}
settings.endGroup();
}
});
m_settings(act);
if (failed) {
Q_EMIT failedToLoadAllAccounts();
}
Q_EMIT finished();
}
static std::optional<QString> computeToken(const AccountSecret *accountSecret,
const secrets::EncryptedSecret &tokenSecret,
const oath::Algorithm &algorithm,
quint64 counter)
{
QScopedPointer<secrets::SecureMemory> secret(accountSecret->decrypt(tokenSecret));
if (!secret) {
qCDebug(logger) << "Unable to compute token: failed to decrypt account secret";
return std::nullopt;
}
return algorithm.compute(counter, reinterpret_cast<char*>(secret->data()), secret->size());
}
ComputeTotp::ComputeTotp(const AccountSecret *secret,
const secrets::EncryptedSecret &tokenSecret, uint tokenLength,
const QDateTime &epoch, uint timeStep, const Account::Hash hash,
const std::function<qint64(void)> &clock) :
AccountJob(), m_secret(secret), m_tokenSecret(tokenSecret), m_tokenLength(tokenLength),
m_epoch(epoch), m_timeStep(timeStep), m_hash(hash), m_clock(clock)
{
}
void ComputeTotp::run(void)
{
if (!m_secret || !m_secret->key()) {
qCDebug(logger) << "Unable to compute TOTP token: secret decryption key not available";
Q_EMIT finished();
return;
}
if (!checkTokenLength(m_tokenLength)) {
qCDebug(logger) << "Unable to compute TOTP token: invalid token length:" << m_tokenLength;
Q_EMIT finished();
return;
}
if (!checkTimeStep(m_timeStep)) {
qCDebug(logger) << "Unable to compute TOTP token: invalid time step:" << m_timeStep;
Q_EMIT finished();
return;
}
if (!checkEpoch(m_epoch, m_clock)) {
qCDebug(logger) << "Unable to compute TOTP token: invalid epoch:" << m_epoch;
Q_EMIT finished();
return;
}
QCryptographicHash::Algorithm hash;
switch(m_hash)
{
case Account::Hash::Sha1:
hash = QCryptographicHash::Sha1;
break;
case Account::Hash::Sha256:
hash = QCryptographicHash::Sha256;
break;
case Account::Hash::Sha512:
hash = QCryptographicHash::Sha512;
break;
default:
qCDebug(logger) << "Unable to compute TOTP token: unknown hashing algorithm:" << m_hash;
Q_EMIT finished();
return;
}
const std::optional<oath::Algorithm> algorithm = oath::Algorithm::totp(hash, m_tokenLength);
if (!algorithm) {
qCDebug(logger) << "Unable to compute TOTP token: failed to construct algorithm";
Q_EMIT finished();
return;
}
const std::optional<quint64> counter = oath::count(m_epoch, m_timeStep, m_clock);
if (!counter) {
qCDebug(logger) << "Unable to compute TOTP token: failed to count time steps";
Q_EMIT finished();
return;
}
const auto counterValue = *counter;
const auto validFrom = counterValue < maxCounter
? oath::fromCounter(counterValue + 1ULL, m_epoch, m_timeStep)
: std::nullopt;
const auto validUntil = counterValue < (maxCounter - 1ULL)
? oath::fromCounter(counterValue + 2ULL, m_epoch, m_timeStep)
: std::nullopt;
if (!validFrom || !validUntil) {
qCDebug(logger) << "Unable to compute TOTP token: failed to determine expiry datetime of tokens";
Q_EMIT finished();
return;
}
const auto token = computeToken(m_secret, m_tokenSecret, *algorithm, counterValue);
const auto nextToken = token
? computeToken(m_secret, m_tokenSecret, *algorithm, counterValue + 1ULL)
: std::nullopt;
if (token && nextToken) {
Q_EMIT otp(*token, *nextToken, *validFrom, *validUntil);
} else {
qCDebug(logger) << "Failed to compute TOTP tokens";
}
Q_EMIT finished();
}
ComputeHotp::ComputeHotp(const AccountSecret *secret,
const secrets::EncryptedSecret &tokenSecret, uint tokenLength,
quint64 counter, const std::optional<uint> offset, bool checksum) :
AccountJob(), m_secret(secret), m_tokenSecret(tokenSecret), m_tokenLength(tokenLength),
m_counter(counter), m_offset(offset), m_checksum(checksum)
{
}
void ComputeHotp::run(void)
{
if (!m_secret || !m_secret->key()) {
qCDebug(logger) << "Unable to compute HOTP token: secret decryption key not available";
Q_EMIT finished();
return;
}
if (!checkTokenLength(m_tokenLength)) {
qCDebug(logger) << "Unable to compute HOTP token: invalid token length:" << m_tokenLength;
Q_EMIT finished();
return;
}
if (!checkOffset(m_offset, QCryptographicHash::Sha1)) {
qCDebug(logger) << "Unable to compute HOTP token: invalid offset:" << *m_offset;
Q_EMIT finished();
return;
}
const std::optional<oath::Algorithm> algorithm = oath::Algorithm::hotp(m_offset, m_tokenLength, m_checksum);
if (!algorithm) {
qCDebug(logger) << "Unable to compute HOTP token: failed to construct algorithm";
Q_EMIT finished();
return;
}
if (m_counter == maxCounter) {
qCDebug(logger) << "Unable to compute HOTP token: counter reached its limit";
Q_EMIT finished();
return;
}
const auto token = computeToken(m_secret, m_tokenSecret, *algorithm, m_counter);
const auto nextToken = token
? computeToken(m_secret, m_tokenSecret, *algorithm, m_counter + 1ULL)
: std::nullopt;
if (token && nextToken) {
Q_EMIT otp(*token, *nextToken, m_counter + 1ULL);
} else {
qCDebug(logger) << "Failed to compute HOTP tokens";
}
Q_EMIT finished();
}
Dispatcher::Dispatcher(QThread *thread, QObject *parent) :
QObject(parent), m_thread(thread), m_current(nullptr)
{
}
bool Dispatcher::empty(void) const
{
return m_pending.isEmpty();
}
void Dispatcher::queueAndProceed(AccountJob *job, const std::function<void(void)> &setup_callbacks)
{
if (job) {
qCDebug(dispatcherLogger) << "Queuing job for dispatcher";
job->moveToThread(m_thread);
setup_callbacks();
m_pending.append(job);
dispatchNext();
}
}
void Dispatcher::dispatchNext(void)
{
qCDebug(dispatcherLogger) << "Handling request to dispatch next job";
if (!empty() && !m_current) {
qCDebug(dispatcherLogger) << "Dispatching next job";
m_current = m_pending.takeFirst();
QObject::connect(m_current, &AccountJob::finished, this, &Dispatcher::next);
QObject::connect(this, &Dispatcher::dispatch, m_current, &AccountJob::run);
Q_EMIT dispatch();
}
}
void Dispatcher::next(void)
{
qCDebug(dispatcherLogger) << "Handling next continuation in dispatcher";
QObject *from = sender();
AccountJob *job = from ? qobject_cast<AccountJob*>(from) : nullptr;
if (job) {
Q_ASSERT_X(job == m_current, Q_FUNC_INFO, "sender() should match 'current' job!");
QObject::disconnect(this, &Dispatcher::dispatch, job, &AccountJob::run);
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks): False positives with QTimer::singleShot
QTimer::singleShot(0, job, &AccountJob::deleteLater);
m_current = nullptr;
dispatchNext();
}
}
}