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

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

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

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

View File

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

View File

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