Use the new Account models.

Drop the AccountDetailsPage instead of trying to update it: see issue #7

With this change issue #2 should be fixed
master
Johan Ouwerkerk 2019-12-27 18:24:59 +01:00 committed by Bhushan Shah
parent 1f15fb6e08
commit a0caf83da2
13 changed files with 53 additions and 952 deletions

View File

@ -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)

View File

@ -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"

View File

@ -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"

View File

@ -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})

View File

@ -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();
}
}

View File

@ -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

View File

@ -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();
}

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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
} }
} }

View File

@ -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.
*/
}
}
}
} }

View File

@ -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;
} }

View File

@ -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>