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 }