refactor: introduce model class to collate validated input from add account forms (flow)

This change prepares the UI for supporting alternative and more complex flows for adding accounts.
All parameters are now collated into a single "validated input" object which is more convenient to pass around between views
This makes it possible to support back- and forth navigation between "basic" and "advanced/details" forms for adding accounts.

Additionally it provides a fundamental building block for adding alternative ways to add accounts (e.g. via OTP token URI/QR code).

Issues: #7
master
Johan Ouwerkerk 2020-07-21 00:04:29 +02:00
parent 170d7f1811
commit 63033b568d
8 changed files with 387 additions and 40 deletions

View File

@ -19,6 +19,9 @@ Kirigami.Page {
property Models.AccountListModel accounts: Keysmith.accountListModel()
property bool acceptable: accountName.acceptableInput && issuerName.acceptableInput && tokenDetails.acceptable
property Models.ValidatedAccountInput validatedInput: Models.ValidatedAccountInput {
}
ColumnLayout {
anchors {
horizontalCenter: parent.horizontalCenter
@ -26,15 +29,22 @@ Kirigami.Page {
Kirigami.FormLayout {
Controls.TextField {
id: accountName
text: validatedInput.name
Kirigami.FormData.label: i18nc("@label:textbox", "Account Name:")
validator: Validators.AccountNameValidator {
id: accountNameValidator
accounts: root.accounts
issuer: issuerName.text
issuer: validatedInput.issuer
}
onTextChanged: {
if (acceptableInput) {
validatedInput.name = text;
}
}
}
Controls.TextField {
id: issuerName
text: validatedInput.issuer
Kirigami.FormData.label: i18nc("@label:textbox", "Account Issuer:")
validator: Validators.AccountIssuerValidator {}
/*
@ -51,11 +61,15 @@ Kirigami.Page {
*/
accountNameValidator.issuer = issuerName.text;
accountName.insert(accountName.text.length, "");
if (acceptableInput) {
validatedInput.issuer = text;
}
}
}
}
TokenDetailsForm {
id: tokenDetails
validatedInput: root.validatedInput
}
}
@ -64,13 +78,7 @@ Kirigami.Page {
iconName: "answer-correct"
enabled: acceptable
onTriggered: {
if (tokenDetails.isTotp) {
console.log("WTF: ", Models.AccountListModel.Sha1);
accounts.addTotp(accountName.text, issuerName.text, tokenDetails.secret, tokenDetails.tokenLength, parseInt(tokenDetails.timeStep), new Date(0), Models.AccountListModel.Sha1);
}
if (tokenDetails.isHotp) {
accounts.addHotp(accountName.text, issuerName.text, tokenDetails.secret, tokenDetails.tokenLength, parseInt(tokenDetails.counter), false, 0, false);
}
root.accounts.addAccount(root.validatedInput);
root.dismissed();
}
}

View File

@ -4,6 +4,7 @@
* SPDX-FileCopyrightText: 2019-2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
*/
import Keysmith.Models 1.0 as Models
import Keysmith.Validators 1.0 as Validators
import QtQuick 2.1
import QtQuick.Layouts 1.2
@ -12,17 +13,12 @@ import org.kde.kirigami 2.8 as Kirigami
Kirigami.FormLayout {
id: root
property bool isTotp: totpRadio.checked && !hotpRadio.checked
property bool isHotp: hotpRadio.checked && !totpRadio.checked
property int tokenLength: tokenLengthField.value
property string timeStep: timeStepField.text
property string secret: accountSecret.text
property string counter: counterField.text
property Models.ValidatedAccountInput validatedInput
property bool secretAcceptable: accountSecret.acceptableInput
property bool timeStepAcceptable: timeStepField.acceptableInput || isHotp
property bool counterAcceptable: counterField.acceptableInput || isTotp
property bool tokenTypeAcceptable: isHotp || isTotp
property bool timeStepAcceptable: timeStepField.acceptableInput || hotpRadio.checked
property bool counterAcceptable: counterField.acceptableInput || totpRadio.checked
property bool tokenTypeAcceptable: hotpRadio.checked || totpRadio.checked
property bool acceptable: counterAcceptable && timeStepAcceptable && secretAcceptable && tokenTypeAcceptable
ColumnLayout {
@ -31,44 +27,69 @@ Kirigami.FormLayout {
Kirigami.FormData.buddyFor: totpRadio
Controls.RadioButton {
id: totpRadio
checked: true
checked: validatedInput && validatedInput.type === Models.ValidatedAccountInput.Totp
text: i18nc("@option:radio", "Time-based OTP")
onCheckedChanged: {
if (checked) {
validatedInput.type = Models.ValidatedAccountInput.Totp;
}
}
}
Controls.RadioButton {
id: hotpRadio
checked: false
checked: validatedInput && validatedInput.type === Models.ValidatedAccountInput.Hotp
text: i18nc("@option:radio", "Hash-based OTP")
onCheckedChanged: {
if (checked) {
validatedInput.type = Models.ValidatedAccountInput.Hotp;
}
}
}
}
Kirigami.PasswordField {
id: accountSecret
placeholderText: i18n("Token secret")
text: ""
text: validatedInput ? validatedInput.secret : ""
Kirigami.FormData.label: i18nc("@label:textbox", "Secret key:")
validator: Validators.Base32SecretValidator {
id: secretValidator
}
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText | Qt.ImhSensitiveData | Qt.ImhHiddenText
onTextChanged: {
if (acceptableInput) {
validatedInput.secret = text;
}
}
}
Controls.TextField {
id: timeStepField
Kirigami.FormData.label: i18nc("@label:textbox", "Timer:")
enabled: totpRadio.checked
text: "30"
enabled: validatedInput.type === Models.ValidatedAccountInput.Totp
text: validatedInput ? "" + validatedInput.timeStep : ""
validator: IntValidator {
bottom: 1
}
inputMethodHints: Qt.ImhDigitsOnly
onTextChanged: {
if (acceptableInput) {
validatedInput.timeStep = parseInt(text);
}
}
}
Controls.TextField {
id: counterField
text: "0"
text: validatedInput ? validatedInput.counter : ""
Kirigami.FormData.label: i18nc("@label:textbox", "Counter:")
enabled: hotpRadio.checked
validator: Validators.HOTPCounterValidator {
id: counterValidator
}
inputMethodHints: Qt.ImhDigitsOnly
onTextChanged: {
if (acceptableInput) {
validatedInput.setCounter(text, validator);
}
}
}
/*
* OATH tokens are derived from a 32bit value, base-10 encoded.
@ -82,6 +103,9 @@ Kirigami.FormLayout {
Kirigami.FormData.label: i18nc("@label:spinbox", "Token length:")
from: 6
to: 10
value: 6
value: validatedInput ? validatedInput.tokenLength : 6
onValueChanged: {
validatedInput.tokenLength = value;
}
}
}

View File

@ -14,6 +14,7 @@
#include "app/keysmith.h"
#include "model/accounts.h"
#include "model/input.h"
#include "validators/countervalidator.h"
#include "validators/datetimevalidator.h"
#include "validators/issuervalidator.h"
@ -36,6 +37,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
qmlRegisterUncreatableType<model::SimpleAccountListModel>("Keysmith.Models", 1, 0, "AccountListModel", "Use the Keysmith singleton to obtain an AccountListModel");
qmlRegisterUncreatableType<model::PasswordRequest>("Keysmith.Models", 1, 0, "PasswordRequestModel", "Use the Keysmith singleton to obtain an PasswordRequestModel");
qmlRegisterUncreatableType<model::AccountView>("Keysmith.Models", 1, 0, "Account", "Use an AccountListModel from the Keysmith singleton to obtain an Account");
qmlRegisterType<model::AccountInput>("Keysmith.Models", 1, 0, "ValidatedAccountInput");
qmlRegisterType<model::SortedAccountsListModel>("Keysmith.Models", 1, 0, "SortedAccountListModel");
qmlRegisterType<model::AccountNameValidator>("Keysmith.Validators", 1, 0, "AccountNameValidator");
qmlRegisterType<validators::EpochValidator>("Keysmith.Validators", 1, 0, "TOTPEpochValidator");

View File

@ -6,6 +6,7 @@
set(model_SRCS
accounts.cpp
password.cpp
input.cpp
)
add_library(model_lib STATIC ${model_SRCS})

View File

@ -153,19 +153,13 @@ namespace model
}
}
void SimpleAccountListModel::addTotp(const QString &account, const QString &issuer,
const QString &secret, uint tokenLength,
uint timeStep, const QDateTime &epoch, TOTPAlgorithms hash)
void SimpleAccountListModel::addAccount(AccountInput *input)
{
m_storage->addTotp(account, issuer, secret, tokenLength, timeStep, epoch, toHash(hash));
}
void SimpleAccountListModel::addHotp(const QString &account, const QString &issuer,
const QString &secret, uint tokenLength,
quint64 counter, bool fixedTruncation, uint offset, bool checksum)
{
const auto o = fixedTruncation ? std::optional<uint>(offset) : std::nullopt;
m_storage->addHotp(account, issuer, secret, tokenLength, counter, o, checksum);
if (!input) {
qCDebug(logger) << "Not adding account, no input provided";
return;
}
input->createNewAccount(m_storage);
}
QHash<int, QByteArray> SimpleAccountListModel::roleNames(void) const

View File

@ -5,6 +5,7 @@
#ifndef MODEL_ACCOUNTS_H
#define MODEL_ACCOUNTS_H
#include "input.h"
#include "../account/account.h"
#include "../validators/namevalidator.h"
@ -74,10 +75,7 @@ namespace model
static accounts::Account::Hash toHash(const TOTPAlgorithms value);
public:
explicit SimpleAccountListModel(accounts::AccountStorage *storage, QObject *parent = nullptr);
Q_INVOKABLE void addTotp(const QString &account, const QString &issuer, const QString &secret, uint tokenLength,
uint timeStep, const QDateTime &epoch, model::SimpleAccountListModel::TOTPAlgorithms hash);
Q_INVOKABLE void addHotp(const QString &account, const QString &issuer, const QString &secret, uint tokenLength,
quint64 counter, bool fixedTruncation, uint offset, bool checksum);
Q_INVOKABLE void addAccount(AccountInput *input);
Q_INVOKABLE bool isAccountStillAvailable(const QString &name, const QString &issuer) const;
Q_INVOKABLE int rowCount(const QModelIndex &parent = QModelIndex()) const override;
Q_INVOKABLE QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;

220
src/model/input.cpp Normal file
View File

@ -0,0 +1,220 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
*/
#include "input.h"
#include <QLocale>
static QDateTime DEFAULT_EPOCH_VALUE = QDateTime::fromMSecsSinceEpoch(0, Qt::UTC);
static QString DEFAULT_EPOCH = DEFAULT_EPOCH_VALUE.toString(Qt::ISODate);
static QString DEFAULT_COUNTER = QLocale::c().toString(0ULL);
namespace model
{
static accounts::Account::Hash toHash(AccountInput::TOTPAlgorithm algorithm)
{
switch(algorithm) {
case AccountInput::TOTPAlgorithm::Sha1:
return accounts::Account::Hash::Sha1;
case AccountInput::TOTPAlgorithm::Sha256:
return accounts::Account::Hash::Sha256;
case AccountInput::TOTPAlgorithm::Sha512:
return accounts::Account::Hash::Sha512;
default:
Q_ASSERT_X(false, Q_FUNC_INFO, "Unknown/unsupported TOTP hashing algorithm?");
return accounts::Account::Hash::Sha1;
}
}
AccountInput::AccountInput(QObject *parent) :
QObject(parent),
m_type(TokenType::Totp), m_name(QString()), m_issuer(QString()), m_secret(QString()), m_tokenLength(6U),
m_timeStep(30U), m_algorithm(TOTPAlgorithm::Sha1), m_epoch(DEFAULT_EPOCH), m_epochValue(DEFAULT_EPOCH_VALUE),
m_checksum(false), m_counter(DEFAULT_COUNTER), m_counterValue(0ULL), m_truncation(std::nullopt)
{
}
void AccountInput::createNewAccount(accounts::AccountStorage *storage) const
{
if (!storage) {
Q_ASSERT_X(false, Q_FUNC_INFO, "Storage must be provided");
return;
}
switch(m_type) {
case Hotp:
storage->addHotp(m_name, m_issuer, m_secret, m_tokenLength, m_counterValue, m_truncation, m_checksum);
break;
case Totp:
storage->addTotp(m_name, m_issuer, m_secret, m_tokenLength, m_timeStep, m_epochValue, toHash(m_algorithm));
break;
default:
Q_ASSERT_X(false, Q_FUNC_INFO, "Unknown/unsupported token type?");
}
}
AccountInput::TokenType AccountInput::type(void) const
{
return m_type;
}
void AccountInput::setType(model::AccountInput::TokenType type)
{
if (m_type != type) {
m_type = type;
Q_EMIT typeChanged();
}
}
QString AccountInput::name(void) const
{
return m_name;
}
void AccountInput::setName(const QString &name)
{
if (m_name != name) {
m_name = name;
Q_EMIT nameChanged();
}
}
QString AccountInput::issuer(void) const
{
return m_issuer;
}
void AccountInput::setIssuer(const QString &issuer)
{
if (m_issuer != issuer) {
m_issuer = issuer;
Q_EMIT issuerChanged();
}
}
QString AccountInput::secret(void) const
{
return m_secret;
}
void AccountInput::setSecret(QString &secret)
{
if (m_secret != secret) {
m_secret = secret;
Q_EMIT secretChanged();
}
}
uint AccountInput::tokenLength(void) const
{
return m_tokenLength;
}
void AccountInput::setTokenLength(uint tokenLength)
{
if (m_tokenLength != tokenLength) {
m_tokenLength = tokenLength;
Q_EMIT tokenLengthChanged();
}
}
uint AccountInput::timeStep(void) const
{
return m_timeStep;
}
void AccountInput::setTimeStep(uint timeStep)
{
if (m_timeStep != timeStep) {
m_timeStep = timeStep;
Q_EMIT timeStepChanged();
}
}
AccountInput::TOTPAlgorithm AccountInput::algorithm(void) const
{
return m_algorithm;
}
void AccountInput::setAlgorithm(model::AccountInput::TOTPAlgorithm algorithm)
{
if (m_algorithm != algorithm) {
m_algorithm = algorithm;
Q_EMIT algorithmChanged();
}
}
QString AccountInput::epoch(void) const
{
return m_epoch;
}
void AccountInput::setEpoch(const QString &epoch)
{
if (m_epoch != epoch) {
m_epoch = epoch;
m_epochValue = validators::parseDateTime(epoch).value_or(DEFAULT_EPOCH_VALUE);
Q_EMIT epochChanged();
}
}
bool AccountInput::checksum(void) const
{
return m_checksum;
}
void AccountInput::setChecksum(bool checksum)
{
if (m_checksum != checksum) {
m_checksum = checksum;
Q_EMIT checksumChanged();
}
}
QString AccountInput::counter(void) const
{
return m_counter;
}
void AccountInput::setCounter(const QString &counter, validators::UnsignedLongValidator *validator)
{
if (!validator) {
Q_ASSERT_X(false, Q_FUNC_INFO, "Validator must be provided");
return;
}
if (m_counter != counter) {
m_counter = counter;
m_counterValue = validators::parse(counter, validator->locale()).value_or(0ULL);
Q_EMIT counterChanged();
}
}
uint AccountInput::truncationOffset(void) const
{
return m_truncation.value_or(0U);
}
void AccountInput::setTruncationOffset(uint truncationOffset)
{
if (!m_truncation || *m_truncation != truncationOffset) {
m_truncation = std::optional<uint>(truncationOffset);
Q_EMIT truncationChanged();
}
}
bool AccountInput::fixedTruncation(void) const
{
return (bool) m_truncation;
}
void AccountInput::setDynamicTruncation(void)
{
if (m_truncation) {
m_truncation = std::nullopt;
Q_EMIT truncationChanged();
}
}
}

100
src/model/input.h Normal file
View File

@ -0,0 +1,100 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
*/
#ifndef MODEL_INPUT_H
#define MODEL_INPUT_H
#include "../account/account.h"
#include "../validators/countervalidator.h"
#include "../validators/datetimevalidator.h"
#include <QDateTime>
#include <QObject>
#include <QString>
#include <optional>
namespace model
{
class AccountInput : public QObject
{
Q_OBJECT
Q_PROPERTY(model::AccountInput::TokenType type READ type WRITE setType NOTIFY typeChanged)
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
Q_PROPERTY(QString issuer READ issuer WRITE setIssuer NOTIFY issuerChanged)
Q_PROPERTY(QString secret READ secret WRITE setSecret NOTIFY secretChanged)
Q_PROPERTY(uint tokenLength READ tokenLength WRITE setTokenLength NOTIFY tokenLengthChanged);
Q_PROPERTY(uint timeStep READ timeStep WRITE setTimeStep NOTIFY timeStepChanged)
Q_PROPERTY(model::AccountInput::TOTPAlgorithm algorithm READ algorithm WRITE setAlgorithm NOTIFY algorithmChanged)
Q_PROPERTY(QString epoch READ epoch WRITE setEpoch NOTIFY epochChanged)
Q_PROPERTY(bool checksum READ checksum WRITE setChecksum NOTIFY checksumChanged)
Q_PROPERTY(QString counter READ counter NOTIFY counterChanged)
Q_PROPERTY(uint truncationOffset READ truncationOffset NOTIFY truncationChanged);
Q_PROPERTY(bool fixedTruncation READ fixedTruncation NOTIFY truncationChanged);
public:
enum TOTPAlgorithm {
Sha1, Sha256, Sha512
};
Q_ENUM(TOTPAlgorithm)
enum TokenType {
Hotp, Totp
};
Q_ENUM(TokenType)
AccountInput(QObject *parent = nullptr);
void createNewAccount(accounts::AccountStorage *storage) const;
public:
TokenType type(void) const;
void setType(model::AccountInput::TokenType type);
QString name(void) const;
void setName(const QString &name);
QString issuer(void) const;
void setIssuer(const QString &issuer);
QString secret(void) const;
void setSecret(QString &secret);
uint tokenLength(void) const;
void setTokenLength(uint tokenLength);
uint timeStep(void) const;
void setTimeStep(uint timeStep);
TOTPAlgorithm algorithm(void) const;
void setAlgorithm(model::AccountInput::TOTPAlgorithm algorithm);
QString epoch(void) const;
void setEpoch(const QString &epoch);
bool checksum(void) const;
void setChecksum(bool checksum);
QString counter(void) const;
Q_INVOKABLE void setCounter(const QString &counter, validators::UnsignedLongValidator *validator);
uint truncationOffset(void) const;
bool fixedTruncation(void) const;
Q_INVOKABLE void setTruncationOffset(uint truncationOffset);
Q_INVOKABLE void setDynamicTruncation(void);
Q_SIGNALS:
void typeChanged(void);
void nameChanged(void);
void issuerChanged(void);
void secretChanged(void);
void tokenLengthChanged(void);
void timeStepChanged(void);
void algorithmChanged(void);
void epochChanged(void);
void checksumChanged(void);
void counterChanged(void);
void truncationChanged(void);
private:
TokenType m_type;
QString m_name;
QString m_issuer;
QString m_secret;
uint m_tokenLength;
uint m_timeStep;
TOTPAlgorithm m_algorithm;
QString m_epoch;
QDateTime m_epochValue;
bool m_checksum;
QString m_counter;
quint64 m_counterValue;
std::optional<uint> m_truncation;
};
}
#endif