Rewrite Account model(s) as a separate (sub)module within Keysmith, layered on top of the new Account (storage) module.

This fixes the model part in issue #2
master
Johan Ouwerkerk 2019-12-27 18:14:22 +01:00 committed by Bhushan Shah
parent 0d5b792637
commit a813810dfe
7 changed files with 344 additions and 0 deletions

View File

@ -2,6 +2,7 @@ include_directories(BEFORE ../src)
add_subdirectory(base32)
add_subdirectory(account)
add_subdirectory(model)
add_subdirectory(validators)
set(Account_UUT_SRCS ../src/account.cpp)

View File

@ -0,0 +1,8 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
#
set(Test_DEP_LIBS Qt5::Core Qt5::Test model_lib)
ecm_add_test(milliseconds-left-for-token.cpp LINK_LIBRARIES ${Test_DEP_LIBS} TEST_NAME milliseconds-left-for-token NAME_PREFIX model-)

View File

@ -0,0 +1,62 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
*/
#include "model/accounts.h"
#include <QDateTime>
#include <QObject>
#include <QTest>
#include <functional>
class MillisecondsLeftForTokenTest: public QObject
{
Q_OBJECT
private Q_SLOTS:
void testMillisecondsLeftForToken(void);
void testMillisecondsLeftForToken_data(void);
};
static void define_test_case(const char *testCase, const QDateTime &epoch, uint timeStep, qint64 now, qint64 left)
{
QTest::newRow(qPrintable(QLatin1String(testCase))) << epoch << timeStep << now << left;
}
void MillisecondsLeftForTokenTest::testMillisecondsLeftForToken(void)
{
QFETCH(QDateTime, epoch);
QFETCH(uint, timeStep);
QFETCH(qint64, now);
const std::function<qint64(void)> clock([now](void) -> qint64
{
return now;
});
QTEST(model::millisecondsLeftForToken(epoch, timeStep, clock), "left");
}
void MillisecondsLeftForTokenTest::testMillisecondsLeftForToken_data(void)
{
QTest::addColumn<QDateTime>("epoch");
QTest::addColumn<uint>("timeStep");
QTest::addColumn<qint64>("now");
QTest::addColumn<qint64>("left");
define_test_case("at the epoch itself", QDateTime::fromMSecsSinceEpoch(0), 30U, 0LL, 30LL * 1000LL);
const QString withLeftToGo(QLatin1String("with %1ms left to go"));
for (qint64 left = 1000LL; left > 0; --left) {
QTest::newRow(qPrintable(withLeftToGo.arg(left))) << QDateTime::fromMSecsSinceEpoch(0) << 1U << 1000LL - left << left;
}
define_test_case("at the start of 'next' token", QDateTime::fromMSecsSinceEpoch(0), 30U, 30LL * 1000LL, 30LL * 1000LL);
define_test_case("using a non-standard epoch (in the past)", QDateTime::fromMSecsSinceEpoch(15LL * 1000LL), 30U, 30LL * 1000LL, 15LL * 1000LL);
define_test_case("using a non-standard epoch (in the future)", QDateTime::fromMSecsSinceEpoch(25LL * 1000LL), 30U, 0LL, 25LL * 1000LL);
}
QTEST_APPLESS_MAIN(MillisecondsLeftForTokenTest)
#include "milliseconds-left-for-token.moc"

View File

