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

0001 /*
0002  * SPDX-License-Identifier: GPL-3.0-or-later
0003  * SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
0004  */
0005 #include "accounts.h"
0006 
0007 #include "../logging_p.h"
0008 
0009 KEYSMITH_LOGGER(logger, ".model.accounts")
0010 
0011 namespace model
0012 {
0013     qint64 millisecondsLeftForToken(const QDateTime &epoch, uint timeStep, const std::function<qint64(void)> &clock)
0014     {
0015         QDateTime now = QDateTime::fromMSecsSinceEpoch(clock());
0016         if (!epoch.isValid() || !now.isValid() || timeStep == 0) {
0017             qCDebug(logger) << "Unable to compute milliseconds left: invalid arguments";
0018             return -1;
0019         }
0020 
0021         /*
0022          * Avoid integer overflow by casting to the wider type first before multiplying.
0023          * Not likely to happen 'in the wild', but good practice nevertheless
0024          */
0025         qint64 step = ((qint64) timeStep) * 1000LL;
0026 
0027         qint64 diff = epoch.msecsTo(now);
0028 
0029         /*
0030          * Compensate for the fact that % operator is not the same as mathematical mod in case diff is negative.
0031          * diff is negative when the given epoch is in the 'future' compared to the current clock value.
0032          */
0033         return diff < 0 ? - (diff % step) : step - (diff % step);
0034     }
0035 
0036     AccountView::AccountView(accounts::Account *model, QObject *parent) :
0037         QObject(parent), m_model(model)
0038     {
0039         QObject::connect(model, &accounts::Account::tokenChanged, this, &AccountView::tokenChanged);
0040         QObject::connect(this, &AccountView::remove, model, &accounts::Account::remove);
0041         QObject::connect(this, &AccountView::recompute, model, &accounts::Account::recompute);
0042         QObject::connect(this, &AccountView::advanceCounter, model, &accounts::Account::advanceCounter);
0043         QObject::connect(this, &AccountView::setCounter, model, &accounts::Account::setCounter);
0044     }
0045 
0046     bool AccountView::isHotp(void) const
0047     {
0048         return m_model->algorithm() == accounts::Account::Hotp;
0049     }
0050 
0051     bool AccountView::isTotp(void) const
0052     {
0053         return m_model->algorithm() == accounts::Account::Totp;
0054     }
0055 
0056     QString AccountView::name(void) const
0057     {
0058         return m_model->name();
0059     }
0060 
0061     QString AccountView::token(void) const
0062     {
0063         return m_model->token();
0064     }
0065 
0066     QString AccountView::issuer(void) const
0067     {
0068         return m_model->issuer();
0069     }
0070 
0071     quint64 AccountView::counter(void) const
0072     {
0073         return m_model->counter();
0074     }
0075 
0076     QDateTime AccountView::epoch(void) const
0077     {
0078         return m_model->epoch();
0079     }
0080 
0081     uint AccountView::timeStep(void) const
0082     {
0083         return m_model->timeStep();
0084     }
0085 
0086     uint AccountView::offset(void) const
0087     {
0088         return m_model->offset().value_or(0);
0089     }
0090 
0091     int AccountView::tokenLength(void) const
0092     {
0093         return m_model->tokenLength();
0094     }
0095 
0096     QString AccountView::hash(void) const
0097     {
0098         switch (m_model->hash()) {
0099         case accounts::Account::Sha1:
0100             return QStringLiteral("SHA1");
0101         case accounts::Account::Sha256:
0102             return QStringLiteral("SHA256");
0103         case accounts::Account::Sha512:
0104             return QStringLiteral("SHA512");
0105         }
0106         return QString();
0107     }
0108 
0109     qint64 AccountView::millisecondsLeftForToken(void) const
0110     {
0111         if (!isTotp()) {
0112             qCDebug(logger) << "Unable to compute milliseconds left for token, wrong account type:" << m_model->algorithm();
0113             return -1;
0114         }
0115 
0116         return model::millisecondsLeftForToken(m_model->epoch(), m_model->timeStep());
0117     }
0118 
0119     SimpleAccountListModel::SimpleAccountListModel(accounts::AccountStorage *storage, QObject *parent) :
0120         QAbstractListModel(parent), m_storage(storage), m_has_error(false), m_index(QVector<QString>())
0121     {
0122         QObject::connect(storage, &accounts::AccountStorage::added, this, &SimpleAccountListModel::added);
0123         QObject::connect(storage, &accounts::AccountStorage::removed, this, &SimpleAccountListModel::removed);
0124         QObject::connect(storage, &accounts::AccountStorage::error, this, &SimpleAccountListModel::handleError);
0125         QObject::connect(storage, &accounts::AccountStorage::loaded, this, &SimpleAccountListModel::loadedChanged);
0126 
0127         beginResetModel();
0128         const auto accounts = m_storage->accounts();
0129         for (const QString &name : accounts) {
0130             populate(name, createView(name));
0131         }
0132         m_has_error = storage->hasError();
0133         endResetModel();
0134     }
0135 
0136 
0137     AccountView * SimpleAccountListModel::createView(const QString &name)
0138     {
0139         accounts::Account * existingAccount = m_storage->get(name);
0140         if (!existingAccount) {
0141             qCDebug(logger) << "Account storage did not yield a valid account object for account name";
0142             return nullptr;
0143         }
0144 
0145         return new AccountView(existingAccount, this);
0146     }
0147 
0148     void SimpleAccountListModel::populate(const QString &name, AccountView *account)
0149     {
0150         if (!account) {
0151             qCDebug(logger) << "Not populating account without a valid account view object";
0152             return;
0153         }
0154 
0155         m_index.append(name);
0156         m_accounts[name] = account;
0157         Q_EMIT account->recompute();
0158     }
0159 
0160     bool SimpleAccountListModel::error(void) const
0161     {
0162         return m_has_error;
0163     }
0164 
0165     void SimpleAccountListModel::setError(bool markAsError)
0166     {
0167         if (!markAsError && m_storage->hasError()) {
0168             m_storage->clearError();
0169         }
0170 
0171         if (markAsError != m_has_error) {
0172             m_has_error = markAsError;
0173             Q_EMIT errorChanged();
0174         }
0175     }
0176 
0177     void SimpleAccountListModel::handleError(void)
0178     {
0179         setError(true);
0180     }
0181 
0182     bool SimpleAccountListModel::loaded(void) const
0183     {
0184         return m_storage->isLoaded();
0185     }
0186 
0187     accounts::Account::Hash SimpleAccountListModel::toHash(TOTPAlgorithms value)
0188     {
0189         switch (value) {
0190         case TOTPAlgorithms::Sha1:
0191             return accounts::Account::Hash::Sha1;
0192         case TOTPAlgorithms::Sha256:
0193             return accounts::Account::Hash::Sha256;
0194         case TOTPAlgorithms::Sha512:
0195             return accounts::Account::Hash::Sha512;
0196         default:
0197             Q_ASSERT_X(false, Q_FUNC_INFO, "Unknown/unsupported totp algorithm value");
0198             return accounts::Account::Hash::Sha1;
0199         }
0200     }
0201 
0202     void SimpleAccountListModel::addAccount(AccountInput *input)
0203     {
0204         if (!input) {
0205             qCDebug(logger) << "Not adding account, no input provided";
0206             return;
0207         }
0208         input->createNewAccount(m_storage);
0209     }
0210 
0211     QHash<int, QByteArray> SimpleAccountListModel::roleNames(void) const
0212     {
0213         QHash<int, QByteArray> roles;
0214         roles[NonStandardRoles::AccountRole] = "account";
0215         return roles;
0216     }
0217 
0218     QVariant SimpleAccountListModel::data(const QModelIndex &account, int role) const
0219     {
0220         if (!account.isValid()) {
0221             qCDebug(logger) << "Not returning any data, model index is invalid";
0222             return QVariant();
0223         }
0224 
0225         int accountIndex = account.row();
0226         if (accountIndex < 0 || m_index.size() < accountIndex) {
0227             qCDebug(logger) << "Not returning any data, model index is out of bounds:" << accountIndex
0228                 << "model size is:" << m_index.size();
0229             return QVariant();
0230         }
0231 
0232         if (role != NonStandardRoles::AccountRole) {
0233             qCDebug(logger) << "Not returning any data, unknown role:" << role;
0234             return QVariant();
0235         }
0236 
0237         const QString accountName = m_index.at(accountIndex);
0238         auto model = m_accounts.value(accountName, nullptr);
0239         if (!model) {
0240             qCDebug(logger) << "Not returning any data, unable to find associated account for:" << accountIndex;
0241             return QVariant();
0242         }
0243 
0244         return QVariant::fromValue(model);
0245     }
0246 
0247     int SimpleAccountListModel::rowCount(const QModelIndex &parent) const
0248     {
0249         return parent.isValid() ? 0 : m_index.size();
0250     }
0251 
0252     void SimpleAccountListModel::added(const QString &account)
0253     {
0254         auto newAccount = createView(account);
0255         if (!newAccount) {
0256             qCDebug(logger) << "Unable to handle added account: unable to construct account view object";
0257             return;
0258         }
0259 
0260         if (m_accounts.contains(account)) {
0261             qCDebug(logger) << "Added account already/still part of the model: requesting removal of the old one from the model first";
0262             removed(account);
0263         }
0264 
0265         int accountIndex = m_index.size();
0266         qCDebug(logger) << "Adding (new) account to the model at position:" << accountIndex;
0267 
0268         beginInsertRows(QModelIndex(), accountIndex, accountIndex);
0269         populate(account, newAccount);
0270         endInsertRows();
0271     }
0272 
0273     void SimpleAccountListModel::removed(const QString &account)
0274     {
0275         int accountIndex = m_index.indexOf(account);
0276         if (accountIndex < 0) {
0277             qCDebug(logger) << "Unable to handle account removal: account not part of the model";
0278             return;
0279         }
0280 
0281         AccountView *v = nullptr;
0282 
0283         qCDebug(logger) << "Removing (old) account from the model at position:" << accountIndex;
0284         beginRemoveRows(QModelIndex(), accountIndex, accountIndex);
0285         m_index.remove(accountIndex);
0286         v = m_accounts.take(account);
0287         endRemoveRows();
0288 
0289         if (v) {
0290             v->deleteLater();
0291         }
0292     }
0293 
0294     bool SimpleAccountListModel::isAccountStillAvailable(const QString &name, const QString &issuer) const
0295     {
0296         return m_storage && m_storage->isAccountStillAvailable(name, issuer);
0297     }
0298 
0299     AccountNameValidator::AccountNameValidator(QObject *parent) :
0300         QValidator(parent), m_validateAvailability(true), m_issuer(std::nullopt), m_accounts(nullptr), m_delegate(nullptr)
0301     {
0302     }
0303 
0304     bool AccountNameValidator::validateAvailability(void) const
0305     {
0306         return m_validateAvailability;
0307     }
0308 
0309     void AccountNameValidator::setValidateAvailability(bool enabled)
0310     {
0311         if (enabled != m_validateAvailability) {
0312             m_validateAvailability = enabled;
0313             Q_EMIT validateAvailabilityChanged();
0314         }
0315     }
0316 
0317     QString AccountNameValidator::issuer(void) const
0318     {
0319         return m_issuer
0320             ? *m_issuer
0321             : QStringLiteral(":: WARNING: dummy invalid issuer; this is meant as a write-only property anyway ::");
0322     }
0323 
0324     void AccountNameValidator::setIssuer(const QString &issuer)
0325     {
0326         if (m_issuer && issuer == *m_issuer) {
0327             qCDebug(logger) << "Ignoring new issuer: same as the current issuer";
0328             return;
0329         }
0330 
0331         m_issuer.emplace(issuer);
0332         Q_EMIT issuerChanged();
0333     }
0334 
0335     QValidator::State AccountNameValidator::validate(QString &input, int &pos) const
0336     {
0337         QValidator::State result = m_delegate.validate(input, pos);
0338         if (!m_validateAvailability) {
0339             qCDebug(logger) << "Not validating account availability: explicitly disabled";
0340             return result;
0341         }
0342 
0343         if (!m_accounts) {
0344             qCDebug(logger) << "Unable to validate account name: missing accounts model object";
0345             return QValidator::Invalid;
0346         }
0347 
0348         if (!m_issuer) {
0349             qCDebug(logger) << "Unable to validate account name: missing issuer";
0350             return QValidator::Invalid;
0351         }
0352 
0353         return result != QValidator::Acceptable || m_accounts->isAccountStillAvailable(input, *m_issuer)
0354             ? result
0355             : QValidator::Intermediate;
0356     }
0357 
0358     void AccountNameValidator::fixup(QString &input) const
0359     {
0360         m_delegate.fixup(input);
0361     }
0362 
0363     SimpleAccountListModel * AccountNameValidator::accounts(void) const
0364     {
0365         return m_accounts;
0366     }
0367 
0368     void AccountNameValidator::setAccounts(SimpleAccountListModel *accounts)
0369     {
0370         if (!accounts) {
0371             qCDebug(logger) << "Ignoring new accounts model: not a valid object";
0372             return;
0373         }
0374 
0375         m_accounts = accounts;
0376         Q_EMIT accountsChanged();
0377     }
0378 
0379     SortedAccountsListModel::SortedAccountsListModel(QObject *parent) : QSortFilterProxyModel(parent)
0380     {
0381     }
0382 
0383     void SortedAccountsListModel::setSourceModel(QAbstractItemModel *sourceModel)
0384     {
0385         SimpleAccountListModel *model = qobject_cast<SimpleAccountListModel*>(sourceModel);
0386         if (!model) {
0387             qCDebug(logger) << "Not setting source model: it is not an accounts list model!";
0388             return;
0389         }
0390 
0391         QSortFilterProxyModel::setSourceModel(sourceModel);
0392         qCDebug(logger) << "Updating properties & resorting the model";
0393         setSortRole(SimpleAccountListModel::NonStandardRoles::AccountRole);
0394         setDynamicSortFilter(true);
0395         setSortLocaleAware(true);
0396         sort(0);
0397     }
0398 
0399     bool SortedAccountsListModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
0400     {
0401         QAbstractItemModel *source = sourceModel();
0402         Q_ASSERT_X(source, Q_FUNC_INFO, "should have a source model at this point");
0403 
0404         const SimpleAccountListModel *model = qobject_cast<const SimpleAccountListModel*>(source);
0405         // useless junk: implement sorting as no-op: claim equality between left & right
0406         if (!model) {
0407             qCDebug(logger) << "Short-circuiting lessThan operator: source model is not an accounts list model!";
0408             return false;
0409         }
0410 
0411         const QVariant leftValue = model->data(source_left, SimpleAccountListModel::NonStandardRoles::AccountRole);
0412         const QVariant rightValue = model->data(source_right, SimpleAccountListModel::NonStandardRoles::AccountRole);
0413 
0414         const AccountView * leftAccount = leftValue.isNull() ? nullptr : leftValue.value<AccountView*>();
0415         const AccountView * rightAccount = rightValue.isNull() ? nullptr : rightValue.value<AccountView*>();
0416 
0417         // useless junk: implement sorting as no-op: claim left == right
0418         if (!leftAccount && !rightAccount) {
0419             qCDebug(logger) << "Short-circuiting lessThan operator: both source model indices do not point to accounts";
0420             return false;
0421         }
0422 
0423         // Sort actual accounts before useless junk: claim left >= right
0424         if (!leftAccount) {
0425             qCDebug(logger) << "Short-circuiting lessThan operator: left source model index does not point to an account";
0426             return false;
0427         }
0428 
0429         // Sort actual accounts before useless junk: claim left < right
0430         if (!rightAccount) {
0431             qCDebug(logger) << "Short-circuiting lessThan operator: right source model index does not point to an account";
0432             return true;
0433         }
0434 
0435         const QString leftIssuer = leftAccount->issuer();
0436         const QString rightIssuer = rightAccount->issuer();
0437 
0438         // both issuers are null: sort by account name
0439         if (leftIssuer.isNull() && rightIssuer.isNull()) {
0440             return leftAccount->name().localeAwareCompare(rightAccount->name()) < 0;
0441         }
0442 
0443         // Sort accounts without issuer to the top: claim left < right
0444         if (leftIssuer.isNull()) {
0445             return true;
0446         }
0447 
0448         // Sort accounts without issuer to the top: claim left >= right
0449         if (rightIssuer.isNull()) {
0450             return false;
0451         }
0452 
0453         // actual sorting by account issuer, then name
0454         int issuer = leftIssuer.localeAwareCompare(rightIssuer);
0455         return issuer == 0 ? leftAccount->name().localeAwareCompare(rightAccount->name()) < 0 : issuer < 0;
0456     }
0457 }