File indexing completed on 2024-04-28 05:50:06

0001 /*
0002  * SPDX-License-Identifier: GPL-3.0-or-later
0003  * SPDX-FileCopyrightText: 2020-2021 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
0004  */
0005 #include "actions_p.h"
0006 #include "validation.h"
0007 
0008 #include "../base32/base32.h"
0009 #include "../logging_p.h"
0010 #include "../oath/oath.h"
0011 
0012 #include <QMetaEnum>
0013 #include <QScopedPointer>
0014 #include <QTimer>
0015 
0016 #include <limits>
0017 
0018 KEYSMITH_LOGGER(logger, ".accounts.actions")
0019 KEYSMITH_LOGGER(dispatcherLogger, ".accounts.dispatcher")
0020 
0021 static const quint64 maxCounter = std::numeric_limits<quint64>::max();
0022 static const int hashTypeId = qRegisterMetaType<accounts::Account::Hash>();
0023 
0024 namespace accounts
0025 {
0026     AccountJob::AccountJob() :
0027         QObject()
0028     {
0029     }
0030 
0031     AccountJob::~AccountJob()
0032     {
0033     }
0034 
0035     Null::Null() :
0036         AccountJob()
0037     {
0038     }
0039 
0040     void Null::run(void)
0041     {
0042         Q_EMIT finished();
0043     }
0044 
0045     void AccountJob::run(void)
0046     {
0047         Q_ASSERT_X(false, Q_FUNC_INFO, "should be overridden in derived classes!");
0048     }
0049 
0050     RequestAccountPassword::RequestAccountPassword(const SettingsProvider &settings, AccountSecret *secret) :
0051         AccountJob(), m_settings(settings), m_secret(secret), m_failed(false), m_succeeded(false)
0052     {
0053     }
0054 
0055     LoadAccounts::LoadAccounts(const SettingsProvider &settings, const AccountSecret *secret,
0056                                const std::function<qint64(void)> &clock) :
0057         AccountJob(), m_settings(settings), m_secret(secret), m_clock(clock)
0058     {
0059     }
0060 
0061     DeleteAccounts::DeleteAccounts(const SettingsProvider &settings, const QSet<QUuid> &ids) :
0062         AccountJob(), m_settings(settings), m_ids(ids)
0063     {
0064     }
0065 
0066     SaveHotp::SaveHotp(const SettingsProvider &settings,
0067                        const QUuid id, const QString &accountName, const QString &issuer,
0068                        const secrets::EncryptedSecret &secret, uint tokenLength,
0069                        quint64 counter, const std::optional<uint> offset, bool checksum) :
0070         AccountJob(), m_settings(settings), m_id(id), m_accountName(accountName), m_issuer(issuer),
0071         m_secret(secret), m_tokenLength(tokenLength), m_counter(counter), m_offset(offset), m_checksum(checksum)
0072     {
0073     }
0074 
0075     SaveTotp::SaveTotp(const SettingsProvider &settings,
0076                        const QUuid id, const QString &accountName, const QString &issuer,
0077                        const secrets::EncryptedSecret &secret, uint tokenLength,
0078                        uint timeStep, const QDateTime &epoch, Account::Hash hash,
0079                        const std::function<qint64(void)> &clock) :
0080         AccountJob(), m_settings(settings), m_id(id), m_accountName(accountName), m_issuer(issuer),
0081         m_secret(secret), m_tokenLength(tokenLength), m_timeStep(timeStep), m_epoch(epoch), m_hash(hash), m_clock(clock)
0082     {
0083     }
0084 
0085     void SaveHotp::run(void)
0086     {
0087         if (!checkId(m_id) || !checkName(m_accountName) || !checkIssuer(m_issuer) ||
0088             !checkTokenLength(m_tokenLength) || !checkOffset(m_offset, QCryptographicHash::Sha1)) {
0089             qCDebug(logger)
0090                 << "Unable to save HOTP account:" << m_id
0091                 << "Invalid account details";
0092             Q_EMIT invalid();
0093             Q_EMIT finished();
0094             return;
0095         }
0096 
0097         const PersistenceAction act([this](QSettings &settings) -> void
0098         {
0099             if (!settings.isWritable()) {
0100                 qCWarning(logger)
0101                     << "Unable to save HOTP account:" << m_id
0102                     << "Storage not writable";
0103                 Q_EMIT invalid();
0104                 return;
0105             }
0106 
0107             qCInfo(logger) << "Saving HOTP account:" << m_id;
0108 
0109             const QString group = m_id.toString();
0110             settings.remove(group);
0111             settings.beginGroup(group);
0112             settings.setValue(QStringLiteral("account"), m_accountName);
0113             if (!m_issuer.isNull()) {
0114                 settings.setValue(QStringLiteral("issuer"), m_issuer);
0115             }
0116             settings.setValue(QStringLiteral("type"), QStringLiteral("hotp"));
0117             QString encodedNonce = QString::fromUtf8(m_secret.nonce().toBase64(QByteArray::Base64Encoding));
0118             QString encodedSecret = QString::fromUtf8(m_secret.cryptText().toBase64(QByteArray::Base64Encoding));
0119             settings.setValue(QStringLiteral("secret"), encodedSecret);
0120             settings.setValue(QStringLiteral("nonce"), encodedNonce);
0121             settings.setValue(QStringLiteral("counter"), m_counter);
0122             settings.setValue(QStringLiteral("pinLength"), m_tokenLength);
0123             if (m_offset) {
0124                 settings.setValue(QStringLiteral("offset"), *m_offset);
0125             }
0126             settings.setValue(QStringLiteral("checksum"), m_checksum);
0127             settings.endGroup();
0128 
0129             // Try to guarantee that data will have been written before claiming the account was actually saved
0130             settings.sync();
0131 
0132             Q_EMIT saved(m_id, m_accountName, m_issuer, m_secret.cryptText(), m_secret.nonce(), m_tokenLength,
0133                          m_counter, m_offset.has_value(), m_offset ? *m_offset : 0U, m_checksum);
0134         });
0135         m_settings(act);
0136 
0137         Q_EMIT finished();
0138     }
0139 
0140     void SaveTotp::run(void)
0141     {
0142         if (!checkId(m_id) || !checkName(m_accountName) || !checkIssuer(m_issuer) ||
0143             !checkTokenLength(m_tokenLength) || !checkTimeStep(m_timeStep) || !checkEpoch(m_epoch, m_clock)) {
0144             qCDebug(logger)
0145                 << "Unable to save TOTP account:" << m_id
0146                 << "Invalid account details";
0147             Q_EMIT invalid();
0148             Q_EMIT finished();
0149             return;
0150         }
0151 
0152         const PersistenceAction act([this](QSettings &settings) -> void
0153         {
0154             if (!settings.isWritable()) {
0155                 qCWarning(logger)
0156                     << "Unable to save TOTP account:" << m_id
0157                     << "Storage not writable";
0158                 Q_EMIT invalid();
0159                 return;
0160             }
0161 
0162             qCInfo(logger) << "Saving TOTP account:" << m_id;
0163 
0164             const QString group = m_id.toString();
0165             settings.remove(group);
0166             settings.beginGroup(group);
0167             settings.setValue(QStringLiteral("account"), m_accountName);
0168             if (!m_issuer.isNull()) {
0169                 settings.setValue(QStringLiteral("issuer"), m_issuer);
0170             }
0171             settings.setValue(QStringLiteral("type"), QStringLiteral("totp"));
0172             QString encodedNonce = QString::fromUtf8(m_secret.nonce().toBase64(QByteArray::Base64Encoding));
0173             QString encodedSecret = QString::fromUtf8(m_secret.cryptText().toBase64(QByteArray::Base64Encoding));
0174             settings.setValue(QStringLiteral("secret"), encodedSecret);
0175             settings.setValue(QStringLiteral("nonce"), encodedNonce);
0176             settings.setValue(QStringLiteral("timeStep"), m_timeStep);
0177             settings.setValue(QStringLiteral("pinLength"), m_tokenLength);
0178             settings.setValue(QStringLiteral("epoch"), m_epoch.toUTC().toString(Qt::ISODateWithMs));
0179             settings.setValue(QStringLiteral("hash"), QVariant::fromValue<Account::Hash>(m_hash).toString());
0180             settings.endGroup();
0181 
0182             // Try to guarantee that data will have been written before claiming the account was actually saved
0183             settings.sync();
0184 
0185             Q_EMIT saved(m_id, m_accountName, m_issuer, m_secret.cryptText(), m_secret.nonce(), m_tokenLength,
0186                          m_timeStep, m_epoch, m_hash);
0187         });
0188         m_settings(act);
0189 
0190         Q_EMIT finished();
0191     }
0192 
0193     void DeleteAccounts::run(void)
0194     {
0195         const PersistenceAction act([this](QSettings &settings) -> void
0196         {
0197             if (!settings.isWritable()) {
0198                 qCWarning(logger) << "Unable to delete accounts: storage not writable";
0199                 Q_EMIT invalid();
0200                 return;
0201             }
0202 
0203             qCInfo(logger) << "Deleting accounts";
0204 
0205             for (const QUuid &id : m_ids) {
0206                 settings.remove(id.toString());
0207             }
0208         });
0209         m_settings(act);
0210 
0211         Q_EMIT finished();
0212     }
0213 
0214     void RequestAccountPassword::fail(void)
0215     {
0216         if (m_failed || m_succeeded) {
0217             qCDebug(logger) << "Suppressing 'failure' in unlocking accounts: already handled";
0218             return;
0219         }
0220 
0221         m_failed = true;
0222         QObject::disconnect(m_secret, &AccountSecret::requestsCancelled, this, &RequestAccountPassword::fail);
0223         QObject::disconnect(m_secret, &AccountSecret::passwordAvailable, this, &RequestAccountPassword::unlock);
0224         QObject::disconnect(m_secret, &AccountSecret::keyAvailable, this, &RequestAccountPassword::finish);
0225         Q_EMIT failed();
0226         Q_EMIT finished();
0227     }
0228 
0229     void RequestAccountPassword::unlock(void)
0230     {
0231         secrets::SecureMasterKey * derived = m_secret->deriveKey();
0232         std::optional<secrets::EncryptedSecret> challenge = m_secret->challenge();
0233         if (derived && challenge) {
0234             qCInfo(logger) << "Successfully derived key for storage";
0235             return;
0236         } else {
0237             qCInfo(logger) << "Failed to unlock storage:"
0238                 << "Unable to derive secret encryption/decryption key or generate its matching challenge";
0239         }
0240     }
0241 
0242     void RequestAccountPassword::finish(void)
0243     {
0244         if (m_succeeded || m_failed) {
0245             qCDebug(logger) << "Suppressing 'success' in unlocking accounts: already handled";
0246             return;
0247         }
0248 
0249         QObject::disconnect(m_secret, &AccountSecret::requestsCancelled, this, &RequestAccountPassword::fail);
0250         QObject::disconnect(m_secret, &AccountSecret::passwordAvailable, this, &RequestAccountPassword::unlock);
0251         QObject::disconnect(m_secret, &AccountSecret::keyAvailable, this, &RequestAccountPassword::finish);
0252         std::optional<secrets::EncryptedSecret> challenge = m_secret->challenge();
0253         secrets::SecureMasterKey * derived = m_secret->key();
0254         if (!derived) {
0255             qCInfo(logger) << "Failed to finish unlocking storage: no secret encryption/decryption key";
0256             m_failed = true;
0257             Q_EMIT failed();
0258             Q_EMIT finished();
0259             return;
0260         }
0261 
0262         // sanity check: challenge should be available once key derivation has completed successfully
0263         if (!challenge) {
0264             qCInfo(logger) << "Failed to finish unlocking storage: no challenge for encryption/decryption key";
0265             m_failed = true;
0266             Q_EMIT failed();
0267             Q_EMIT finished();
0268             return;
0269         }
0270 
0271         bool ok = false;
0272         m_settings([derived, &challenge, &ok](QSettings &settings) -> void
0273         {
0274             if (!settings.isWritable()) {
0275                 qCWarning(logger) << "Unable to save account secret key parameters: storage not writable";
0276                 return;
0277             }
0278 
0279             const secrets::KeyDerivationParameters params = derived->params();
0280 
0281             QString encodedSalt = QString::fromUtf8(derived->salt().toBase64(QByteArray::Base64Encoding));
0282             QString encodedChallenge = QString::fromUtf8(challenge->cryptText().toBase64(QByteArray::Base64Encoding));
0283             QString encodedNonce = QString::fromUtf8(challenge->nonce().toBase64(QByteArray::Base64Encoding));
0284             settings.beginGroup(QStringLiteral("master-key"));
0285             settings.setValue(QStringLiteral("salt"), encodedSalt);
0286             settings.setValue(QStringLiteral("cpu"), params.cpuCost());
0287             settings.setValue(QStringLiteral("memory"), (quint64) params.memoryCost());
0288             settings.setValue(QStringLiteral("algorithm"), params.algorithm());
0289             settings.setValue(QStringLiteral("length"), params.keyLength());
0290             settings.setValue(QStringLiteral("nonce"), encodedNonce);
0291             settings.setValue(QStringLiteral("challenge"), encodedChallenge);
0292             settings.endGroup();
0293             ok = true;
0294         });
0295 
0296         if (ok) {
0297             qCInfo(logger) << "Successfully unlocked storage";
0298             m_succeeded = true;
0299             Q_EMIT unlocked();
0300         } else {
0301             qCInfo(logger) << "Failed to finish unlocking storage: unable to store parameters";
0302             m_failed = true;
0303             Q_EMIT failed();
0304         }
0305         Q_EMIT finished();
0306     }
0307 
0308     void RequestAccountPassword::run(void)
0309     {
0310         if (!m_secret) {
0311             qCDebug(logger) << "Unable to request accounts password: no account secret object";
0312             m_failed = true;
0313             Q_EMIT failed();
0314             Q_EMIT finished();
0315             return;
0316         }
0317 
0318         QObject::connect(m_secret, &AccountSecret::passwordAvailable, this, &RequestAccountPassword::unlock);
0319         QObject::connect(m_secret, &AccountSecret::requestsCancelled, this, &RequestAccountPassword::fail);
0320         QObject::connect(m_secret, &AccountSecret::keyAvailable, this, &RequestAccountPassword::finish);
0321 
0322         if (!m_secret->isStillAlive()) {
0323             qCDebug(logger) << "Unable to request accounts password: account secret marked for death";
0324             fail();
0325             return;
0326         }
0327 
0328         bool ok = false;
0329         m_settings([this, &ok](QSettings &settings) -> void
0330         {
0331             if (!settings.isWritable()) {
0332                 qCWarning(logger) << "Unable to request password for accounts: storage not writable";
0333                 return;
0334             }
0335 
0336             QStringList groups = settings.childGroups();
0337             if (!groups.contains(QStringLiteral("master-key"))) {
0338                 qCInfo(logger) << "No key derivation parameters found: requesting 'new' password for accounts";
0339                 ok = m_secret->requestNewPassword();
0340                 return;
0341             }
0342 
0343             settings.beginGroup(QStringLiteral("master-key"));
0344             QByteArray salt;
0345             QByteArray nonce;
0346             QByteArray challenge;
0347             quint64 cpuCost = 0ULL;
0348             quint64 keyLength = 0ULL;
0349             size_t memoryCost = 0ULL;
0350             // HACK: disables challenge verification, remove at some point!
0351             bool challengeAvailable = settings.contains(QStringLiteral("challenge"));
0352             int algorithm = settings.value(QStringLiteral("algorithm")).toInt(&ok);
0353             if (ok) {
0354                 ok = false;
0355                 keyLength = settings.value(QStringLiteral("length")).toULongLong(&ok);
0356             }
0357             if (ok) {
0358                 ok = false;
0359                 cpuCost = settings.value(QStringLiteral("cpu")).toULongLong(&ok);
0360             }
0361             if (ok) {
0362                 ok = false;
0363                 memoryCost = settings.value(QStringLiteral("memory")).toULongLong(&ok);
0364             }
0365             if (ok) {
0366                 QByteArray encodedSalt = settings.value(QStringLiteral("salt")).toString().toUtf8();
0367                 salt = QByteArray::fromBase64(encodedSalt, QByteArray::Base64Encoding);
0368                 ok = !salt.isEmpty() && secrets::SecureMasterKey::validate(salt);
0369             }
0370 
0371             // HACK: disables challenge verification, remove at some point!
0372             if (challengeAvailable && ok) {
0373                 QByteArray encodedChallenge = settings.value(QStringLiteral("challenge")).toString().toUtf8();
0374                 challenge = QByteArray::fromBase64(encodedChallenge, QByteArray::Base64Encoding);
0375                 ok = !challenge.isEmpty();
0376             }
0377             // HACK: disables challenge verification, remove at some point!
0378             if (challengeAvailable && ok) {
0379                 QByteArray encodedNonce = settings.value(QStringLiteral("nonce")).toString().toUtf8();
0380                 nonce = QByteArray::fromBase64(encodedNonce, QByteArray::Base64Encoding);
0381                 ok = !nonce.isEmpty();
0382             }
0383             settings.endGroup();
0384 
0385             const auto params = secrets::KeyDerivationParameters::create(keyLength, algorithm, memoryCost, cpuCost);
0386             const auto encryptedChallenge = secrets::EncryptedSecret::from(challenge, nonce);
0387 
0388             // HACK: disables challenge verification, remove at some point!
0389             if (!ok || !params || !secrets::SecureMasterKey::validate(*params) || (challengeAvailable && !encryptedChallenge)) {
0390                 qCDebug(logger) << "Unable to request 'existing' password: invalid challenge, nonce, salt or key derivation parameters";
0391                 return;
0392             }
0393 
0394             qCInfo(logger) << "Requesting 'existing' password for accounts";
0395             ok = challengeAvailable
0396                 ? m_secret->requestExistingPassword(*encryptedChallenge, salt, *params)
0397                 : m_secret->requestExistingPassword(salt, *params); // HACK: disables challenge verification, remove at some point!
0398         });
0399 
0400         if (!ok) {
0401             qCInfo(logger) << "Unable to unlock storage: failed to request password for accounts";
0402             fail();
0403         }
0404     }
0405 
0406     void LoadAccounts::run(void)
0407     {
0408         if (!m_secret || !m_secret->key()) {
0409             qCDebug(logger) << "Unable to load accounts: secret decryption key not available";
0410             Q_EMIT finished();
0411             return;
0412         }
0413 
0414         bool failed = false;
0415         const PersistenceAction act([this, &failed](QSettings &settings) -> void
0416         {
0417             qCInfo(logger, "Loading accounts from storage");
0418             const QStringList entries = settings.childGroups();
0419             for (const QString &group : entries) {
0420                 if (group == QLatin1String("master-key")) {
0421                     continue;
0422                 }
0423 
0424                 const QUuid id(group);
0425                 if (id.isNull()) {
0426                     qCDebug(logger)
0427                         << "Ignoring:" << group
0428                         << "Not an account section";
0429                     failed = true;
0430                     continue;
0431                 }
0432 
0433                 settings.beginGroup(group);
0434 
0435                 const QString accountName = settings.value(QStringLiteral("account")).toString();
0436                 if (!checkName(accountName)) {
0437                     qCWarning(logger)
0438                         << "Skipping invalid account:" << id
0439                         << "Invalid account name";
0440                     settings.endGroup();
0441                     continue;
0442                 }
0443 
0444                 const QString issuer = settings.value(QStringLiteral("issuer"), QString()).toString();
0445                 if (!checkIssuer(issuer)) {
0446                     qCWarning(logger)
0447                         << "Skipping invalid account:" << id
0448                         << "Invalid account issuer";
0449                     settings.endGroup();
0450                     continue;
0451                 }
0452 
0453                 const QString type = settings.value(QStringLiteral("type")).toString();
0454                 if (type != QStringLiteral("hotp") && type != QStringLiteral("totp")) {
0455                     qCWarning(logger)
0456                         << "Skipping invalid account:" << id
0457                         << "Invalid account type";
0458                     settings.endGroup();
0459                     failed = true;
0460                     continue;
0461                 }
0462 
0463                 bool ok = false;
0464                 const int tokenLength = settings.value(QStringLiteral("pinLength")).toInt(&ok);
0465                 if (!ok || !checkTokenLength(tokenLength)) {
0466                     qCWarning(logger)
0467                         << "Skipping invalid account:" << id
0468                         << "Invalid token length";
0469                     settings.endGroup();
0470                     failed = true;
0471                     continue;
0472                 }
0473 
0474                 const QByteArray encodedNonce = settings.value(QStringLiteral("nonce")).toString().toUtf8();
0475                 const QByteArray encodedSecret = settings.value(QStringLiteral("secret")).toString().toUtf8();
0476                 const QByteArray nonce = QByteArray::fromBase64(encodedNonce, QByteArray::Base64Encoding);
0477                 const QByteArray secret = QByteArray::fromBase64(encodedSecret, QByteArray::Base64Encoding);
0478 
0479                 const auto encryptedSecret = secrets::EncryptedSecret::from(secret, nonce);
0480                 if (!encryptedSecret) {
0481                     qCWarning(logger)
0482                         << "Skipping invalid account:" << id
0483                         << "Invalid token secret";
0484                     settings.endGroup();
0485                     failed = true;
0486                     continue;
0487                 }
0488 
0489                 QScopedPointer<secrets::SecureMemory> decrypted(m_secret->decrypt(*encryptedSecret));
0490                 if (!decrypted) {
0491                     qCWarning(logger)
0492                         << "Skipping invalid account:" << id
0493                         << "Unable to decrypt token secret";
0494                     settings.endGroup();
0495                     failed = true;
0496                     continue;
0497                 }
0498 
0499                 if (type == QStringLiteral("totp")) {
0500                     ok = false;
0501                     const uint timeStep = settings.value(QStringLiteral("timeStep")).toUInt(&ok);
0502                     if (!ok || !checkTimeStep(timeStep)) {
0503                         qCWarning(logger)
0504                             << "Skipping invalid account:" << id
0505                             << "Invalid time step";
0506                         settings.endGroup();
0507                         failed = true;
0508                         continue;
0509                     }
0510 
0511                     const QDateTime epoch = settings.value(QStringLiteral("epoch"), QDateTime::fromMSecsSinceEpoch(0))
0512                         .toDateTime();
0513                     if (!checkEpoch(epoch, m_clock)) {
0514                         qCWarning(logger)
0515                             << "Skipping invalid account:" << id
0516                             << "Invalid epoch";
0517                         settings.endGroup();
0518                         failed = true;
0519                         continue;
0520                     }
0521 
0522                     ok = false;
0523 
0524                     const auto hashEnum = QMetaEnum::fromType<accounts::Account::Hash>();
0525                     const auto hashDefault = QVariant::fromValue<accounts::Account::Hash>(accounts::Account::Sha1);
0526                     const QByteArray hashName = settings.value(QStringLiteral("hash"), hashDefault).toByteArray();
0527                     int hash = hashEnum.keyToValue(hashName.constData(), &ok);
0528                     if (!ok) {
0529                         qCWarning(logger)
0530                             << "Skipping invalid account:" << id
0531                             << "Invalid hash";
0532                         settings.endGroup();
0533                         failed = true;
0534                         continue;
0535                     }
0536 
0537                     qCInfo(logger) << "Found valid TOTP account:" << id;
0538                     Q_EMIT foundTotp(id, accountName, issuer, secret, nonce, tokenLength,
0539                                      timeStep, epoch, (Account::Hash) hash);
0540                 }
0541 
0542                 if (type == QStringLiteral("hotp")) {
0543                     ok = false;
0544                     const quint64 counter = settings.value(QStringLiteral("counter")).toULongLong(&ok);
0545                     if (!ok) {
0546                         qCWarning(logger)
0547                             << "Skipping invalid account:" << id
0548                             << "Invalid counter";
0549                         settings.endGroup();
0550                         failed = true;
0551                         continue;
0552                     }
0553 
0554                     const QVariant offsetVariant = settings.value(QStringLiteral("offset"));
0555                     ok = offsetVariant.isNull();
0556                     std::optional<uint> offset = ok ? std::nullopt : std::optional<uint>(offsetVariant.toUInt(&ok));
0557 
0558                     if (!ok || !checkOffset(offset, QCryptographicHash::Sha1)) {
0559                         qCWarning(logger)
0560                             << "Skipping invalid account:" << id
0561                             << "Invalid offset";
0562                         settings.endGroup();
0563                         failed = true;
0564                         continue;
0565                     }
0566 
0567                     const auto checkSumOff = QStringLiteral("false");
0568                     const auto checksum = settings.value(QStringLiteral("checksum"), checkSumOff).toString();
0569                     if (checksum != QStringLiteral("true") && checksum != checkSumOff) {
0570                         qCWarning(logger)
0571                             << "Skipping invalid account:" << id
0572                             << "Invalid checksum";
0573                         settings.endGroup();
0574                         failed = true;
0575                         continue;
0576                     }
0577 
0578                     qCInfo(logger) << "Found valid HOTP account:" << id;
0579                     Q_EMIT foundHotp(id, accountName, issuer, secret, nonce, tokenLength,
0580                                      counter, offset.has_value(), offset ? *offset : 0U,
0581                                      checksum == QStringLiteral("true"));
0582                 }
0583 
0584                 settings.endGroup();
0585             }
0586         });
0587         m_settings(act);
0588 
0589         if (failed) {
0590             Q_EMIT failedToLoadAllAccounts();
0591         }
0592         Q_EMIT finished();
0593     }
0594 
0595     static std::optional<QString> computeToken(const AccountSecret *accountSecret,
0596                                                const secrets::EncryptedSecret &tokenSecret,
0597                                                const oath::Algorithm &algorithm,
0598                                                quint64 counter)
0599     {
0600         QScopedPointer<secrets::SecureMemory> secret(accountSecret->decrypt(tokenSecret));
0601         if (!secret) {
0602             qCDebug(logger) << "Unable to compute token: failed to decrypt account secret";
0603             return std::nullopt;
0604         }
0605 
0606         return algorithm.compute(counter, reinterpret_cast<char*>(secret->data()), secret->size());
0607     }
0608 
0609 
0610     ComputeTotp::ComputeTotp(const AccountSecret *secret,
0611                              const secrets::EncryptedSecret &tokenSecret, uint tokenLength,
0612                              const QDateTime &epoch, uint timeStep, const Account::Hash hash,
0613                              const std::function<qint64(void)> &clock) :
0614         AccountJob(), m_secret(secret), m_tokenSecret(tokenSecret), m_tokenLength(tokenLength),
0615         m_epoch(epoch), m_timeStep(timeStep), m_hash(hash), m_clock(clock)
0616     {
0617     }
0618 
0619     void ComputeTotp::run(void)
0620     {
0621         if (!m_secret || !m_secret->key()) {
0622             qCDebug(logger) << "Unable to compute TOTP token: secret decryption key not available";
0623             Q_EMIT finished();
0624             return;
0625         }
0626 
0627         if (!checkTokenLength(m_tokenLength)) {
0628             qCDebug(logger) << "Unable to compute TOTP token: invalid token length:" << m_tokenLength;
0629             Q_EMIT finished();
0630             return;
0631         }
0632 
0633         if (!checkTimeStep(m_timeStep)) {
0634             qCDebug(logger) << "Unable to compute TOTP token: invalid time step:" << m_timeStep;
0635             Q_EMIT finished();
0636             return;
0637         }
0638 
0639         if (!checkEpoch(m_epoch, m_clock)) {
0640             qCDebug(logger) << "Unable to compute TOTP token: invalid epoch:" << m_epoch;
0641             Q_EMIT finished();
0642             return;
0643         }
0644 
0645         QCryptographicHash::Algorithm hash;
0646         switch(m_hash)
0647         {
0648         case Account::Hash::Sha1:
0649             hash = QCryptographicHash::Sha1;
0650             break;
0651         case Account::Hash::Sha256:
0652             hash = QCryptographicHash::Sha256;
0653             break;
0654         case Account::Hash::Sha512:
0655             hash = QCryptographicHash::Sha512;
0656             break;
0657         default:
0658             qCDebug(logger) << "Unable to compute TOTP token: unknown hashing algorithm:" << m_hash;
0659             Q_EMIT finished();
0660             return;
0661 
0662         }
0663 
0664         const std::optional<oath::Algorithm> algorithm = oath::Algorithm::totp(hash, m_tokenLength);
0665         if (!algorithm) {
0666             qCDebug(logger) << "Unable to compute TOTP token: failed to construct algorithm";
0667             Q_EMIT finished();
0668             return;
0669         }
0670 
0671         const std::optional<quint64> counter = oath::count(m_epoch, m_timeStep, m_clock);
0672         if (!counter) {
0673             qCDebug(logger) << "Unable to compute TOTP token: failed to count time steps";
0674             Q_EMIT finished();
0675             return;
0676         }
0677 
0678         const auto counterValue = *counter;
0679         const auto validFrom = counterValue < maxCounter
0680             ? oath::fromCounter(counterValue + 1ULL, m_epoch, m_timeStep)
0681             : std::nullopt;
0682         const auto validUntil = counterValue < (maxCounter - 1ULL)
0683             ? oath::fromCounter(counterValue + 2ULL, m_epoch, m_timeStep)
0684             : std::nullopt;
0685         if (!validFrom || !validUntil) {
0686             qCDebug(logger) << "Unable to compute TOTP token: failed to determine expiry datetime of tokens";
0687             Q_EMIT finished();
0688             return;
0689         }
0690 
0691         const auto token = computeToken(m_secret, m_tokenSecret, *algorithm, counterValue);
0692         const auto nextToken = token
0693             ? computeToken(m_secret, m_tokenSecret, *algorithm, counterValue + 1ULL)
0694             : std::nullopt;
0695         if (token && nextToken) {
0696             Q_EMIT otp(*token, *nextToken, *validFrom, *validUntil);
0697         } else {
0698             qCDebug(logger) << "Failed to compute TOTP tokens";
0699         }
0700 
0701         Q_EMIT finished();
0702     }
0703 
0704     ComputeHotp::ComputeHotp(const AccountSecret *secret,
0705                              const secrets::EncryptedSecret &tokenSecret, uint tokenLength,
0706                              quint64 counter, const std::optional<uint> offset, bool checksum) :
0707         AccountJob(), m_secret(secret), m_tokenSecret(tokenSecret), m_tokenLength(tokenLength),
0708         m_counter(counter), m_offset(offset), m_checksum(checksum)
0709     {
0710     }
0711 
0712     void ComputeHotp::run(void)
0713     {
0714         if (!m_secret || !m_secret->key()) {
0715             qCDebug(logger) << "Unable to compute HOTP token: secret decryption key not available";
0716             Q_EMIT finished();
0717             return;
0718         }
0719 
0720         if (!checkTokenLength(m_tokenLength)) {
0721             qCDebug(logger) << "Unable to compute HOTP token: invalid token length:" << m_tokenLength;
0722             Q_EMIT finished();
0723             return;
0724         }
0725 
0726         if (!checkOffset(m_offset, QCryptographicHash::Sha1)) {
0727             qCDebug(logger) << "Unable to compute HOTP token: invalid offset:" << *m_offset;
0728             Q_EMIT finished();
0729             return;
0730         }
0731 
0732         const std::optional<oath::Algorithm> algorithm = oath::Algorithm::hotp(m_offset, m_tokenLength, m_checksum);
0733         if (!algorithm) {
0734             qCDebug(logger) << "Unable to compute HOTP token: failed to construct algorithm";
0735             Q_EMIT finished();
0736             return;
0737         }
0738 
0739         if (m_counter == maxCounter) {
0740             qCDebug(logger) << "Unable to compute HOTP token: counter reached its limit";
0741             Q_EMIT finished();
0742             return;
0743         }
0744 
0745         const auto token = computeToken(m_secret, m_tokenSecret, *algorithm, m_counter);
0746         const auto nextToken = token
0747             ? computeToken(m_secret, m_tokenSecret, *algorithm, m_counter + 1ULL)
0748             : std::nullopt;
0749         if (token && nextToken) {
0750             Q_EMIT otp(*token, *nextToken, m_counter + 1ULL);
0751         } else {
0752             qCDebug(logger) << "Failed to compute HOTP tokens";
0753         }
0754 
0755         Q_EMIT finished();
0756     }
0757 
0758     Dispatcher::Dispatcher(QThread *thread, QObject *parent) :
0759         QObject(parent), m_thread(thread),  m_current(nullptr)
0760     {
0761     }
0762 
0763     bool Dispatcher::empty(void) const
0764     {
0765         return m_pending.isEmpty();
0766     }
0767 
0768     void Dispatcher::queueAndProceed(AccountJob *job, const std::function<void(void)> &setup_callbacks)
0769     {
0770         if (job) {
0771             qCDebug(dispatcherLogger) << "Queuing job for dispatcher";
0772             job->moveToThread(m_thread);
0773             setup_callbacks();
0774             m_pending.append(job);
0775             dispatchNext();
0776         }
0777     }
0778 
0779     void Dispatcher::dispatchNext(void)
0780     {
0781         qCDebug(dispatcherLogger) << "Handling request to dispatch next job";
0782 
0783         if (!empty() && !m_current) {
0784             qCDebug(dispatcherLogger) << "Dispatching next job";
0785 
0786             m_current = m_pending.takeFirst();
0787             QObject::connect(m_current, &AccountJob::finished, this, &Dispatcher::next);
0788             QObject::connect(this, &Dispatcher::dispatch, m_current, &AccountJob::run);
0789             Q_EMIT dispatch();
0790         }
0791     }
0792 
0793     void Dispatcher::next(void)
0794     {
0795         qCDebug(dispatcherLogger) << "Handling next continuation in dispatcher";
0796 
0797         QObject *from = sender();
0798         AccountJob *job = from ? qobject_cast<AccountJob*>(from) : nullptr;
0799         if (job) {
0800             Q_ASSERT_X(job == m_current, Q_FUNC_INFO, "sender() should match 'current' job!");
0801             QObject::disconnect(this, &Dispatcher::dispatch, job, &AccountJob::run);
0802             // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks): False positives with QTimer::singleShot
0803             QTimer::singleShot(0, job, &AccountJob::deleteLater);
0804             m_current = nullptr;
0805             dispatchNext();
0806         }
0807     }
0808 }