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(account)
|
||||||
add_subdirectory(model)
|
add_subdirectory(model)
|
||||||
add_subdirectory(validators)
|
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
|
set(keysmith_SRCS
|
||||||
main.cpp
|
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)
|
qt5_add_resources(RESOURCES resources.qrc)
|
||||||
add_executable(keysmith ${keysmith_SRCS} ${RESOURCES})
|
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 Oath.Validators 1.0 as Validators
|
||||||
import QtQuick 2.1
|
import QtQuick 2.1
|
||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
|
@ -28,33 +27,31 @@ import org.kde.kirigami 2.4 as Kirigami
|
||||||
|
|
||||||
Kirigami.FormLayout {
|
Kirigami.FormLayout {
|
||||||
id: root
|
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 int tokenLength: pinLengthField.value
|
||||||
property string timeStep: timerField.text
|
property string timeStep: timerField.text
|
||||||
property string secret: accountSecret.text
|
property string secret: accountSecret.text
|
||||||
property string counter: counterField.text
|
property string counter: counterField.text
|
||||||
|
|
||||||
property Account account: null
|
|
||||||
property bool editable: false
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Layout.rowSpan: 2
|
Layout.rowSpan: 2
|
||||||
Kirigami.FormData.label: i18nc("@label:chooser", "Account Type:")
|
Kirigami.FormData.label: i18nc("@label:chooser", "Account Type:")
|
||||||
Kirigami.FormData.buddyFor: totpRadio
|
Kirigami.FormData.buddyFor: totpRadio
|
||||||
Controls.RadioButton {
|
Controls.RadioButton {
|
||||||
id: totpRadio
|
id: totpRadio
|
||||||
checked: !account || account.type == Account.TypeTOTP
|
checked: true
|
||||||
text: i18nc("@option:radio", "Time-based OTP")
|
text: i18nc("@option:radio", "Time-based OTP")
|
||||||
}
|
}
|
||||||
Controls.RadioButton {
|
Controls.RadioButton {
|
||||||
id: hotpRadio
|
id: hotpRadio
|
||||||
checked: account && account.type == Account.TypeHOTP
|
checked: false
|
||||||
text: i18nc("@option:radio", "Hash-based OTP")
|
text: i18nc("@option:radio", "Hash-based OTP")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Controls.TextField {
|
Controls.TextField {
|
||||||
id: accountSecret
|
id: accountSecret
|
||||||
text: account ? account.secret : ""
|
text: ""
|
||||||
Kirigami.FormData.label: i18nc("@label:textbox", "Secret key:")
|
Kirigami.FormData.label: i18nc("@label:textbox", "Secret key:")
|
||||||
validator: Validators.Base32SecretValidator {
|
validator: Validators.Base32SecretValidator {
|
||||||
id: secretValidator
|
id: secretValidator
|
||||||
|
@ -65,13 +62,13 @@ Kirigami.FormLayout {
|
||||||
id: timerField
|
id: timerField
|
||||||
Kirigami.FormData.label: i18nc("@label:textbox", "Timer:")
|
Kirigami.FormData.label: i18nc("@label:textbox", "Timer:")
|
||||||
enabled: totpRadio.checked
|
enabled: totpRadio.checked
|
||||||
text: account ? "" + account.timeStep : "30"
|
text: "30"
|
||||||
inputMask: "0009"
|
inputMask: "0009"
|
||||||
inputMethodHints: Qt.ImhDigitsOnly
|
inputMethodHints: Qt.ImhDigitsOnly
|
||||||
}
|
}
|
||||||
Controls.TextField {
|
Controls.TextField {
|
||||||
id: counterField
|
id: counterField
|
||||||
text: account ? "" + account.counter : ""
|
text: "0"
|
||||||
Kirigami.FormData.label: i18nc("@label:textbox", "Counter:")
|
Kirigami.FormData.label: i18nc("@label:textbox", "Counter:")
|
||||||
enabled: hotpRadio.checked
|
enabled: hotpRadio.checked
|
||||||
validator: Validators.HOTPCounterValidator {
|
validator: Validators.HOTPCounterValidator {
|
||||||
|
@ -90,6 +87,6 @@ Kirigami.FormLayout {
|
||||||
Kirigami.FormData.label: i18nc("@label:spinbox", "Token length:")
|
Kirigami.FormData.label: i18nc("@label:spinbox", "Token length:")
|
||||||
from: 6
|
from: 6
|
||||||
to: 8
|
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 Oath.Validators 1.0 as Validators
|
||||||
|
|
||||||
import QtQuick 2.1
|
import QtQuick 2.1
|
||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
import QtQuick.Controls 2.0 as Controls
|
import QtQuick.Controls 2.0 as Controls
|
||||||
|
@ -29,13 +31,11 @@ import org.kde.kirigami 2.4 as Kirigami
|
||||||
Kirigami.ApplicationWindow {
|
Kirigami.ApplicationWindow {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
pageStack.initialPage: accounts.rowCount() > 0 ? mainPageComponent : addPageComponent
|
pageStack.initialPage: mainPageComponent
|
||||||
|
|
||||||
property bool addActionEnabled: true
|
property bool addActionEnabled: true
|
||||||
|
|
||||||
AccountModel {
|
property Models.AccountListModel accounts: Keysmith.accountListModel()
|
||||||
id: accounts
|
|
||||||
}
|
|
||||||
|
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
id: addAction
|
id: addAction
|
||||||
|
@ -53,29 +53,10 @@ Kirigami.ApplicationWindow {
|
||||||
Kirigami.ScrollablePage {
|
Kirigami.ScrollablePage {
|
||||||
title: i18n("OTP")
|
title: i18n("OTP")
|
||||||
actions.main: addAction
|
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 {
|
Kirigami.CardsListView {
|
||||||
id: view
|
id: view
|
||||||
model: accounts
|
model: accounts
|
||||||
delegate: Kirigami.AbstractCard {
|
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 {
|
contentItem: Item {
|
||||||
implicitWidth: delegateLayout.implicitWidth
|
implicitWidth: delegateLayout.implicitWidth
|
||||||
implicitHeight: delegateLayout.implicitHeight
|
implicitHeight: delegateLayout.implicitHeight
|
||||||
|
@ -93,32 +74,39 @@ Kirigami.ApplicationWindow {
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Controls.Label {
|
Controls.Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: model.name
|
text: model.account ? model.account.name : i18nc("placeholder text if no account name is available", "(untitled)")
|
||||||
}
|
}
|
||||||
Kirigami.Heading {
|
Kirigami.Heading {
|
||||||
level: 2
|
level: 2
|
||||||
text: model.otp
|
text: model.account && model.account.token && model.account.token.length > 0 ? model.account.token : i18nc("placeholder text if no token is available", "(refresh)")
|
||||||
onTextChanged: {
|
|
||||||
if(model.type === Account.TypeTOTP) {
|
|
||||||
timeoutTimer.restart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Controls.Button {
|
Controls.Button {
|
||||||
Layout.alignment: Qt.AlignRight|Qt.AlignVCenter
|
Layout.alignment: Qt.AlignRight|Qt.AlignVCenter
|
||||||
Layout.columnSpan: 2
|
Layout.columnSpan: 2
|
||||||
text: i18nc("%1 is current counter numerical value", "Refresh (%1)", model.counter)
|
text: i18nc("%1 is current counter numerical value", "Refresh (%1)", model.counter)
|
||||||
visible: model.type === Account.TypeHOTP
|
visible: model.account && model.account.isHotp
|
||||||
onClicked: {
|
onClicked: {
|
||||||
accounts.generateNext(index);
|
if(model.account) {
|
||||||
|
model.account.advanceCounter();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Timer {
|
Timer {
|
||||||
id: timeoutTimer
|
id: timeoutTimer
|
||||||
repeat: true
|
repeat: false
|
||||||
interval: model.timeStep * 1000
|
interval: model.account && model.account.isTotp ? model.account.millisecondsLeftForToken() : 0
|
||||||
running: model.type === Account.TypeTOTP
|
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 {
|
Rectangle {
|
||||||
id: timeoutIndicatorRect
|
id: timeoutIndicatorRect
|
||||||
|
@ -141,7 +129,7 @@ Kirigami.ApplicationWindow {
|
||||||
from: delegateLayout.height
|
from: delegateLayout.height
|
||||||
to: 0
|
to: 0
|
||||||
duration: timeoutTimer.interval
|
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")
|
text: i18n("Add")
|
||||||
iconName: "answer-correct"
|
iconName: "answer-correct"
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
/*
|
if (tokenDetails.isTotp) {
|
||||||
* Nota Bene: order is significant.
|
accounts.addTotp(accountName.text, tokenDetails.secret, parseInt(tokenDetails.timeStep), tokenDetails.tokenLength);
|
||||||
* Accounts are being appended in order of creation,
|
}
|
||||||
* meaning the account index for the newly created
|
if (tokenDetails.isHotp) {
|
||||||
* account is equal to the size of the list as it was
|
accounts.addHotp(accountName.text, tokenDetails.secret, parseInt(tokenDetails.counter), tokenDetails.tokenLength);
|
||||||
* 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)
|
|
||||||
|
|
||||||
pageStack.pop();
|
pageStack.pop();
|
||||||
addActionEnabled = true;
|
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 <KLocalizedContext>
|
||||||
#include <KLocalizedString>
|
#include <KLocalizedString>
|
||||||
|
|
||||||
#include "accountmodel.h"
|
#include "app/keysmith.h"
|
||||||
#include "account.h"
|
#include "model/accounts.h"
|
||||||
#include "validators/qmlsupport.h"
|
#include "../validators/qmlsupport.h"
|
||||||
|
|
||||||
Q_DECL_EXPORT int main(int argc, char *argv[])
|
Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
|
@ -45,11 +45,18 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
engine.rootContext()->setContextObject(new KLocalizedContext(&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();
|
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()) {
|
if (engine.rootObjects().isEmpty()) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,5 @@
|
||||||
<qresource prefix="/">
|
<qresource prefix="/">
|
||||||
<file alias="main.qml">contents/ui/main.qml</file>
|
<file alias="main.qml">contents/ui/main.qml</file>
|
||||||
<file alias="TokenDetailsForm.qml">contents/ui/TokenDetailsForm.qml</file>
|
<file alias="TokenDetailsForm.qml">contents/ui/TokenDetailsForm.qml</file>
|
||||||
<file alias="AccountDetailsPage.qml">contents/ui/AccountDetailsPage.qml</file>
|
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|
Loading…
Reference in New Issue