Use the new Account models.
Drop the AccountDetailsPage instead of trying to update it: see issue #7 With this change issue #2 should be fixedmaster
parent
1f15fb6e08
commit
a0caf83da2
|
@ -4,9 +4,3 @@ add_subdirectory(base32)
|
|||
add_subdirectory(account)
|
||||
add_subdirectory(model)
|
||||
add_subdirectory(validators)
|
||||
|
||||
set(Account_UUT_SRCS ../src/account.cpp)
|
||||
set(Test_DEP_LIBS Qt5::Core Qt5::Test ${LIBOATH_LIBRARIES} base32_lib)
|
||||
|
||||
ecm_add_test(hotp-generator-samples.cpp ${Account_UUT_SRCS} LINK_LIBRARIES ${Test_DEP_LIBS} TEST_NAME hotp-generator-samples)
|
||||
ecm_add_test(totp-generator-samples.cpp ${Account_UUT_SRCS} LINK_LIBRARIES ${Test_DEP_LIBS} TEST_NAME totp-generator-samples)
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
/*****************************************************************************
|
||||
* Copyright: 2019 Johan Ouwerkerk <jm.ouwerkerk@gmail.com> *
|
||||
* *
|
||||
* This project is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This project is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
****************************************************************************/
|
||||
|
||||
#include "account.h"
|
||||
|
||||
#include <QTest>
|
||||
#include <QtDebug>
|
||||
|
||||
class HOTPGeneratorSamplesTest: public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
private Q_SLOTS:
|
||||
void testDefaults(void);
|
||||
void testDefaults_data(void);
|
||||
};
|
||||
|
||||
void HOTPGeneratorSamplesTest::testDefaults(void)
|
||||
{
|
||||
/*
|
||||
* RFC test vector uses the key: 12345678901234567890
|
||||
* The secret value below is the bas32 encoded version of that
|
||||
*/
|
||||
static QLatin1String secret("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ");
|
||||
|
||||
// doesn't really matter, just some random UUID
|
||||
static QUuid uuid("5df6378b-92b2-45c8-88bd-c5a178f7b538");
|
||||
|
||||
Account a(uuid);
|
||||
a.setName(QLatin1String("RFC test vector sample"));
|
||||
a.setType(Account::TypeHOTP);
|
||||
a.setPinLength(6);
|
||||
a.setSecret(secret);
|
||||
|
||||
QFETCH(quint64, counter);
|
||||
|
||||
a.setCounter(counter);
|
||||
a.generate();
|
||||
|
||||
QTEST(a.otp(), "rfc-test-vector");
|
||||
}
|
||||
|
||||
static void define_test_case(int k, const char *expected)
|
||||
{
|
||||
|
||||
QByteArray output(expected, 6);
|
||||
|
||||
QTest::newRow(qPrintable(QStringLiteral("RFC 4226 test vector, counter value = %1").arg(k))) << (quint64) k << QString::fromLocal8Bit(output);
|
||||
}
|
||||
|
||||
void HOTPGeneratorSamplesTest::testDefaults_data(void)
|
||||
{
|
||||
static const char * corpus[10] {
|
||||
"755224",
|
||||
"287082",
|
||||
"359152",
|
||||
"969429",
|
||||
"338314",
|
||||
"254676",
|
||||
"287922",
|
||||
"162583",
|
||||
"399871",
|
||||
"520489"
|
||||
};
|
||||
|
||||
QTest::addColumn<quint64>("counter");
|
||||
QTest::addColumn<QString>("rfc-test-vector");
|
||||
|
||||
for(int k = 0; k < 10; ++k) {
|
||||
define_test_case(k, corpus[k]);
|
||||
}
|
||||
}
|
||||
|
||||
QTEST_APPLESS_MAIN(HOTPGeneratorSamplesTest)
|
||||
|
||||
#include "hotp-generator-samples.moc"
|
|
@ -1,98 +0,0 @@
|
|||
/*****************************************************************************
|
||||
* Copyright: 2019 Johan Ouwerkerk <jm.ouwerkerk@gmail.com> *
|
||||
* *
|
||||
* This project is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This project is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
****************************************************************************/
|
||||
|
||||
#include "account.h"
|
||||
|
||||
#include <QTest>
|
||||
#include <QtDebug>
|
||||
|
||||
class TOTPGeneratorSamplesTest: public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
private Q_SLOTS:
|
||||
void testDefaults(void);
|
||||
void testDefaults_data(void);
|
||||
};
|
||||
|
||||
void TOTPGeneratorSamplesTest::testDefaults(void)
|
||||
{
|
||||
/*
|
||||
* RFC test vector uses the key: 12345678901234567890
|
||||
* The secret value below is the bas32 encoded version of that
|
||||
*/
|
||||
static QLatin1String secret("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ");
|
||||
|
||||
// the default TOTP timestep is 30s, ie. 30000ms
|
||||
static qint64 DEFAULT_TIMESTEP = 30000;
|
||||
|
||||
// doesn't really matter, just some random UUID
|
||||
static QUuid uuid("5df6378b-92b2-45c8-88bd-c5a178f7b538");
|
||||
|
||||
QFETCH(qint64, counter);
|
||||
|
||||
std::function<qint64(void)> clock([counter](void) -> qint64 {
|
||||
return counter * DEFAULT_TIMESTEP;
|
||||
});
|
||||
|
||||
Account a(uuid, clock);
|
||||
a.setName(QLatin1String("RFC test vector sample"));
|
||||
a.setType(Account::TypeTOTP);
|
||||
a.setPinLength(6);
|
||||
a.setSecret(secret);
|
||||
a.setTimeStep(30);
|
||||
|
||||
a.setCounter(counter);
|
||||
a.generate();
|
||||
|
||||
QTEST(a.otp(), "rfc-test-vector");
|
||||
}
|
||||
|
||||
static void define_test_case(int k, const char *expected)
|
||||
{
|
||||
|
||||
QByteArray output(expected, 6);
|
||||
|
||||
QTest::newRow(qPrintable(QStringLiteral("RFC 4226 test vector, # time steps = %1").arg(k))) << (qint64) k << QString::fromLocal8Bit(output);
|
||||
}
|
||||
|
||||
void TOTPGeneratorSamplesTest::testDefaults_data(void)
|
||||
{
|
||||
static const char * corpus[10] {
|
||||
"755224",
|
||||
"287082",
|
||||
"359152",
|
||||
"969429",
|
||||
"338314",
|
||||
"254676",
|
||||
"287922",
|
||||
"162583",
|
||||
"399871",
|
||||
"520489"
|
||||
};
|
||||
|
||||
QTest::addColumn<qint64>("counter");
|
||||
QTest::addColumn<QString>("rfc-test-vector");
|
||||
|
||||
for(int k = 0; k < 10; ++k) {
|
||||
define_test_case(k, corpus[k]);
|
||||
}
|
||||
}
|
||||
|
||||
QTEST_APPLESS_MAIN(TOTPGeneratorSamplesTest)
|
||||
|
||||
#include "totp-generator-samples.moc"
|
|
@ -6,11 +6,9 @@ add_subdirectory(app)
|
|||
|
||||
set(keysmith_SRCS
|
||||
main.cpp
|
||||
accountmodel.cpp
|
||||
account.cpp
|
||||
)
|
||||
|
||||
set(keysmith_internal_libs base32_lib validator_lib)
|
||||
set(keysmith_internal_libs base32_lib validator_lib account_lib model_lib keysmith_lib)
|
||||
|
||||
qt5_add_resources(RESOURCES resources.qrc)
|
||||
add_executable(keysmith ${keysmith_SRCS} ${RESOURCES})
|
||||
|
|
209
src/account.cpp
209
src/account.cpp
|
@ -1,209 +0,0 @@
|
|||
/*****************************************************************************
|
||||
* Copyright: 2013 Michael Zanetti <michael_zanetti@gmx.net> *
|
||||
* *
|
||||
* This project is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This project is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
****************************************************************************/
|
||||
|
||||
#include "account.h"
|
||||
|
||||
#include "base32/base32.h"
|
||||
#include "oath_p.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QtDebug>
|
||||
|
||||
Account::Account(const QUuid &id, QObject *parent) : Account(id, &QDateTime::currentMSecsSinceEpoch, parent) {}
|
||||
|
||||
Account::Account(const QUuid &id, const std::function<qint64(void)>& clock, QObject *parent) :
|
||||
QObject(parent),
|
||||
m_id(id),
|
||||
/*
|
||||
* Make sure to initialise each member beforehand to some default
|
||||
* This is needed because the setters for various properties trigger a re-computation of the OTP token,
|
||||
* and that computation depends itself on the values of these fields.
|
||||
* I.e. without this the code would branch on uninitialised memory.
|
||||
*/
|
||||
m_type(Account::TypeTOTP),
|
||||
m_counter(0),
|
||||
m_timeStep(30),
|
||||
m_pinLength(6),
|
||||
m_clock(clock)
|
||||
{
|
||||
m_totpTimer.setSingleShot(true);
|
||||
connect(&m_totpTimer, &QTimer::timeout, this, &Account::generate);
|
||||
}
|
||||
|
||||
QUuid Account::id() const
|
||||
{
|
||||
return m_id;
|
||||
}
|
||||
|
||||
QString Account::name() const
|
||||
{
|
||||
return m_name;
|
||||
}
|
||||
|
||||
void Account::setName(const QString &name)
|
||||
{
|
||||
if (m_name != name) {
|
||||
m_name = name;
|
||||
Q_EMIT nameChanged();
|
||||
}
|
||||
}
|
||||
|
||||
Account::Type Account::type() const
|
||||
{
|
||||
return m_type;
|
||||
}
|
||||
|
||||
void Account::setType(Account::Type type)
|
||||
{
|
||||
if (m_type != type) {
|
||||
m_type = type;
|
||||
// qDebug() << "setting type" << type;
|
||||
Q_EMIT typeChanged();
|
||||
generate();
|
||||
}
|
||||
}
|
||||
|
||||
QString Account::secret() const
|
||||
{
|
||||
return m_secret;
|
||||
}
|
||||
|
||||
void Account::setSecret(const QString &secret)
|
||||
{
|
||||
if (m_secret != secret) {
|
||||
m_secret = secret;
|
||||
Q_EMIT secretChanged();
|
||||
generate();
|
||||
}
|
||||
}
|
||||
|
||||
quint64 Account::counter() const
|
||||
{
|
||||
return m_counter;
|
||||
}
|
||||
|
||||
void Account::setCounter(quint64 counter)
|
||||
{
|
||||
if (m_counter != counter) {
|
||||
m_counter = counter;
|
||||
Q_EMIT counterChanged();
|
||||
generate();
|
||||
}
|
||||
}
|
||||
|
||||
int Account::timeStep() const
|
||||
{
|
||||
return m_timeStep;
|
||||
}
|
||||
|
||||
void Account::setTimeStep(int timeStep)
|
||||
{
|
||||
if (m_timeStep != timeStep) {
|
||||
m_timeStep = timeStep;
|
||||
Q_EMIT timeStepChanged();
|
||||
generate();
|
||||
}
|
||||
}
|
||||
|
||||
int Account::pinLength() const
|
||||
{
|
||||
return m_pinLength;
|
||||
}
|
||||
|
||||
void Account::setPinLength(int pinLength)
|
||||
{
|
||||
if (m_pinLength != pinLength) {
|
||||
m_pinLength = pinLength;
|
||||
Q_EMIT pinLengthChanged();
|
||||
generate();
|
||||
}
|
||||
}
|
||||
|
||||
QString Account::otp() const
|
||||
{
|
||||
return m_otp;
|
||||
}
|
||||
|
||||
qint64 Account::msecsToNext() const
|
||||
{
|
||||
if (m_timeStep <= 0) {
|
||||
return 0;
|
||||
}
|
||||
qint64 now = m_clock();
|
||||
qint64 msecsSinceLast = now % (m_timeStep * 1000);
|
||||
qint64 msecsToNext = (m_timeStep * 1000) - msecsSinceLast;
|
||||
return msecsToNext;
|
||||
}
|
||||
|
||||
void Account::next()
|
||||
{
|
||||
m_counter++;
|
||||
// qDebug() << "emitting changed";
|
||||
Q_EMIT counterChanged();
|
||||
generate();
|
||||
}
|
||||
|
||||
void Account::generate()
|
||||
{
|
||||
if (m_secret.isEmpty()) {
|
||||
// qWarning() << "No secret set. Cannot generate otp.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_pinLength <= 0) {
|
||||
// qWarning() << "Pin length is" << m_pinLength << ". Cannot generate otp.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_type == TypeTOTP && m_timeStep <= 0) {
|
||||
// qWarning() << "Time step is 0. Cannot generate totp";
|
||||
return;
|
||||
}
|
||||
|
||||
// qDebug() << "generating for account" << m_name;
|
||||
std::optional<QByteArray> secret = base32::decode(m_secret);
|
||||
|
||||
if(!secret.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// qDebug() << "hexSecret" << hexSecret;
|
||||
char code[m_pinLength];
|
||||
if (m_type == TypeHOTP) {
|
||||
oath_hotp_generate(secret->data(), secret->length(), m_counter, m_pinLength, false, OATH_HOTP_DYNAMIC_TRUNCATION, code);
|
||||
} else {
|
||||
QDateTime now = QDateTime::fromMSecsSinceEpoch(m_clock());
|
||||
oath_totp_generate(secret->data(), secret->length(), now.toTime_t(), m_timeStep, 0, m_pinLength, code);
|
||||
}
|
||||
|
||||
m_otp = QLatin1String(code);
|
||||
// qDebug() << "Generating secret" << m_name << m_secret << m_counter << m_pinLength << m_otp << m_timeStep;
|
||||
Q_EMIT otpChanged();
|
||||
|
||||
if (m_type == TypeTOTP) {
|
||||
|
||||
// QTimer tends to be a wee bit too early...
|
||||
// let's just add half a sec to make sure we end up in
|
||||
// the current time slot and avoid restarting timers in the ui
|
||||
m_totpTimer.setInterval(msecsToNext() + 500);
|
||||
// qDebug() << "restarting timer for" << m_name << m_totpTimer.interval() << msecsToNext << QDateTime::currentDateTime().toMSecsSinceEpoch();
|
||||
m_totpTimer.start();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
/*****************************************************************************
|
||||
* Copyright: 2013 Michael Zanetti <michael_zanetti@gmx.net> *
|
||||
* *
|
||||
* This project is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This project is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef ACCOUNT_H
|
||||
#define ACCOUNT_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QUuid>
|
||||
#include <QTimer>
|
||||
|
||||
#include <functional>
|
||||
|
||||
class Account : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
|
||||
Q_PROPERTY(Type type READ type WRITE setType NOTIFY typeChanged)
|
||||
Q_PROPERTY(QString secret READ secret WRITE setSecret NOTIFY secretChanged)
|
||||
Q_PROPERTY(quint64 counter READ counter WRITE setCounter NOTIFY counterChanged)
|
||||
Q_PROPERTY(int timeStep READ timeStep WRITE setTimeStep NOTIFY timeStepChanged)
|
||||
Q_PROPERTY(int pinLength READ pinLength WRITE setPinLength NOTIFY pinLengthChanged)
|
||||
Q_PROPERTY(QString otp READ otp NOTIFY otpChanged)
|
||||
public:
|
||||
enum Type {
|
||||
TypeHOTP,
|
||||
TypeTOTP
|
||||
};
|
||||
Q_ENUM(Type)
|
||||
|
||||
explicit Account(const QUuid &id, QObject *parent = 0);
|
||||
explicit Account(const QUuid &id, const std::function<qint64(void)>& clock, QObject *parent = 0);
|
||||
|
||||
QUuid id() const;
|
||||
|
||||
QString name() const;
|
||||
void setName(const QString &name);
|
||||
|
||||
Type type() const;
|
||||
void setType(Type type);
|
||||
|
||||
QString secret() const;
|
||||
void setSecret(const QString &secret);
|
||||
|
||||
quint64 counter() const;
|
||||
void setCounter(quint64 counter);
|
||||
|
||||
int timeStep() const;
|
||||
void setTimeStep(int timeStep);
|
||||
|
||||
int pinLength() const;
|
||||
void setPinLength(int pinLength);
|
||||
|
||||
QString otp() const;
|
||||
|
||||
Q_INVOKABLE qint64 msecsToNext() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void nameChanged();
|
||||
void typeChanged();
|
||||
void secretChanged();
|
||||
void counterChanged();
|
||||
void timeStepChanged();
|
||||
void pinLengthChanged();
|
||||
void otpChanged();
|
||||
|
||||
public Q_SLOTS:
|
||||
void generate();
|
||||
void next();
|
||||
|
||||
private:
|
||||
QUuid m_id;
|
||||
QString m_name;
|
||||
Type m_type;
|
||||
QString m_secret;
|
||||
quint64 m_counter;
|
||||
int m_timeStep;
|
||||
int m_pinLength;
|
||||
QString m_otp;
|
||||
QTimer m_totpTimer;
|
||||
const std::function<qint64(void)> m_clock;
|
||||
};
|
||||
|
||||
#endif // ACCOUNT_H
|
|
@ -1,186 +0,0 @@
|
|||
/*****************************************************************************
|
||||
* Copyright: 2013 Michael Zanetti <michael_zanetti@gmx.net> *
|
||||
* *
|
||||
* This project is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This project is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
****************************************************************************/
|
||||
|
||||
#include "accountmodel.h"
|
||||
|
||||
#include "account.h"
|
||||
|
||||
#include <QSettings>
|
||||
#include <QStringList>
|
||||
//#include <QDebug>
|
||||
|
||||
AccountModel::AccountModel(QObject *parent) :
|
||||
QAbstractListModel(parent)
|
||||
{
|
||||
QSettings settings("org.kde.keysmith", "Keysmith");
|
||||
const QStringList entries = settings.childGroups();
|
||||
// qDebug() << "loading settings file:" << settings.fileName();
|
||||
for(const QString &group : entries) {
|
||||
// qDebug() << "found group" << group << QUuid(group).toString();
|
||||
|
||||
QUuid id(group);
|
||||
|
||||
settings.beginGroup(group);
|
||||
Account *account = new Account(id, this);
|
||||
account->setName(settings.value("account").toString());
|
||||
account->setType(settings.value("type", "hotp").toString() == "totp" ? Account::TypeTOTP : Account::TypeHOTP);
|
||||
account->setSecret(settings.value("secret").toString());
|
||||
account->setCounter(settings.value("counter").toInt());
|
||||
account->setTimeStep(settings.value("timeStep").toInt());
|
||||
account->setPinLength(settings.value("pinLength").toInt());
|
||||
|
||||
m_accounts.append(account);
|
||||
wireAccount(account);
|
||||
settings.endGroup();
|
||||
}
|
||||
}
|
||||
|
||||
int AccountModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return m_accounts.count();
|
||||
}
|
||||
|
||||
void AccountModel::wireAccount(const Account *account)
|
||||
{
|
||||
const auto h = &AccountModel::accountChanged;
|
||||
QObject::connect(account, &Account::nameChanged, this, h);
|
||||
QObject::connect(account, &Account::typeChanged, this, h);
|
||||
QObject::connect(account, &Account::secretChanged, this, h);
|
||||
QObject::connect(account, &Account::counterChanged, this, h);
|
||||
QObject::connect(account, &Account::pinLengthChanged, this, h);
|
||||
QObject::connect(account, &Account::otpChanged, this, h);
|
||||
}
|
||||
|
||||
QVariant AccountModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
switch (role) {
|
||||
case RoleName:
|
||||
return m_accounts.at(index.row())->name();
|
||||
case RoleType:
|
||||
return m_accounts.at(index.row())->type();
|
||||
case RoleSecret:
|
||||
return m_accounts.at(index.row())->secret();
|
||||
case RoleCounter:
|
||||
return m_accounts.at(index.row())->counter();
|
||||
case RoleTimeStep:
|
||||
return m_accounts.at(index.row())->timeStep();
|
||||
case RolePinLength:
|
||||
return m_accounts.at(index.row())->pinLength();
|
||||
case RoleOtp:
|
||||
return m_accounts.at(index.row())->otp();
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
Account *AccountModel::get(int index) const
|
||||
{
|
||||
if (index > -1 && m_accounts.count() > index) {
|
||||
return m_accounts.at(index);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Account *AccountModel::createAccount()
|
||||
{
|
||||
Account *account = new Account(QUuid::createUuid(), this);
|
||||
beginInsertRows(QModelIndex(), m_accounts.count(), m_accounts.count());
|
||||
m_accounts.append(account);
|
||||
|
||||
wireAccount(account);
|
||||
storeAccount(account);
|
||||
|
||||
endInsertRows();
|
||||
return account;
|
||||
}
|
||||
|
||||
void AccountModel::deleteAccount(int index)
|
||||
{
|
||||
// qDebug() << "starting deleteAccount" << index << m_accounts.count();
|
||||
beginRemoveRows(QModelIndex(), index, index);
|
||||
|
||||
Account *account = m_accounts.takeAt(index);
|
||||
// qDebug() << "got account" << account;
|
||||
QSettings settings("org.kde.keysmith", "Keysmith");
|
||||
settings.beginGroup(account->id().toString());
|
||||
settings.remove("");
|
||||
settings.endGroup();
|
||||
|
||||
// qDebug() << "removed from settings";
|
||||
account->deleteLater();
|
||||
|
||||
endRemoveRows();
|
||||
// qDebug() << "done with deleteAccount";
|
||||
}
|
||||
|
||||
void AccountModel::deleteAccount(Account *account)
|
||||
{
|
||||
int index = m_accounts.indexOf(account);
|
||||
deleteAccount(index);
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> AccountModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles.insert(RoleName, "name");
|
||||
roles.insert(RoleType, "type");
|
||||
roles.insert(RoleSecret, "secret");
|
||||
roles.insert(RoleCounter, "counter");
|
||||
roles.insert(RoleTimeStep, "timeStep");
|
||||
roles.insert(RolePinLength, "pinLength");
|
||||
roles.insert(RoleOtp, "otp");
|
||||
return roles;
|
||||
}
|
||||
|
||||
void AccountModel::generateNext(int account)
|
||||
{
|
||||
m_accounts.at(account)->next();
|
||||
Q_EMIT dataChanged(index(account), index(account), QVector<int>() << RoleCounter << RoleOtp);
|
||||
}
|
||||
|
||||
void AccountModel::refresh()
|
||||
{
|
||||
Q_EMIT beginResetModel();
|
||||
Q_EMIT endResetModel();
|
||||
}
|
||||
|
||||
void AccountModel::accountChanged()
|
||||
{
|
||||
Account *account = qobject_cast<Account*>(sender());
|
||||
storeAccount(account);
|
||||
|
||||
// qDebug() << "account changed";
|
||||
int accountIndex = m_accounts.indexOf(account);
|
||||
Q_EMIT dataChanged(index(accountIndex), index(accountIndex));
|
||||
}
|
||||
|
||||
void AccountModel::storeAccount(const Account *account)
|
||||
{
|
||||
QSettings settings("org.kde.keysmith", "Keysmith");
|
||||
settings.beginGroup(account->id().toString());
|
||||
settings.setValue("account", account->name());
|
||||
settings.setValue("type", account->type() == Account::TypeTOTP ? "totp" : "hotp");
|
||||
settings.setValue("secret", account->secret());
|
||||
settings.setValue("counter", account->counter());
|
||||
settings.setValue("timeStep", account->timeStep());
|
||||
settings.setValue("pinLength", account->pinLength());
|
||||
settings.endGroup();
|
||||
// qDebug() << "saved to" << settings.fileName();
|
||||
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
/*****************************************************************************
|
||||
* Copyright: 2013 Michael Zanetti <michael_zanetti@gmx.net> *
|
||||
* *
|
||||
* This project is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This project is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef ACCOUNTMODEL_H
|
||||
#define ACCOUNTMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
class Account;
|
||||
|
||||
class AccountModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Roles {
|
||||
RoleName,
|
||||
RoleType,
|
||||
RoleSecret,
|
||||
RoleCounter,
|
||||
RoleTimeStep,
|
||||
RolePinLength,
|
||||
RoleOtp
|
||||
};
|
||||
|
||||
explicit AccountModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_INVOKABLE Account *get(int index) const;
|
||||
Q_INVOKABLE Account *createAccount();
|
||||
Q_INVOKABLE void deleteAccount(int index);
|
||||
Q_INVOKABLE void deleteAccount(Account *account);
|
||||
|
||||
public Q_SLOTS:
|
||||
void generateNext(int account);
|
||||
void refresh();
|
||||
|
||||
private Q_SLOTS:
|
||||
void accountChanged();
|
||||
void storeAccount(const Account *account);
|
||||
|
||||
private:
|
||||
void wireAccount(const Account *account);
|
||||
|
||||
private:
|
||||
QList<Account*> m_accounts;
|
||||
};
|
||||
|
||||
#endif // ACCOUNTMODEL_H
|
|
@ -1,86 +0,0 @@
|
|||
/*
|
||||
* Copyright 2019 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 3 of
|
||||
* the License or any later version accepted by the membership of
|
||||
* KDE e.V. (or its successor approved by the membership of KDE
|
||||
* e.V.), which shall act as a proxy defined in Section 14 of
|
||||
* version 3 of the license.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import Oath 1.0
|
||||
import Oath.Validators 1.0 as Validators
|
||||
import QtQuick 2.1
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Controls 2.0 as Controls
|
||||
import org.kde.kirigami 2.4 as Kirigami
|
||||
|
||||
Kirigami.Page {
|
||||
id: root
|
||||
|
||||
property Account account
|
||||
property int accountIndex;
|
||||
|
||||
signal accountUpdate(Account account, int index)
|
||||
signal tokenRefresh(Account account, int index)
|
||||
|
||||
property bool editMode: false
|
||||
property bool hideSensitive: true
|
||||
|
||||
Kirigami.Action {
|
||||
id: leftAction
|
||||
text: root.hideSensitive ? i18n("Show") : i18n("Hide")
|
||||
iconName: root.hideSensitive ? "view-visible" : "view-hidden"
|
||||
onTriggered: {
|
||||
root.hideSensitive = !root.hideSensitive;
|
||||
root.editMode = false;
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
id: rightAction
|
||||
text: i18nc("@action:button", "Generate Token")
|
||||
iconName: "view-refresh"
|
||||
onTriggered: {
|
||||
root.tokenRefresh(account, accountIndex)
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Action {
|
||||
id: mainAction
|
||||
text: root.editMode ? i18n("Apply") : i18n("Edit")
|
||||
iconName: root.editMode ? "document-save" : "document-edit"
|
||||
onTriggered: {
|
||||
var fromEditor = root.editMode;
|
||||
root.editMode = !fromEditor;
|
||||
if (fromEditor) {
|
||||
accountUpdate(root.account, root.accountIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actions.main: mainAction
|
||||
actions.left: editMode ? null : leftAction
|
||||
actions.right: editMode ? null : rightAction
|
||||
title: account ? account.name : i18nc("@title:window", "Account Details")
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
TokenDetailsForm {
|
||||
id: tokenDetails
|
||||
account: root.account
|
||||
editable: editMode
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,7 +19,6 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import Oath 1.0
|
||||
import Oath.Validators 1.0 as Validators
|
||||
import QtQuick 2.1
|
||||
import QtQuick.Layouts 1.2
|
||||
|
@ -28,33 +27,31 @@ import org.kde.kirigami 2.4 as Kirigami
|
|||
|
||||
Kirigami.FormLayout {
|
||||
id: root
|
||||
property int type: totpRadio.checked ? Account.TypeTOTP : Account.TypeHOTP
|
||||
property bool isTotp: totpRadio.checked && !hotpRadio.checked
|
||||
property bool isHotp: hotpRadio.checked && !totpRadio.checked
|
||||
property int tokenLength: pinLengthField.value
|
||||
property string timeStep: timerField.text
|
||||
property string secret: accountSecret.text
|
||||
property string counter: counterField.text
|
||||
|
||||
property Account account: null
|
||||
property bool editable: false
|
||||
|
||||
ColumnLayout {
|
||||
Layout.rowSpan: 2
|
||||
Kirigami.FormData.label: i18nc("@label:chooser", "Account Type:")
|
||||
Kirigami.FormData.buddyFor: totpRadio
|
||||
Controls.RadioButton {
|
||||
id: totpRadio
|
||||
checked: !account || account.type == Account.TypeTOTP
|
||||
checked: true
|
||||
text: i18nc("@option:radio", "Time-based OTP")
|
||||
}
|
||||
Controls.RadioButton {
|
||||
id: hotpRadio
|
||||
checked: account && account.type == Account.TypeHOTP
|
||||
checked: false
|
||||
text: i18nc("@option:radio", "Hash-based OTP")
|
||||
}
|
||||
}
|
||||
Controls.TextField {
|
||||
id: accountSecret
|
||||
text: account ? account.secret : ""
|
||||
text: ""
|
||||
Kirigami.FormData.label: i18nc("@label:textbox", "Secret key:")
|
||||
validator: Validators.Base32SecretValidator {
|
||||
id: secretValidator
|
||||
|
@ -65,13 +62,13 @@ Kirigami.FormLayout {
|
|||
id: timerField
|
||||
Kirigami.FormData.label: i18nc("@label:textbox", "Timer:")
|
||||
enabled: totpRadio.checked
|
||||
text: account ? "" + account.timeStep : "30"
|
||||
text: "30"
|
||||
inputMask: "0009"
|
||||
inputMethodHints: Qt.ImhDigitsOnly
|
||||
}
|
||||
Controls.TextField {
|
||||
id: counterField
|
||||
text: account ? "" + account.counter : ""
|
||||
text: "0"
|
||||
Kirigami.FormData.label: i18nc("@label:textbox", "Counter:")
|
||||
enabled: hotpRadio.checked
|
||||
validator: Validators.HOTPCounterValidator {
|
||||
|
@ -90,6 +87,6 @@ Kirigami.FormLayout {
|
|||
Kirigami.FormData.label: i18nc("@label:spinbox", "Token length:")
|
||||
from: 6
|
||||
to: 8
|
||||
value: account ? account.pinLength : 6
|
||||
value: 6
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,8 +19,10 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import Oath 1.0
|
||||
import Keysmith.Application 1.0
|
||||
import Keysmith.Models 1.0 as Models
|
||||
import Oath.Validators 1.0 as Validators
|
||||
|
||||
import QtQuick 2.1
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Controls 2.0 as Controls
|
||||
|
@ -29,13 +31,11 @@ import org.kde.kirigami 2.4 as Kirigami
|
|||
Kirigami.ApplicationWindow {
|
||||
id: root
|
||||
|
||||
pageStack.initialPage: accounts.rowCount() > 0 ? mainPageComponent : addPageComponent
|
||||
pageStack.initialPage: mainPageComponent
|
||||
|
||||
property bool addActionEnabled: true
|
||||
|
||||
AccountModel {
|
||||
id: accounts
|
||||
}
|
||||
property Models.AccountListModel accounts: Keysmith.accountListModel()
|
||||
|
||||
Kirigami.Action {
|
||||
id: addAction
|
||||
|
@ -53,29 +53,10 @@ Kirigami.ApplicationWindow {
|
|||
Kirigami.ScrollablePage {
|
||||
title: i18n("OTP")
|
||||
actions.main: addAction
|
||||
Controls.Label {
|
||||
text: i18nc("Text shown when no accounts are added", "No account set up. Use the Add button to add accounts.")
|
||||
visible: view.count == 0
|
||||
}
|
||||
Kirigami.CardsListView {
|
||||
id: view
|
||||
model: accounts
|
||||
delegate: Kirigami.AbstractCard {
|
||||
onClicked: {
|
||||
/*
|
||||
* `model` is some kind of wrapper item that exposes
|
||||
* bound properties but is not a *real* account.
|
||||
*
|
||||
* Retrieve the actual underlying account by its index
|
||||
*/
|
||||
var actualAccount = accounts.get(index);
|
||||
pageStack.push(accountDetailsPageComponent, {
|
||||
account: actualAccount,
|
||||
accountIndex: index,
|
||||
editMode: false,
|
||||
hideSensitive: true
|
||||
});
|
||||
}
|
||||
contentItem: Item {
|
||||
implicitWidth: delegateLayout.implicitWidth
|
||||
implicitHeight: delegateLayout.implicitHeight
|
||||
|
@ -93,32 +74,39 @@ Kirigami.ApplicationWindow {
|
|||
ColumnLayout {
|
||||
Controls.Label {
|
||||
Layout.fillWidth: true
|
||||
text: model.name
|
||||
text: model.account ? model.account.name : i18nc("placeholder text if no account name is available", "(untitled)")
|
||||
}
|
||||
Kirigami.Heading {
|
||||
level: 2
|
||||
text: model.otp
|
||||
onTextChanged: {
|
||||
if(model.type === Account.TypeTOTP) {
|
||||
timeoutTimer.restart();
|
||||
}
|
||||
}
|
||||
text: model.account && model.account.token && model.account.token.length > 0 ? model.account.token : i18nc("placeholder text if no token is available", "(refresh)")
|
||||
}
|
||||
}
|
||||
Controls.Button {
|
||||
Layout.alignment: Qt.AlignRight|Qt.AlignVCenter
|
||||
Layout.columnSpan: 2
|
||||
text: i18nc("%1 is current counter numerical value", "Refresh (%1)", model.counter)
|
||||
visible: model.type === Account.TypeHOTP
|
||||
visible: model.account && model.account.isHotp
|
||||
onClicked: {
|
||||
accounts.generateNext(index);
|
||||
if(model.account) {
|
||||
model.account.advanceCounter();
|
||||
}
|
||||
}
|
||||
}
|
||||
Timer {
|
||||
id: timeoutTimer
|
||||
repeat: true
|
||||
interval: model.timeStep * 1000
|
||||
running: model.type === Account.TypeTOTP
|
||||
repeat: false
|
||||
interval: model.account && model.account.isTotp ? model.account.millisecondsLeftForToken() : 0
|
||||
running: model.account && model.account.isTotp
|
||||
onTriggered: {
|
||||
if (model.account) {
|
||||
model.account.recompute();
|
||||
timeoutTimer.stop();
|
||||
timeoutIndicatorAnimation.stop();
|
||||
timeoutTimer.interval = model.account.millisecondsLeftForToken();
|
||||
timeoutTimer.restart();
|
||||
timeoutIndicatorAnimation.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
id: timeoutIndicatorRect
|
||||
|
@ -141,7 +129,7 @@ Kirigami.ApplicationWindow {
|
|||
from: delegateLayout.height
|
||||
to: 0
|
||||
duration: timeoutTimer.interval
|
||||
running: model.type === Account.TypeTOTP && units.longDuration > 1
|
||||
running: model.account && model.account.isTotp && units.longDuration > 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -157,47 +145,15 @@ Kirigami.ApplicationWindow {
|
|||
text: i18n("Add")
|
||||
iconName: "answer-correct"
|
||||
onTriggered: {
|
||||
/*
|
||||
* Nota Bene: order is significant.
|
||||
* Accounts are being appended in order of creation,
|
||||
* meaning the account index for the newly created
|
||||
* account is equal to the size of the list as it was
|
||||
* before createAccount() (which will add the new entry).
|
||||
*/
|
||||
var newAccountIndex = accounts.rowCount();
|
||||
var newAccount = accounts.createAccount();
|
||||
|
||||
newAccount.name = accountName.text;
|
||||
newAccount.type = tokenDetails.type
|
||||
newAccount.secret = tokenDetails.secret
|
||||
newAccount.counter = parseInt(tokenDetails.counter)
|
||||
newAccount.timeStep = parseInt(tokenDetails.timeStep)
|
||||
newAccount.pinLength = parseInt(tokenDetails.tokenLength)
|
||||
if (tokenDetails.isTotp) {
|
||||
accounts.addTotp(accountName.text, tokenDetails.secret, parseInt(tokenDetails.timeStep), tokenDetails.tokenLength);
|
||||
}
|
||||
if (tokenDetails.isHotp) {
|
||||
accounts.addHotp(accountName.text, tokenDetails.secret, parseInt(tokenDetails.counter), tokenDetails.tokenLength);
|
||||
}
|
||||
|
||||
pageStack.pop();
|
||||
addActionEnabled = true;
|
||||
/*
|
||||
* Check if the pageStack is now 'empty', which will be the case if
|
||||
* the starting page was this addPageComponent.
|
||||
*
|
||||
* According to Qt docs the StackView 'empty' property is supposed to exist
|
||||
* and be a bool but in practice it does not appear to work (it is undefined).
|
||||
* Therefore check the StackView.depth instead.
|
||||
*/
|
||||
if (pageStack.depth < 1) {
|
||||
pageStack.push(mainPageComponent);
|
||||
}
|
||||
|
||||
/*
|
||||
* Auto navigate to the details page for the newly
|
||||
* created account
|
||||
*/
|
||||
pageStack.push(accountDetailsPageComponent, {
|
||||
account: newAccount,
|
||||
accountIndex: newAccountIndex,
|
||||
editMode: false,
|
||||
hideSensitive: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -223,20 +179,4 @@ Kirigami.ApplicationWindow {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: accountDetailsPageComponent
|
||||
AccountDetailsPage {
|
||||
onTokenRefresh: {
|
||||
accounts.generateNext(index);
|
||||
}
|
||||
onAccountUpdate: {
|
||||
/*
|
||||
* This is a NOP for now because account edits are instant
|
||||
* apply, possibly by accident of implementation rather
|
||||
* than by design. I.e. there is nothing to do here, yet.
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
19
src/main.cpp
19
src/main.cpp
|
@ -27,9 +27,9 @@
|
|||
#include <KLocalizedContext>
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "accountmodel.h"
|
||||
#include "account.h"
|
||||
#include "validators/qmlsupport.h"
|
||||
#include "app/keysmith.h"
|
||||
#include "model/accounts.h"
|
||||
#include "../validators/qmlsupport.h"
|
||||
|
||||
Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||
{
|
||||
|
@ -45,11 +45,18 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
|
|||
QQmlApplicationEngine engine;
|
||||
engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
|
||||
|
||||
qmlRegisterType<AccountModel>("Oath", 1, 0, "AccountModel");
|
||||
qmlRegisterUncreatableType<Account>("Oath", 1, 0, "Account", "Use AccountModel::createAccount() to create a new account");
|
||||
validators::registerValidatorTypes();
|
||||
engine.load(QUrl(QStringLiteral("qrc:///main.qml")));
|
||||
qmlRegisterUncreatableType<model::SimpleAccountListModel>("Keysmith.Models", 1, 0, "AccountListModel", "Use the Keysmith singleton to obtain an AccountListModel");
|
||||
qmlRegisterUncreatableType<model::AccountView>("Keysmith.Models", 1, 0, "Account", "Use an AccountListModel from the Keysmith singleton to obtain an Account");
|
||||
qmlRegisterSingletonType<app::Keysmith>("Keysmith.Application", 1, 0, "Keysmith", [](QQmlEngine *qml, QJSEngine *js) -> QObject *
|
||||
{
|
||||
Q_UNUSED(qml);
|
||||
Q_UNUSED(js);
|
||||
|
||||
return new app::Keysmith();
|
||||
});
|
||||
|
||||
engine.load(QUrl(QStringLiteral("qrc:///main.qml")));
|
||||
if (engine.rootObjects().isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,5 @@
|
|||
<qresource prefix="/">
|
||||
<file alias="main.qml">contents/ui/main.qml</file>
|
||||
<file alias="TokenDetailsForm.qml">contents/ui/TokenDetailsForm.qml</file>
|
||||
<file alias="AccountDetailsPage.qml">contents/ui/AccountDetailsPage.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
Loading…
Reference in New Issue