From a0caf83da201e0fc9f546d66ed7c37611fe433a7 Mon Sep 17 00:00:00 2001 From: Johan Ouwerkerk Date: Fri, 27 Dec 2019 18:24:59 +0100 Subject: [PATCH] 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 --- autotests/CMakeLists.txt | 6 - autotests/hotp-generator-samples.cpp | 90 -------------- autotests/totp-generator-samples.cpp | 98 ---------------- src/CMakeLists.txt | 4 +- src/account.cpp | 209 --------------------------------- src/account.h | 99 ---------------- src/accountmodel.cpp | 186 ----------------------------- src/accountmodel.h | 66 ----------- src/contents/ui/AccountDetailsPage.qml | 86 -------------- src/contents/ui/TokenDetailsForm.qml | 19 ++- src/contents/ui/main.qml | 122 +++++-------------- src/main.cpp | 19 ++- src/resources.qrc | 1 - 13 files changed, 53 insertions(+), 952 deletions(-) delete mode 100644 autotests/hotp-generator-samples.cpp delete mode 100644 autotests/totp-generator-samples.cpp delete mode 100644 src/account.cpp delete mode 100644 src/account.h delete mode 100644 src/accountmodel.cpp delete mode 100644 src/accountmodel.h delete mode 100644 src/contents/ui/AccountDetailsPage.qml diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 1bbe36b..b378bd8 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -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) diff --git a/autotests/hotp-generator-samples.cpp b/autotests/hotp-generator-samples.cpp deleted file mode 100644 index 152620c..0000000 --- a/autotests/hotp-generator-samples.cpp +++ /dev/null @@ -1,90 +0,0 @@ -/***************************************************************************** - * Copyright: 2019 Johan Ouwerkerk * - * * - * 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 . * - * * - ****************************************************************************/ - -#include "account.h" - -#include -#include - -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("counter"); - QTest::addColumn("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" diff --git a/autotests/totp-generator-samples.cpp b/autotests/totp-generator-samples.cpp deleted file mode 100644 index fb3a08c..0000000 --- a/autotests/totp-generator-samples.cpp +++ /dev/null @@ -1,98 +0,0 @@ -/***************************************************************************** - * Copyright: 2019 Johan Ouwerkerk * - * * - * 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 . * - * * - ****************************************************************************/ - -#include "account.h" - -#include -#include - -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 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("counter"); - QTest::addColumn("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" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 64e91a7..8603e8b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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}) diff --git a/src/account.cpp b/src/account.cpp deleted file mode 100644 index a24490f..0000000 --- a/src/account.cpp +++ /dev/null @@ -1,209 +0,0 @@ -/***************************************************************************** - * Copyright: 2013 Michael Zanetti * - * * - * 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 . * - * * - ****************************************************************************/ - -#include "account.h" - -#include "base32/base32.h" -#include "oath_p.h" - -#include -#include - -Account::Account(const QUuid &id, QObject *parent) : Account(id, &QDateTime::currentMSecsSinceEpoch, parent) {} - -Account::Account(const QUuid &id, const std::function& 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 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(); - } - -} - diff --git a/src/account.h b/src/account.h deleted file mode 100644 index 858439f..0000000 --- a/src/account.h +++ /dev/null @@ -1,99 +0,0 @@ -/***************************************************************************** - * Copyright: 2013 Michael Zanetti * - * * - * 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 . * - * * - ****************************************************************************/ - -#ifndef ACCOUNT_H -#define ACCOUNT_H - -#include -#include -#include - -#include - -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& 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 m_clock; -}; - -#endif // ACCOUNT_H diff --git a/src/accountmodel.cpp b/src/accountmodel.cpp deleted file mode 100644 index b2a4c98..0000000 --- a/src/accountmodel.cpp +++ /dev/null @@ -1,186 +0,0 @@ -/***************************************************************************** - * Copyright: 2013 Michael Zanetti * - * * - * 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 . * - * * - ****************************************************************************/ - -#include "accountmodel.h" - -#include "account.h" - -#include -#include -//#include - -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 AccountModel::roleNames() const -{ - QHash 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() << RoleCounter << RoleOtp); -} - -void AccountModel::refresh() -{ - Q_EMIT beginResetModel(); - Q_EMIT endResetModel(); -} - -void AccountModel::accountChanged() -{ - Account *account = qobject_cast(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(); - -} diff --git a/src/accountmodel.h b/src/accountmodel.h deleted file mode 100644 index 8859642..0000000 --- a/src/accountmodel.h +++ /dev/null @@ -1,66 +0,0 @@ -/***************************************************************************** - * Copyright: 2013 Michael Zanetti * - * * - * 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 . * - * * - ****************************************************************************/ - -#ifndef ACCOUNTMODEL_H -#define ACCOUNTMODEL_H - -#include - -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 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 m_accounts; -}; - -#endif // ACCOUNTMODEL_H diff --git a/src/contents/ui/AccountDetailsPage.qml b/src/contents/ui/AccountDetailsPage.qml deleted file mode 100644 index df42eed..0000000 --- a/src/contents/ui/AccountDetailsPage.qml +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2019 Johan Ouwerkerk - * - * 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 . - * - */ - -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 - } - } -} diff --git a/src/contents/ui/TokenDetailsForm.qml b/src/contents/ui/TokenDetailsForm.qml index df21806..11d06dc 100644 --- a/src/contents/ui/TokenDetailsForm.qml +++ b/src/contents/ui/TokenDetailsForm.qml @@ -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 } } diff --git a/src/contents/ui/main.qml b/src/contents/ui/main.qml index 17182a4..c55c2be 100644 --- a/src/contents/ui/main.qml +++ b/src/contents/ui/main.qml @@ -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. - */ - } - } - } } diff --git a/src/main.cpp b/src/main.cpp index 2b53c26..c104d35 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -27,9 +27,9 @@ #include #include -#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("Oath", 1, 0, "AccountModel"); - qmlRegisterUncreatableType("Oath", 1, 0, "Account", "Use AccountModel::createAccount() to create a new account"); validators::registerValidatorTypes(); - engine.load(QUrl(QStringLiteral("qrc:///main.qml"))); + qmlRegisterUncreatableType("Keysmith.Models", 1, 0, "AccountListModel", "Use the Keysmith singleton to obtain an AccountListModel"); + qmlRegisterUncreatableType("Keysmith.Models", 1, 0, "Account", "Use an AccountListModel from the Keysmith singleton to obtain an Account"); + qmlRegisterSingletonType("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; } diff --git a/src/resources.qrc b/src/resources.qrc index f82cef8..d863868 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -2,6 +2,5 @@ contents/ui/main.qml contents/ui/TokenDetailsForm.qml - contents/ui/AccountDetailsPage.qml