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(base32)
|
||||||
add_subdirectory(account)
|
add_subdirectory(account)
|
||||||
|
add_subdirectory(model)
|
||||||
add_subdirectory(validators)
|
add_subdirectory(validators)
|
||||||
|
|
||||||
set(Account_UUT_SRCS ../src/account.cpp)
|
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(base32)
|
||||||
add_subdirectory(account)
|
add_subdirectory(account)
|
||||||
|
add_subdirectory(model)
|
||||||
add_subdirectory(validators)
|
add_subdirectory(validators)
|
||||||
|
|
||||||
set(keysmith_SRCS
|
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