diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f1389c55d..ea09d43b9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -128,7 +128,7 @@ set(keepassx_SOURCES gui/SettingsWidget.cpp gui/SearchWidget.cpp gui/SortFilterHideProxyModel.cpp - gui/SetupTotpDialog.cpp + gui/TotpSetupDialog.cpp gui/TotpDialog.cpp gui/UnlockDatabaseWidget.cpp gui/UnlockDatabaseDialog.cpp diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 598ea8497..4233d89e3 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -45,8 +45,6 @@ Entry::Entry() m_data.iconNumber = DefaultIconNumber; m_data.autoTypeEnabled = true; m_data.autoTypeObfuscation = 0; - m_data.totpStep = Totp::defaultStep; - m_data.totpDigits = Totp::defaultDigits; connect(m_attributes, SIGNAL(modified()), SLOT(updateTotp())); connect(m_attributes, SIGNAL(modified()), this, SIGNAL(modified())); @@ -347,74 +345,45 @@ const CustomData* Entry::customData() const bool Entry::hasTotp() const { - return m_attributes->hasKey("TOTP Seed") || m_attributes->hasKey("otp"); + return !m_data.totpSettings.isNull(); } QString Entry::totp() const { if (hasTotp()) { - QString seed = totpSeed(); - quint64 time = QDateTime::currentDateTime().toTime_t(); - QString output = Totp::generateTotp(seed.toLatin1(), time, m_data.totpDigits, m_data.totpStep); - - return QString(output); + return Totp::generateTotp(m_data.totpSettings); } return {}; } -void Entry::setTotp(const QString& seed, quint8& step, quint8& digits) +void Entry::setTotp(QSharedPointer settings) { beginUpdate(); - if (step == 0) { - step = Totp::defaultStep; - } + m_data.totpSettings = settings; - if (digits == 0) { - digits = Totp::defaultDigits; - } - QString data; - - const Totp::Encoder& enc = Totp::encoders.value(digits, Totp::defaultEncoder); - - if (m_attributes->hasKey("otp")) { - data = QString("key=%1&step=%2&size=%3").arg(seed).arg(step).arg(enc.digits == 0 ? digits : enc.digits); - if (!enc.name.isEmpty()) { - data.append("&enocder=").append(enc.name); - } - m_attributes->set("otp", data, true); + auto text = Totp::writeSettings(m_data.totpSettings); + if (m_attributes->hasKey(Totp::ATTRIBUTE_OTP)) { + m_attributes->set(Totp::ATTRIBUTE_OTP, text, true); } else { - m_attributes->set("TOTP Seed", seed, true); - if (!enc.shortName.isEmpty()) { - data = QString("%1;%2").arg(step).arg(enc.shortName); - } else { - data = QString("%1;%2").arg(step).arg(digits); - } - m_attributes->set("TOTP Settings", data); + m_attributes->set(Totp::ATTRIBUTE_SEED, m_data.totpSettings->key, true); + m_attributes->set(Totp::ATTRIBUTE_SETTINGS, text); } endUpdate(); } -QString Entry::totpSeed() const +void Entry::updateTotp() { - QString secret = ""; - - if (m_attributes->hasKey("otp")) { - secret = m_attributes->value("otp"); - } else if (m_attributes->hasKey("TOTP Seed")) { - secret = m_attributes->value("TOTP Seed"); + if (m_attributes->contains(Totp::ATTRIBUTE_SETTINGS)) { + m_data.totpSettings = Totp::parseSettings(m_attributes->value(Totp::ATTRIBUTE_SETTINGS), + m_attributes->value(Totp::ATTRIBUTE_SEED)); + } else if (m_attributes->contains(Totp::ATTRIBUTE_OTP)) { + m_data.totpSettings = Totp::parseSettings(m_attributes->value(Totp::ATTRIBUTE_OTP)); } - - return Totp::parseOtpString(secret, m_data.totpDigits, m_data.totpStep); } -quint8 Entry::totpStep() const +QSharedPointer Entry::totpSettings() const { - return m_data.totpStep; -} - -quint8 Entry::totpDigits() const -{ - return m_data.totpDigits; + return m_data.totpSettings; } void Entry::setUuid(const QUuid& uuid) @@ -725,33 +694,6 @@ void Entry::updateModifiedSinceBegin() m_modifiedSinceBegin = true; } -/** - * Update TOTP data whenever entry attributes have changed. - */ -void Entry::updateTotp() -{ - m_data.totpDigits = Totp::defaultDigits; - m_data.totpStep = Totp::defaultStep; - - if (!m_attributes->hasKey("TOTP Settings")) { - return; - } - - // this regex must be kept in sync with the set of allowed short names Totp::shortNameToEncoder - QRegularExpression rx(QString("(\\d+);((?:\\d+)|S)")); - QRegularExpressionMatch m = rx.match(m_attributes->value("TOTP Settings")); - if (!m.hasMatch()) { - return; - } - - m_data.totpStep = static_cast(m.captured(1).toUInt()); - if (Totp::shortNameToEncoder.contains(m.captured(2))) { - m_data.totpDigits = Totp::shortNameToEncoder[m.captured(2)]; - } else { - m_data.totpDigits = static_cast(m.captured(2).toUInt()); - } -} - QString Entry::resolveMultiplePlaceholdersRecursive(const QString& str, int maxDepth) const { if (maxDepth <= 0) { diff --git a/src/core/Entry.h b/src/core/Entry.h index de6a4b398..aa2426c5e 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -36,6 +36,9 @@ class Database; class Group; +namespace Totp { + struct Settings; +} enum class EntryReferenceType { @@ -61,8 +64,7 @@ struct EntryData int autoTypeObfuscation; QString defaultAutoTypeSequence; TimeInfo timeInfo; - mutable quint8 totpDigits; - mutable quint8 totpStep; + QSharedPointer totpSettings; }; class Entry : public QObject @@ -98,9 +100,7 @@ public: QString password() const; QString notes() const; QString totp() const; - QString totpSeed() const; - quint8 totpDigits() const; - quint8 totpStep() const; + QSharedPointer totpSettings() const; bool hasTotp() const; bool isExpired() const; @@ -135,7 +135,7 @@ public: void setNotes(const QString& notes); void setExpires(const bool& value); void setExpiryTime(const QDateTime& dateTime); - void setTotp(const QString& seed, quint8& step, quint8& digits); + void setTotp(QSharedPointer settings); QList historyItems(); const QList& historyItems() const; diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index fde356fd5..c96c9000c 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -47,7 +47,7 @@ #include "gui/DetailsWidget.h" #include "gui/KeePass1OpenWidget.h" #include "gui/MessageBox.h" -#include "gui/SetupTotpDialog.h" +#include "gui/TotpSetupDialog.h" #include "gui/TotpDialog.h" #include "gui/UnlockDatabaseDialog.h" #include "gui/UnlockDatabaseWidget.h" @@ -444,15 +444,8 @@ void DatabaseWidget::setupTotp() return; } - auto setupTotpDialog = new SetupTotpDialog(this, currentEntry); - if (currentEntry->hasTotp()) { - setupTotpDialog->setSeed(currentEntry->totpSeed()); - setupTotpDialog->setStep(currentEntry->totpStep()); - setupTotpDialog->setDigits(currentEntry->totpDigits()); - // now that all settings are set, decide whether it's default, steam or custom - setupTotpDialog->setSettings(currentEntry->totpDigits()); - } - + auto setupTotpDialog = new TotpSetupDialog(this, currentEntry); + connect(setupTotpDialog, SIGNAL(totpUpdated()), SIGNAL(entrySelectionChanged())); setupTotpDialog->open(); } @@ -938,6 +931,13 @@ void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::Mod openUrlForEntry(entry); } break; + case EntryModel::Totp: + if (entry->hasTotp()) { + setClipboardTextAndMinimize(entry->totp()); + } else { + setupTotp(); + } + break; // TODO: switch to 'Notes' tab in details view/pane // case EntryModel::Notes: // break; diff --git a/src/gui/DetailsWidget.cpp b/src/gui/DetailsWidget.cpp index 70286b9d7..ff8861172 100644 --- a/src/gui/DetailsWidget.cpp +++ b/src/gui/DetailsWidget.cpp @@ -39,8 +39,6 @@ DetailsWidget::DetailsWidget(QWidget* parent) , m_locked(false) , m_currentEntry(nullptr) , m_currentGroup(nullptr) - , m_step(0) - , m_totpTimer(nullptr) , m_selectedTabEntry(0) , m_selectedTabGroup(0) { @@ -56,6 +54,7 @@ DetailsWidget::DetailsWidget(QWidget* parent) connect(m_ui->entryTotpButton, SIGNAL(toggled(bool)), m_ui->entryTotpWidget, SLOT(setVisible(bool))); connect(m_ui->entryCloseButton, SIGNAL(toggled(bool)), SLOT(hide())); connect(m_ui->entryTabWidget, SIGNAL(tabBarClicked(int)), SLOT(updateTabIndexes()), Qt::QueuedConnection); + connect(&m_totpTimer, SIGNAL(timeout()), this, SLOT(updateTotpLabel())); // Group m_ui->groupCloseButton->setIcon(filePath()->icon("actions", "dialog-close")); @@ -65,7 +64,6 @@ DetailsWidget::DetailsWidget(QWidget* parent) DetailsWidget::~DetailsWidget() { - deleteTotpTimer(); } void DetailsWidget::setEntry(Entry* selectedEntry) @@ -146,16 +144,11 @@ void DetailsWidget::updateEntryTotp() m_ui->entryTotpButton->setChecked(false); if (hasTotp) { - deleteTotpTimer(); - m_totpTimer = new QTimer(m_currentEntry); - connect(m_totpTimer, SIGNAL(timeout()), this, SLOT(updateTotpLabel())); - m_totpTimer->start(1000); - - m_step = m_currentEntry->totpStep(); + m_totpTimer.start(1000); updateTotpLabel(); } else { m_ui->entryTotpLabel->clear(); - stopTotpTimer(); + m_totpTimer.stop(); } } @@ -274,30 +267,16 @@ void DetailsWidget::updateGroupNotesTab() m_ui->groupNotesEdit->setText(notes); } -void DetailsWidget::stopTotpTimer() -{ - if (m_totpTimer) { - m_totpTimer->stop(); - } -} - -void DetailsWidget::deleteTotpTimer() -{ - if (m_totpTimer) { - delete m_totpTimer; - } -} - void DetailsWidget::updateTotpLabel() { - if (!m_locked && m_currentEntry) { + if (!m_locked && m_currentEntry && m_currentEntry->hasTotp()) { const QString totpCode = m_currentEntry->totp(); const QString firstHalf = totpCode.left(totpCode.size() / 2); const QString secondHalf = totpCode.mid(totpCode.size() / 2); m_ui->entryTotpLabel->setText(firstHalf + " " + secondHalf); } else { m_ui->entryTotpLabel->clear(); - stopTotpTimer(); + m_totpTimer.stop(); } } diff --git a/src/gui/DetailsWidget.h b/src/gui/DetailsWidget.h index 3a4c277d9..ba42e5278 100644 --- a/src/gui/DetailsWidget.h +++ b/src/gui/DetailsWidget.h @@ -33,7 +33,7 @@ class DetailsWidget : public QWidget public: explicit DetailsWidget(QWidget* parent = nullptr); - ~DetailsWidget(); + ~DetailsWidget() override; public slots: void setEntry(Entry* selectedEntry); @@ -56,8 +56,6 @@ private slots: void updateGroupGeneralTab(); void updateGroupNotesTab(); - void stopTotpTimer(); - void deleteTotpTimer(); void updateTotpLabel(); void updateTabIndexes(); @@ -71,8 +69,7 @@ private: bool m_locked; Entry* m_currentEntry; Group* m_currentGroup; - quint8 m_step; - QPointer m_totpTimer; + QTimer m_totpTimer; quint8 m_selectedTabEntry; quint8 m_selectedTabGroup; }; diff --git a/src/gui/SetupTotpDialog.cpp b/src/gui/SetupTotpDialog.cpp deleted file mode 100644 index ef7ee9e7c..000000000 --- a/src/gui/SetupTotpDialog.cpp +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (C) 2017 Weslly Honorato <weslly@protonmail.com> - * Copyright (C) 2017 KeePassXC Team - * - * 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 2 or (at your option) - * 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 . - */ - -#include "SetupTotpDialog.h" -#include "totp/totp.h" -#include "ui_SetupTotpDialog.h" - -SetupTotpDialog::SetupTotpDialog(DatabaseWidget* parent, Entry* entry) - : QDialog(parent) - , m_ui(new Ui::SetupTotpDialog()) -{ - m_entry = entry; - m_parent = parent; - - m_ui->setupUi(this); - setAttribute(Qt::WA_DeleteOnClose); - - this->setFixedSize(this->sizeHint()); - - connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(close())); - connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(setupTotp())); - connect(m_ui->radioDefault, SIGNAL(toggled(bool)), SLOT(toggleDefault(bool))); - connect(m_ui->radioSteam, SIGNAL(toggled(bool)), SLOT(toggleSteam(bool))); - connect(m_ui->radioCustom, SIGNAL(toggled(bool)), SLOT(toggleCustom(bool))); -} - -void SetupTotpDialog::setupTotp() -{ - quint8 digits; - - if (m_ui->radioSteam->isChecked()) { - digits = Totp::ENCODER_STEAM; - } else if (m_ui->radio8Digits->isChecked()) { - digits = 8; - } else { - digits = 6; - } - - quint8 step = m_ui->stepSpinBox->value(); - QString seed = Totp::parseOtpString(m_ui->seedEdit->text(), digits, step); - m_entry->setTotp(seed, step, digits); - emit m_parent->entrySelectionChanged(); - close(); -} - -void SetupTotpDialog::toggleDefault(bool status) -{ - if (status) { - setStep(Totp::defaultStep); - setDigits(Totp::defaultDigits); - } -} - -void SetupTotpDialog::toggleSteam(bool status) -{ - if (status) { - setStep(Totp::defaultStep); - setDigits(Totp::ENCODER_STEAM); - } -} - -void SetupTotpDialog::toggleCustom(bool status) -{ - m_ui->digitsLabel->setEnabled(status); - m_ui->radio6Digits->setEnabled(status); - m_ui->radio8Digits->setEnabled(status); - - m_ui->stepLabel->setEnabled(status); - m_ui->stepSpinBox->setEnabled(status); -} - -void SetupTotpDialog::setSeed(QString value) -{ - m_ui->seedEdit->setText(value); -} - -void SetupTotpDialog::setSettings(quint8 digits) -{ - quint8 step = m_ui->stepSpinBox->value(); - - bool isDefault = ((step == Totp::defaultStep) && (digits == Totp::defaultDigits)); - bool isSteam = (digits == Totp::ENCODER_STEAM); - - if (isSteam) { - m_ui->radioSteam->setChecked(true); - } else if (isDefault) { - m_ui->radioDefault->setChecked(true); - } else { - m_ui->radioCustom->setChecked(true); - } -} - -void SetupTotpDialog::setStep(quint8 step) -{ - m_ui->stepSpinBox->setValue(step); -} - -void SetupTotpDialog::setDigits(quint8 digits) -{ - if (digits == 8) { - m_ui->radio8Digits->setChecked(true); - m_ui->radio6Digits->setChecked(false); - } else { - m_ui->radio6Digits->setChecked(true); - m_ui->radio8Digits->setChecked(false); - } -} - -SetupTotpDialog::~SetupTotpDialog() -{ -} diff --git a/src/gui/SetupTotpDialog.ui b/src/gui/SetupTotpDialog.ui deleted file mode 100644 index 327ca6d0a..000000000 --- a/src/gui/SetupTotpDialog.ui +++ /dev/null @@ -1,167 +0,0 @@ - - - SetupTotpDialog - - - - 0 - 0 - 282 - 364 - - - - Setup TOTP - - - - - - - - Key: - - - - - - - - - - - - - - Default RFC 6238 token settings - - - settingsButtonGroup - - - - - - - Steam token settings - - - settingsButtonGroup - - - - - - - Use custom settings - - - settingsButtonGroup - - - - - - - - - Note: Change these settings only if you know what you are doing. - - - true - - - - - - - QFormLayout::ExpandingFieldsGrow - - - QFormLayout::DontWrapRows - - - Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing - - - - - false - - - Time step: - - - - - - - false - - - 8 digits - - - - - - - false - - - 6 digits - - - true - - - - - - - false - - - Code size: - - - - - - - false - - - sec - - - 1 - - - 60 - - - 30 - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - - - diff --git a/src/gui/TotpDialog.cpp b/src/gui/TotpDialog.cpp index 75cf6a482..c2de9adbd 100644 --- a/src/gui/TotpDialog.cpp +++ b/src/gui/TotpDialog.cpp @@ -22,21 +22,25 @@ #include "core/Config.h" #include "gui/Clipboard.h" -TotpDialog::TotpDialog(DatabaseWidget* parent, Entry* entry) +TotpDialog::TotpDialog(QWidget* parent, Entry* entry) : QDialog(parent) , m_ui(new Ui::TotpDialog()) - , m_totpUpdateTimer(new QTimer(entry)) , m_entry(entry) { + if (!m_entry->hasTotp()) { + close(); + return; + } + m_ui->setupUi(this); - m_step = m_entry->totpStep(); - uCounter = resetCounter(); + m_step = m_entry->totpSettings()->step; + resetCounter(); updateProgressBar(); - connect(m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateProgressBar())); - connect(m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateSeconds())); - m_totpUpdateTimer->start(m_step * 10); + connect(&m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateProgressBar())); + connect(&m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateSeconds())); + m_totpUpdateTimer.start(m_step * 10); updateTotp(); setAttribute(Qt::WA_DeleteOnClose); @@ -47,6 +51,10 @@ TotpDialog::TotpDialog(DatabaseWidget* parent, Entry* entry) connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(copyToClipboard())); } +TotpDialog::~TotpDialog() +{ +} + void TotpDialog::copyToClipboard() { clipboard()->setText(m_entry->totp()); @@ -57,13 +65,13 @@ void TotpDialog::copyToClipboard() void TotpDialog::updateProgressBar() { - if (uCounter < 100) { - m_ui->progressBar->setValue(static_cast(100 - uCounter)); + if (m_counter < 100) { + m_ui->progressBar->setValue(100 - m_counter); m_ui->progressBar->update(); - uCounter++; + ++m_counter; } else { updateTotp(); - uCounter = resetCounter(); + resetCounter(); } } @@ -81,16 +89,8 @@ void TotpDialog::updateTotp() m_ui->totpLabel->setText(firstHalf + " " + secondHalf); } -double TotpDialog::resetCounter() +void TotpDialog::resetCounter() { uint epoch = QDateTime::currentDateTime().toTime_t(); - double counter = qRound(static_cast(epoch % m_step) / m_step * 100); - return counter; -} - -TotpDialog::~TotpDialog() -{ - if (m_totpUpdateTimer) { - delete m_totpUpdateTimer; - } + m_counter = static_cast(static_cast(epoch % m_step) / m_step * 100); } diff --git a/src/gui/TotpDialog.h b/src/gui/TotpDialog.h index e002cb82a..0ab035185 100644 --- a/src/gui/TotpDialog.h +++ b/src/gui/TotpDialog.h @@ -37,24 +37,23 @@ class TotpDialog : public QDialog Q_OBJECT public: - explicit TotpDialog(DatabaseWidget* parent = nullptr, Entry* entry = nullptr); - ~TotpDialog(); - -private: - double uCounter; - quint8 m_step = Totp::defaultStep; - QScopedPointer m_ui; - QPointer m_totpUpdateTimer; + explicit TotpDialog(QWidget* parent = nullptr, Entry* entry = nullptr); + ~TotpDialog() override; private Q_SLOTS: void updateTotp(); void updateProgressBar(); void updateSeconds(); void copyToClipboard(); - double resetCounter(); -protected: +private: + QScopedPointer m_ui; + + void resetCounter(); Entry* m_entry; + int m_counter; + uint m_step; + QTimer m_totpUpdateTimer; }; #endif // KEEPASSX_TOTPDIALOG_H diff --git a/src/gui/TotpSetupDialog.cpp b/src/gui/TotpSetupDialog.cpp new file mode 100644 index 000000000..69290e7a3 --- /dev/null +++ b/src/gui/TotpSetupDialog.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2017 Weslly Honorato <weslly@protonmail.com> + * Copyright (C) 2017 KeePassXC Team + * + * 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 2 or (at your option) + * 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 . + */ + +#include "TotpSetupDialog.h" +#include "totp/totp.h" +#include "ui_TotpSetupDialog.h" + +TotpSetupDialog::TotpSetupDialog(QWidget* parent, Entry* entry) + : QDialog(parent) + , m_ui(new Ui::TotpSetupDialog()) + , m_entry(entry) +{ + m_ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); + setFixedSize(sizeHint()); + + connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(close())); + connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(saveSettings())); + connect(m_ui->radioCustom, SIGNAL(toggled(bool)), SLOT(toggleCustom(bool))); + + init(); +} + +TotpSetupDialog::~TotpSetupDialog() +{ +} + +void TotpSetupDialog::saveSettings() +{ + QString encShortName; + uint digits = Totp::DEFAULT_DIGITS; + if (m_ui->radio8Digits->isChecked()) { + digits = 8; + } else if (m_ui->radioSteam->isChecked()) { + digits = Totp::STEAM_DIGITS; + encShortName = Totp::STEAM_SHORTNAME; + } + + auto settings = Totp::createSettings(m_ui->seedEdit->text(), digits, m_ui->stepSpinBox->value(), encShortName); + m_entry->setTotp(settings); + emit totpUpdated(); + close(); +} + +void TotpSetupDialog::toggleCustom(bool status) +{ + m_ui->customGroup->setEnabled(status); +} + +void TotpSetupDialog::init() +{ + auto settings = m_entry->totpSettings(); + if (!settings.isNull()) { + m_ui->seedEdit->setText(settings->key); + m_ui->stepSpinBox->setValue(settings->step); + + if (settings->encoder.shortName == Totp::STEAM_SHORTNAME) { + m_ui->radioSteam->setChecked(true); + } else if (settings->custom) { + m_ui->radioCustom->setChecked(true); + if (settings->digits == 8) { + m_ui->radio8Digits->setChecked(true); + } else { + m_ui->radio6Digits->setChecked(true); + } + } + } +} diff --git a/src/gui/SetupTotpDialog.h b/src/gui/TotpSetupDialog.h similarity index 68% rename from src/gui/SetupTotpDialog.h rename to src/gui/TotpSetupDialog.h index 7c34bb5f1..905b2a2ca 100644 --- a/src/gui/SetupTotpDialog.h +++ b/src/gui/TotpSetupDialog.h @@ -27,33 +27,28 @@ namespace Ui { - class SetupTotpDialog; + class TotpSetupDialog; } -class SetupTotpDialog : public QDialog +class TotpSetupDialog : public QDialog { Q_OBJECT public: - explicit SetupTotpDialog(DatabaseWidget* parent = nullptr, Entry* entry = nullptr); - ~SetupTotpDialog(); - void setSeed(QString value); - void setStep(quint8 step); - void setDigits(quint8 digits); - void setSettings(quint8 digits); + explicit TotpSetupDialog(QWidget* parent = nullptr, Entry* entry = nullptr); + ~TotpSetupDialog() override; + void init(); -private Q_SLOTS: - void toggleDefault(bool status); - void toggleSteam(bool status); +signals: + void totpUpdated(); + +private slots: void toggleCustom(bool status); - void setupTotp(); + void saveSettings(); private: - QScopedPointer m_ui; - -protected: + QScopedPointer m_ui; Entry* m_entry; - DatabaseWidget* m_parent; }; #endif // KEEPASSX_SETUPTOTPDIALOG_H diff --git a/src/gui/TotpSetupDialog.ui b/src/gui/TotpSetupDialog.ui new file mode 100644 index 000000000..7b3e9318b --- /dev/null +++ b/src/gui/TotpSetupDialog.ui @@ -0,0 +1,182 @@ + + + TotpSetupDialog + + + + 0 + 0 + 249 + 248 + + + + Setup TOTP + + + + + + 5 + + + 5 + + + + + Key: + + + + + + + + + + + + border:none + + + + + + false + + + false + + + + + + Default RFC 6238 token settings + + + true + + + settingsButtonGroup + + + + + + + Steam token settings + + + settingsButtonGroup + + + + + + + Use custom settings + + + settingsButtonGroup + + + + + + + + + + false + + + + + + Custom Settings + + + + QFormLayout::ExpandingFieldsGrow + + + QFormLayout::DontWrapRows + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + 5 + + + 5 + + + + + Time step: + + + + + + + sec + + + 1 + + + 60 + + + 30 + + + + + + + Code size: + + + + + + + 6 digits + + + true + + + + + + + 8 digits + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp index d3859e498..fe1f70476 100644 --- a/src/gui/entry/EntryModel.cpp +++ b/src/gui/entry/EntryModel.cpp @@ -127,7 +127,7 @@ int EntryModel::columnCount(const QModelIndex& parent) const return 0; } - return 12; + return 13; } QVariant EntryModel::data(const QModelIndex& index, int role) const @@ -201,16 +201,20 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const case Accessed: result = entry->timeInfo().lastAccessTime().toLocalTime().toString(EntryModel::DateFormat); return result; - case Attachments: - // Display comma-separated list of attachments - QList attachments = entry->attachments()->keys(); - for (int i = 0; i < attachments.size(); ++i) { - if (result.isEmpty()) { - result.append(attachments.at(i)); - continue; + case Attachments: { + // Display comma-separated list of attachments + QList attachments = entry->attachments()->keys(); + for (int i = 0; i < attachments.size(); ++i) { + if (result.isEmpty()) { + result.append(attachments.at(i)); + continue; + } + result.append(QString(", ") + attachments.at(i)); } - result.append(QString(", ") + attachments.at(i)); + return result; } + case Totp: + result = entry->hasTotp() ? tr("Yes") : ""; return result; } } else if (role == Qt::UserRole) { // Qt::UserRole is used as sort role, see EntryView::EntryView() @@ -309,6 +313,8 @@ QVariant EntryModel::headerData(int section, Qt::Orientation orientation, int ro return tr("Accessed"); case Attachments: return tr("Attachments"); + case Totp: + return tr("TOTP"); } } else if (role == Qt::DecorationRole) { if (section == Paperclip) { diff --git a/src/gui/entry/EntryModel.h b/src/gui/entry/EntryModel.h index 1965066a1..e8c90f7e4 100644 --- a/src/gui/entry/EntryModel.h +++ b/src/gui/entry/EntryModel.h @@ -42,7 +42,8 @@ public: Modified = 8, Accessed = 9, Paperclip = 10, - Attachments = 11 + Attachments = 11, + Totp = 12 }; explicit EntryModel(QObject* parent = nullptr); diff --git a/src/totp/totp.cpp b/src/totp/totp.cpp index d187bd0f6..286d383ab 100644 --- a/src/totp/totp.cpp +++ b/src/totp/totp.cpp @@ -18,6 +18,7 @@ #include "totp.h" #include "core/Base32.h" + #include #include #include @@ -28,114 +29,107 @@ #include #include -const quint8 Totp::defaultStep = 30; -const quint8 Totp::defaultDigits = 6; - -/** - * Custom encoder types. Each should be unique and >= 128 and < 255 - * Values have no meaning outside of keepassxc - */ -/** - * Encoder for Steam Guard TOTP - */ -const quint8 Totp::ENCODER_STEAM = 254; - -const Totp::Encoder Totp::defaultEncoder = {"", "", "0123456789", 0, 0, false}; -const QMap Totp::encoders{ - {Totp::ENCODER_STEAM, {"steam", "S", "23456789BCDFGHJKMNPQRTVWXY", 5, 30, true}}, +static QList encoders { + {"", "", "0123456789", Totp::DEFAULT_DIGITS, Totp::DEFAULT_STEP, false}, + {"steam", Totp::STEAM_SHORTNAME, "23456789BCDFGHJKMNPQRTVWXY", Totp::STEAM_DIGITS, Totp::DEFAULT_STEP, true}, }; -/** - * These map the second field of the "TOTP Settings" field to our internal encoder number - * that overloads the digits field. Make sure that the key matches the shortName value - * in the corresponding Encoder - * NOTE: when updating this map, a corresponding edit to the settings regex must be made - * in Entry::totpSeed() - */ -const QMap Totp::shortNameToEncoder{ - {"S", Totp::ENCODER_STEAM}, -}; -/** - * These map the "encoder=" URL parameter of the "otp" field to our internal encoder number - * that overloads the digits field. Make sure that the key matches the name value - * in the corresponding Encoder - */ -const QMap Totp::nameToEncoder{ - {"steam", Totp::ENCODER_STEAM}, -}; - -Totp::Totp() +QSharedPointer Totp::parseSettings(const QString& rawSettings, const QString& key) { -} + // Create default settings + auto settings = createSettings(key, DEFAULT_DIGITS, DEFAULT_STEP); -QString Totp::parseOtpString(QString key, quint8& digits, quint8& step) -{ - QUrl url(key); - - QString seed; - uint q_digits, q_step; - - // Default OTP url format + QUrl url(rawSettings); if (url.isValid() && url.scheme() == "otpauth") { + // Default OTP url format QUrlQuery query(url); - - seed = query.queryItemValue("secret"); - - q_digits = query.queryItemValue("digits").toUInt(); - if (q_digits == 6 || q_digits == 8) { - digits = q_digits; - } - - q_step = query.queryItemValue("period").toUInt(); - if (q_step > 0 && q_step <= 60) { - step = q_step; - } - QString encName = query.queryItemValue("encoder"); - if (!encName.isEmpty() && nameToEncoder.contains(encName)) { - digits = nameToEncoder[encName]; + settings->otpUrl = true; + settings->key = query.queryItemValue("secret"); + settings->digits = query.queryItemValue("digits").toUInt(); + settings->step = query.queryItemValue("period").toUInt(); + if (query.hasQueryItem("encoder")) { + settings->encoder = getEncoderByName(query.queryItemValue("encoder")); } } else { - // Compatibility with "KeeOtp" plugin string format - QRegExp rx("key=(.+)", Qt::CaseInsensitive, QRegExp::RegExp); - - if (rx.exactMatch(key)) { - QUrlQuery query(key); - - seed = query.queryItemValue("key"); - q_digits = query.queryItemValue("size").toUInt(); - if (q_digits == 6 || q_digits == 8) { - digits = q_digits; - } - - q_step = query.queryItemValue("step").toUInt(); - if (q_step > 0 && q_step <= 60) { - step = q_step; - } - + QUrlQuery query(rawSettings); + if (query.hasQueryItem("key")) { + // Compatibility with "KeeOtp" plugin + // if settings are changed, will convert to semi-colon format + settings->key = query.queryItemValue("key"); + settings->digits = query.queryItemValue("size").toUInt(); + settings->step = query.queryItemValue("step").toUInt(); } else { - seed = key; + // Parse semi-colon separated values ([step];[digits|S]) + auto vars = rawSettings.split(";"); + if (vars.size() >= 2) { + if (vars[1] == STEAM_SHORTNAME) { + // Explicit steam encoder + settings->encoder = steamEncoder(); + } else { + // Extract step and digits + settings->step = vars[0].toUInt(); + settings->digits = vars[1].toUInt(); + } + } } } - if (digits == 0) { - digits = defaultDigits; + // Bound digits and step + settings->digits = qMax(1u, settings->digits); + settings->step = qBound(1u, settings->step, 60u); + + // Detect custom settings, used by setup GUI + if (settings->encoder.shortName != STEAM_SHORTNAME + && (settings->digits != DEFAULT_DIGITS || settings->step != DEFAULT_STEP)) { + settings->custom = true; } - if (step == 0) { - step = defaultStep; - } - - return seed; + return settings; } -QString Totp::generateTotp(const QByteArray key, - quint64 time, - const quint8 numDigits = defaultDigits, - const quint8 step = defaultStep) +QSharedPointer Totp::createSettings(const QString& key, const uint digits, const uint step, + const QString& encoderShortName) { - quint64 current = qToBigEndian(time / step); + bool isCustom = digits != DEFAULT_DIGITS || step != DEFAULT_STEP; + return QSharedPointer(new Totp::Settings { + getEncoderByShortName(encoderShortName), key, false, isCustom, digits, step + }); +} - QVariant secret = Base32::decode(Base32::sanitizeInput(key)); +QString Totp::writeSettings(const QSharedPointer settings) +{ + // OTP Url output + if (settings->otpUrl) { + auto urlstring = QString("key=%1&step=%2&size=%3").arg(settings->key).arg(settings->step).arg(settings->digits); + if (!settings->encoder.name.isEmpty()) { + urlstring.append("&encoder=").append(settings->encoder.name); + } + return urlstring; + } + + // Semicolon output [step];[encoder] + if (!settings->encoder.shortName.isEmpty()) { + return QString("%1;%2").arg(settings->step).arg(settings->encoder.shortName); + } + + // Semicolon output [step];[digits] + return QString("%1;%2").arg(settings->step).arg(settings->digits); +} + +QString Totp::generateTotp(const QSharedPointer settings, const quint64 time) +{ + const Encoder& encoder = settings->encoder; + uint step = settings->custom ? settings->step : encoder.step; + uint digits = settings->custom ? settings->digits : encoder.digits; + + quint64 current; + if (time == 0) { + current = qToBigEndian(QDateTime::currentDateTime().toTime_t() / step); + } else { + current = qToBigEndian(time / step); + } + + QVariant secret = Base32::decode(Base32::sanitizeInput(settings->key.toLatin1())); if (secret.isNull()) { return "Invalid TOTP secret key"; } @@ -155,9 +149,6 @@ QString Totp::generateTotp(const QByteArray key, | (hmac[offset + 3] & 0xff); // clang-format on - const Encoder& encoder = encoders.value(numDigits, defaultEncoder); - // if encoder.digits is 0, we need to use the passed-in number of digits (default encoder) - quint8 digits = encoder.digits == 0 ? numDigits : encoder.digits; int direction = -1; int startpos = digits - 1; if (encoder.reverse) { @@ -175,26 +166,34 @@ QString Totp::generateTotp(const QByteArray key, return retval; } -// See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format -QUrl Totp::generateOtpString(const QString& secret, - const QString& type, - const QString& issuer, - const QString& username, - const QString& algorithm, - quint8 digits, - quint8 step) +Totp::Encoder& Totp::defaultEncoder() { - QUrl keyUri; - keyUri.setScheme("otpauth"); - keyUri.setHost(type); - keyUri.setPath(QString("/%1:%2").arg(issuer).arg(username)); - QUrlQuery parameters; - parameters.addQueryItem("secret", secret); - parameters.addQueryItem("issuer", issuer); - parameters.addQueryItem("algorithm", algorithm); - parameters.addQueryItem("digits", QString::number(digits)); - parameters.addQueryItem("period", QString::number(step)); - keyUri.setQuery(parameters); - - return keyUri; + // The first encoder is always the default + Q_ASSERT(!encoders.empty()); + return encoders[0]; +} + +Totp::Encoder& Totp::steamEncoder() +{ + return getEncoderByShortName("S"); +} + +Totp::Encoder& Totp::getEncoderByShortName(QString shortName) +{ + for (auto& encoder : encoders) { + if (encoder.shortName == shortName) { + return encoder; + } + } + return defaultEncoder(); +} + +Totp::Encoder& Totp::getEncoderByName(QString name) +{ + for (auto& encoder : encoders) { + if (encoder.name == name) { + return encoder; + } + } + return defaultEncoder(); } diff --git a/src/totp/totp.h b/src/totp/totp.h index 1c159e290..b4a592918 100644 --- a/src/totp/totp.h +++ b/src/totp/totp.h @@ -22,39 +22,52 @@ #include #include #include +#include class QUrl; -class Totp +namespace Totp { + +struct Encoder { -public: - Totp(); - static QString parseOtpString(QString rawSecret, quint8& digits, quint8& step); - static QString generateTotp(const QByteArray key, quint64 time, const quint8 numDigits, const quint8 step); - static QUrl generateOtpString(const QString& secret, - const QString& type, - const QString& issuer, - const QString& username, - const QString& algorithm, - quint8 digits, - quint8 step); - static const quint8 defaultStep; - static const quint8 defaultDigits; - struct Encoder - { - QString name; - QString shortName; - QString alphabet; - quint8 digits; - quint8 step; - bool reverse; - }; - static const Encoder defaultEncoder; - // custom encoder values that overload the digits field - static const quint8 ENCODER_STEAM; - static const QMap encoders; - static const QMap shortNameToEncoder; - static const QMap nameToEncoder; + QString name; + QString shortName; + QString alphabet; + uint digits; + uint step; + bool reverse; }; +struct Settings +{ + Totp::Encoder encoder; + QString key; + bool otpUrl; + bool custom; + uint digits; + uint step; +}; + +constexpr uint DEFAULT_STEP = 30u; +constexpr uint DEFAULT_DIGITS = 6u; +constexpr uint STEAM_DIGITS = 5u; +static const QString STEAM_SHORTNAME = "S"; + +static const QString ATTRIBUTE_OTP = "otp"; +static const QString ATTRIBUTE_SEED = "TOTP Seed"; +static const QString ATTRIBUTE_SETTINGS = "TOTP Settings"; + +QSharedPointer parseSettings(const QString& rawSettings, const QString& key = {}); +QSharedPointer createSettings(const QString& key, const uint digits, const uint step, + const QString& encoderShortName = {}); +QString writeSettings(const QSharedPointer settings); + +QString generateTotp(const QSharedPointer settings, const quint64 time = 0ull); + +Encoder& defaultEncoder(); +Encoder& steamEncoder(); +Encoder& getEncoderByShortName(QString shortName); +Encoder& getEncoderByName(QString name); +} + #endif // QTOTP_H diff --git a/tests/TestEntryModel.cpp b/tests/TestEntryModel.cpp index 3378a9f9a..49939e256 100644 --- a/tests/TestEntryModel.cpp +++ b/tests/TestEntryModel.cpp @@ -291,11 +291,11 @@ void TestEntryModel::testProxyModel() * @author Fonic * Update comparison value of modelProxy->columnCount() to account for * additional columns 'Password', 'Notes', 'Expires', 'Created', 'Modified', - * 'Accessed', 'Paperclip' and 'Attachments' + * 'Accessed', 'Paperclip', 'Attachments', and TOTP */ QSignalSpy spyColumnRemove(modelProxy, SIGNAL(columnsAboutToBeRemoved(QModelIndex, int, int))); modelProxy->hideColumn(0, true); - QCOMPARE(modelProxy->columnCount(), 11); + QCOMPARE(modelProxy->columnCount(), 12); QVERIFY(spyColumnRemove.size() >= 1); int oldSpyColumnRemoveSize = spyColumnRemove.size(); @@ -313,11 +313,11 @@ void TestEntryModel::testProxyModel() * @author Fonic * Update comparison value of modelProxy->columnCount() to account for * additional columns 'Password', 'Notes', 'Expires', 'Created', 'Modified', - * 'Accessed', 'Paperclip' and 'Attachments' + * 'Accessed', 'Paperclip', 'Attachments', and TOTP */ QSignalSpy spyColumnInsert(modelProxy, SIGNAL(columnsAboutToBeInserted(QModelIndex, int, int))); modelProxy->hideColumn(0, false); - QCOMPARE(modelProxy->columnCount(), 12); + QCOMPARE(modelProxy->columnCount(), 13); QVERIFY(spyColumnInsert.size() >= 1); int oldSpyColumnInsertSize = spyColumnInsert.size(); diff --git a/tests/TestTotp.cpp b/tests/TestTotp.cpp index a91ae00b3..f4de2c6ad 100644 --- a/tests/TestTotp.cpp +++ b/tests/TestTotp.cpp @@ -31,138 +31,107 @@ void TestTotp::initTestCase() void TestTotp::testParseSecret() { - quint8 digits = 0; - quint8 step = 0; + // OTP URL Parsing QString secret = "otpauth://totp/" "ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=" "SHA1&digits=6&period=30"; - QCOMPARE(Totp::parseOtpString(secret, digits, step), QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ")); - QCOMPARE(digits, quint8(6)); - QCOMPARE(step, quint8(30)); + auto settings = Totp::parseSettings(secret); + QVERIFY(!settings.isNull()); + QCOMPARE(settings->key, QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ")); + QCOMPARE(settings->custom, false); + QCOMPARE(settings->digits, 6u); + QCOMPARE(settings->step, 30u); - digits = Totp::defaultDigits; - step = Totp::defaultStep; + // KeeOTP Parsing secret = "key=HXDMVJECJJWSRBY%3d&step=25&size=8"; - QCOMPARE(Totp::parseOtpString(secret, digits, step), QString("HXDMVJECJJWSRBY=")); - QCOMPARE(digits, quint8(8)); - QCOMPARE(step, quint8(25)); + settings = Totp::parseSettings(secret); + QVERIFY(!settings.isNull()); + QCOMPARE(settings->key, QString("HXDMVJECJJWSRBY=")); + QCOMPARE(settings->custom, true); + QCOMPARE(settings->digits, 8u); + QCOMPARE(settings->step, 25u); - digits = 0; - step = 0; + // Semi-colon delineated "TOTP Settings" secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq"; - QCOMPARE(Totp::parseOtpString(secret, digits, step), QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq")); - QCOMPARE(digits, quint8(6)); - QCOMPARE(step, quint8(30)); + settings = Totp::parseSettings("30;8", secret); + QVERIFY(!settings.isNull()); + QCOMPARE(settings->key, QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq")); + QCOMPARE(settings->custom, true); + QCOMPARE(settings->digits, 8u); + QCOMPARE(settings->step, 30u); + + // Bare secret (no "TOTP Settings" attribute) + secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq"; + settings = Totp::parseSettings("", secret); + QVERIFY(!settings.isNull()); + QCOMPARE(settings->key, QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq")); + QCOMPARE(settings->custom, false); + QCOMPARE(settings->digits, 6u); + QCOMPARE(settings->step, 30u); } void TestTotp::testTotpCode() { // Test vectors from RFC 6238 // https://tools.ietf.org/html/rfc6238#appendix-B + auto settings = Totp::createSettings("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", Totp::DEFAULT_DIGITS, Totp::DEFAULT_STEP); - QByteArray seed = QString("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ").toLatin1(); - + // Test 6 digit TOTP (default) quint64 time = 1234567890; - QString output = Totp::generateTotp(seed, time, 6, 30); - QCOMPARE(output, QString("005924")); + QCOMPARE(Totp::generateTotp(settings, time), QString("005924")); time = 1111111109; - output = Totp::generateTotp(seed, time, 6, 30); - QCOMPARE(output, QString("081804")); + QCOMPARE(Totp::generateTotp(settings, time), QString("081804")); + // Test 8 digit TOTP (custom) + settings->digits = 8; + settings->custom = true; time = 1111111111; - output = Totp::generateTotp(seed, time, 8, 30); - QCOMPARE(output, QString("14050471")); + QCOMPARE(Totp::generateTotp(settings, time), QString("14050471")); time = 2000000000; - output = Totp::generateTotp(seed, time, 8, 30); - QCOMPARE(output, QString("69279037")); -} - -void TestTotp::testEncoderData() -{ - for (quint8 key : Totp::encoders.keys()) { - const Totp::Encoder& enc = Totp::encoders.value(key); - QVERIFY2( - enc.digits != 0, - qPrintable( - QString("Custom encoders cannot have zero-value for digits field: %1(%2)").arg(enc.name).arg(key))); - QVERIFY2(!enc.name.isEmpty(), - qPrintable(QString("Custom encoders must have a name: %1(%2)").arg(enc.name).arg(key))); - QVERIFY2(!enc.shortName.isEmpty(), - qPrintable(QString("Custom encoders must have a shortName: %1(%2)").arg(enc.name).arg(key))); - QVERIFY2(Totp::shortNameToEncoder.contains(enc.shortName), - qPrintable(QString("No shortNameToEncoder entry found for custom encoder: %1(%2) %3") - .arg(enc.name) - .arg(key) - .arg(enc.shortName))); - QVERIFY2(Totp::shortNameToEncoder[enc.shortName] == key, - qPrintable(QString("shortNameToEncoder doesn't reference this custome encoder: %1(%2) %3") - .arg(enc.name) - .arg(key) - .arg(enc.shortName))); - QVERIFY2(Totp::nameToEncoder.contains(enc.name), - qPrintable(QString("No nameToEncoder entry found for custom encoder: %1(%2) %3") - .arg(enc.name) - .arg(key) - .arg(enc.shortName))); - QVERIFY2(Totp::nameToEncoder[enc.name] == key, - qPrintable(QString("nameToEncoder doesn't reference this custome encoder: %1(%2) %3") - .arg(enc.name) - .arg(key) - .arg(enc.shortName))); - } - - for (const QString& key : Totp::nameToEncoder.keys()) { - quint8 value = Totp::nameToEncoder.value(key); - QVERIFY2(Totp::encoders.contains(value), - qPrintable(QString("No custom encoder found for encoder named %1(%2)").arg(value).arg(key))); - QVERIFY2(Totp::encoders[value].name == key, - qPrintable( - QString("nameToEncoder doesn't reference the right custom encoder: %1(%2)").arg(value).arg(key))); - } - - for (const QString& key : Totp::shortNameToEncoder.keys()) { - quint8 value = Totp::shortNameToEncoder.value(key); - QVERIFY2(Totp::encoders.contains(value), - qPrintable(QString("No custom encoder found for short-name encoder %1(%2)").arg(value).arg(key))); - QVERIFY2( - Totp::encoders[value].shortName == key, - qPrintable( - QString("shortNameToEncoder doesn't reference the right custom encoder: %1(%2)").arg(value).arg(key))); - } + QCOMPARE(Totp::generateTotp(settings, time), QString("69279037")); } void TestTotp::testSteamTotp() { - quint8 digits = 0; - quint8 step = 0; + // OTP URL Parsing QString secret = "otpauth://totp/" "test:test@example.com?secret=63BEDWCQZKTQWPESARIERL5DTTQFCJTK&issuer=Valve&algorithm=" "SHA1&digits=5&period=30&encoder=steam"; - QCOMPARE(Totp::parseOtpString(secret, digits, step), QString("63BEDWCQZKTQWPESARIERL5DTTQFCJTK")); - QCOMPARE(digits, quint8(Totp::ENCODER_STEAM)); - QCOMPARE(step, quint8(30)); + auto settings = Totp::parseSettings(secret); - QByteArray seed = QString("63BEDWCQZKTQWPESARIERL5DTTQFCJTK").toLatin1(); + QCOMPARE(settings->key, QString("63BEDWCQZKTQWPESARIERL5DTTQFCJTK")); + QCOMPARE(settings->encoder.shortName, Totp::STEAM_SHORTNAME); + QCOMPARE(settings->digits, Totp::STEAM_DIGITS); + QCOMPARE(settings->step, 30u); // These time/value pairs were created by running the Steam Guard function of the // Steam mobile app with a throw-away steam account. The above secret was extracted // from the Steam app's data for use in testing here. quint64 time = 1511200518; - QCOMPARE(Totp::generateTotp(seed, time, Totp::ENCODER_STEAM, 30), QString("FR8RV")); + QCOMPARE(Totp::generateTotp(settings, time), QString("FR8RV")); time = 1511200714; - QCOMPARE(Totp::generateTotp(seed, time, Totp::ENCODER_STEAM, 30), QString("9P3VP")); + QCOMPARE(Totp::generateTotp(settings, time), QString("9P3VP")); } void TestTotp::testEntryHistory() { Entry entry; - quint8 step = 16; - quint8 digits = 6; + uint step = 16; + uint digits = 6; + auto settings = Totp::createSettings("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", digits, step); + // Test that entry starts without TOTP QCOMPARE(entry.historyItems().size(), 0); - entry.setTotp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", step, digits); + QVERIFY(!entry.hasTotp()); + // Add TOTP to entry + entry.setTotp(settings); QCOMPARE(entry.historyItems().size(), 1); - entry.setTotp("foo", step, digits); + QVERIFY(entry.hasTotp()); + QCOMPARE(entry.totpSettings()->key, QString("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ")); + // Change key and verify settings changed + settings->key = "foo"; + entry.setTotp(settings); QCOMPARE(entry.historyItems().size(), 2); + QCOMPARE(entry.totpSettings()->key, QString("foo")); } diff --git a/tests/TestTotp.h b/tests/TestTotp.h index 784eb8f22..92fa7a0e1 100644 --- a/tests/TestTotp.h +++ b/tests/TestTotp.h @@ -21,8 +21,6 @@ #include -class Totp; - class TestTotp : public QObject { Q_OBJECT @@ -31,7 +29,6 @@ private slots: void initTestCase(); void testParseSecret(); void testTotpCode(); - void testEncoderData(); void testSteamTotp(); void testEntryHistory(); }; diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 5833a241b..930dac561 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -56,7 +56,7 @@ #include "gui/MessageBox.h" #include "gui/PasswordEdit.h" #include "gui/SearchWidget.h" -#include "gui/SetupTotpDialog.h" +#include "gui/TotpSetupDialog.h" #include "gui/TotpDialog.h" #include "gui/entry/EditEntryWidget.h" #include "gui/entry/EntryView.h" @@ -636,7 +636,7 @@ void TestGui::testTotp() triggerAction("actionEntrySetupTotp"); - SetupTotpDialog* setupTotpDialog = m_dbWidget->findChild("SetupTotpDialog"); + TotpSetupDialog* setupTotpDialog = m_dbWidget->findChild("TotpSetupDialog"); Tools::wait(100);