@ -1,5 +1,6 @@
add_subdirectory(base32)
add_subdirectory(account)
add_subdirectory(model)
add_subdirectory(validators)
set(keysmith_SRCS

11
src/model/CMakeLists.txt Normal file
View File

@ -0,0 +1,11 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
#
set(model_SRCS
accounts.cpp
)
add_library(model_lib STATIC ${model_SRCS})
target_link_libraries(model_lib Qt5::Core Qt5::Qml Qt5::Gui account_lib)

181
src/model/accounts.cpp Normal file
View File

@ -0,0 +1,181 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
*/
#include "accounts.h"
#include <QtDebug>
namespace model
{
qint64 millisecondsLeftForToken(const QDateTime &epoch, uint timeStep, const std::function<qint64(void)> &clock)
{
QDateTime now = QDateTime::fromMSecsSinceEpoch(clock());
if (epoch.isValid() && now.isValid() && timeStep > 0) {
/*
* Avoid integer overflow by casting to the wider type first before multiplying.
* Not likely to happen 'in the wild', but good practice nevertheless
*/
qint64 step = ((qint64) timeStep) * 1000LL;
qint64 diff = epoch.msecsTo(now);
/*
* Compensate for the fact that % operator is not the same as mathemtical mod in case diff is negative.
* diff is negative when the given epoch is in the 'future' compared to the current clock value.
*/
return diff < 0 ? - (diff % step) : step - (diff % step);
}
// TODO: warn if not
return -1;
}
AccountView::AccountView(accounts::Account *model, QObject *parent) : QObject(parent), m_model(model)
{
QObject::connect(model, &accounts::Account::tokenChanged, this, &AccountView::tokenChanged);
QObject::connect(this, &AccountView::remove, model, &accounts::Account::remove);
QObject::connect(this, &AccountView::recompute, model, &accounts::Account::recompute);
QObject::connect(this, &AccountView::advanceCounter, model, &accounts::Account::advanceCounter);
QObject::connect(this, &AccountView::setCounter, model, &accounts::Account::setCounter);
}
bool AccountView::isHotp(void) const
{
return m_model->algorithm() == accounts::Account::Hotp;
}
bool AccountView::isTotp(void) const
{
return m_model->algorithm() == accounts::Account::Totp;
}
QString AccountView::name(void) const
{
return m_model->name();
}
QString AccountView::token(void) const
{
return m_model->token();
}
quint64 AccountView::counter(void) const
{
return m_model->counter();
}
uint AccountView::timeStep(void) const
{
return m_model->timeStep();
}
qint64 AccountView::millisecondsLeftForToken(void) const
{
if (isTotp()) {
return model::millisecondsLeftForToken(m_model->epoch(), m_model->timeStep());
}
// TODO: warn if not
return -1;
}
SimpleAccountListModel::SimpleAccountListModel(accounts::AccountStorage *storage, QObject *parent) : QAbstractListModel(parent), m_storage(storage), m_index(QVector<QString>())
{
QObject::connect(storage, &accounts::AccountStorage::added, this, &SimpleAccountListModel::added);
QObject::connect(storage, &accounts::AccountStorage::removed, this, &SimpleAccountListModel::removed);
beginResetModel();
for (const QString &name : m_storage->accounts()) {
accounts::Account * existingAccount = m_storage->get(name);
if (existingAccount) {
m_index.append(name);
m_accounts[name] = existingAccount;
existingAccount->recompute();
}
// TODO: warn if not
}
endResetModel();
}
void SimpleAccountListModel::addTotp(const QString &account, const QString &secret, uint timeStep, int tokenLength)
{
m_storage->addTotp(account, secret, timeStep, tokenLength);
}
void SimpleAccountListModel::addHotp(const QString &account, const QString &secret, quint64 counter, int tokenLength)
{
m_storage->addHotp(account, secret, counter, tokenLength);
}
QHash<int, QByteArray> SimpleAccountListModel::roleNames(void) const
{
QHash<int, QByteArray> roles;
roles[NonStandardRoles::AccountRole] = "account";
return roles;
}
QVariant SimpleAccountListModel::data(const QModelIndex &account, int role) const
{
if (!account.isValid()) {
// TODO warn about this
return QVariant();
}
int accountIndex = account.row();
if (accountIndex < 0 || m_index.size() < accountIndex) {
// TODO warn about this
return QVariant();
}
if (role == NonStandardRoles::AccountRole) {
const QString accountName = m_index.at(accountIndex);
accounts::Account * model = m_accounts.value(accountName, nullptr);
if (model) {
// assume QML ownership: don't worry about object lifecycle
return QVariant::fromValue(new AccountView(model));
}
}
// TODO warn about this
return QVariant();
}
int SimpleAccountListModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_index.size();
}
void SimpleAccountListModel::added(const QString &account)
{
accounts::Account * newAccount = m_storage->get(account);
if (newAccount) {
if (m_accounts.contains(account)) {
//TODO warn about this
removed(account);
}
int accountIndex = m_index.size();
beginInsertRows(QModelIndex(), accountIndex, accountIndex);
m_index.append(account);
m_accounts[account] = newAccount;
newAccount->recompute();
endInsertRows();
}
// TODO: warn if not
}
void SimpleAccountListModel::removed(const QString &account)
{
int accountIndex = m_index.indexOf(account);
if (accountIndex >= 0) {
beginRemoveRows(QModelIndex(), accountIndex, accountIndex);
m_index.remove(accountIndex);
m_accounts.remove(account);
endRemoveRows();
}
// TODO: warn if not
}
}

80
src/model/accounts.h Normal file
View File

@ -0,0 +1,80 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
*/
#ifndef MODEL_ACCOUNTS_H
#define MODEL_ACCOUNTS_H
#include "../account/account.h"
#include <QAbstractListModel>
#include <QByteArray>
#include <QHash>
#include <QModelIndex>
#include <QObject>
#include <QString>
#include <QVector>
namespace model
{
qint64 millisecondsLeftForToken(const QDateTime &epoch, uint timeStep, const std::function<qint64(void)> &clock = &QDateTime::currentMSecsSinceEpoch);
class AccountView : public QObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name NOTIFY never)
Q_PROPERTY(QString token READ token NOTIFY tokenChanged)
Q_PROPERTY(quint64 counter READ counter NOTIFY tokenChanged);
Q_PROPERTY(uint timeStep READ timeStep NOTIFY never);
Q_PROPERTY(bool isHotp READ isHotp NOTIFY never);
Q_PROPERTY(bool isTotp READ isTotp NOTIFY never);
public:
explicit AccountView(accounts::Account *model, QObject *parent = nullptr);
QString name(void) const;
QString token(void) const;
uint timeStep(void) const;
quint64 counter(void) const;
bool isHotp(void) const;
bool isTotp(void) const;
Q_INVOKABLE qint64 millisecondsLeftForToken(void) const;
Q_SIGNALS:
void never(void);
void tokenChanged(void);
void remove(void);
void recompute(void);
void advanceCounter(quint64 by = 1ULL);
void setCounter(quint64 value);
private:
accounts::Account * const m_model;
};
class SimpleAccountListModel: public QAbstractListModel
{
Q_OBJECT
public:
enum NonStandardRoles {
AccountRole = Qt::ItemDataRole::UserRole
};
Q_ENUM(NonStandardRoles)
public:
explicit SimpleAccountListModel(accounts::AccountStorage *storage, QObject *parent = nullptr);
Q_INVOKABLE void addTotp(const QString &account, const QString &secret, uint timeStep, int tokenLength);
Q_INVOKABLE void addHotp(const QString &account, const QString &secret, quint64 counter, int tokenLength);
Q_INVOKABLE int rowCount(const QModelIndex &parent = QModelIndex()) const override;
Q_INVOKABLE QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames(void) const override;
private Q_SLOTS:
void added(const QString &account);
void removed(const QString &removed);
private:
accounts::AccountStorage * const m_storage;
private:
QVector<QString> m_index;
QHash<QString, accounts::Account*> m_accounts;
};
}
Q_DECLARE_METATYPE(model::AccountView *);
#endif