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 #2master
parent
0d5b792637
commit
a813810dfe
|
@ -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)
|
||||
|
|
|
@ -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-)
|
|
@ -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"
|
|
@ -1,5 +1,6 @@
|
|||
add_subdirectory(base32)
|
||||
add_subdirectory(account)
|
||||
add_subdirectory(model)
|
||||
add_subdirectory(validators)
|
||||
|
||||
set(keysmith_SRCS
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
Loading…
Reference in New Issue