Complete refactor of TOTP integration

* Eliminate TOTP logic from GUI elements
* Consolidate TOTP functionality under the Totp namespace
* Eliminate guessing about state and encoders
* Increased test cases
* Add entry view column for TOTP [#2132]
* General code cleanup, reduction of unnecessary steps, separation of concerns
* Rename SetupTotpDialog to TotpSetupDialog for consistency
This commit is contained in:
Jonathan White 2018-09-05 16:20:57 -04:00
parent b74fb3e208
commit 1dc9f10c7f
No known key found for this signature in database
GPG Key ID: 440FC65F2E0C6E01
21 changed files with 585 additions and 716 deletions

View File

@ -128,7 +128,7 @@ set(keepassx_SOURCES
gui/SettingsWidget.cpp gui/SettingsWidget.cpp
gui/SearchWidget.cpp gui/SearchWidget.cpp
gui/SortFilterHideProxyModel.cpp gui/SortFilterHideProxyModel.cpp
gui/SetupTotpDialog.cpp gui/TotpSetupDialog.cpp
gui/TotpDialog.cpp gui/TotpDialog.cpp
gui/UnlockDatabaseWidget.cpp gui/UnlockDatabaseWidget.cpp
gui/UnlockDatabaseDialog.cpp gui/UnlockDatabaseDialog.cpp

View File

@ -45,8 +45,6 @@ Entry::Entry()
m_data.iconNumber = DefaultIconNumber; m_data.iconNumber = DefaultIconNumber;
m_data.autoTypeEnabled = true; m_data.autoTypeEnabled = true;
m_data.autoTypeObfuscation = 0; 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()), SLOT(updateTotp()));
connect(m_attributes, SIGNAL(modified()), this, SIGNAL(modified())); connect(m_attributes, SIGNAL(modified()), this, SIGNAL(modified()));
@ -347,74 +345,45 @@ const CustomData* Entry::customData() const
bool Entry::hasTotp() const bool Entry::hasTotp() const
{ {
return m_attributes->hasKey("TOTP Seed") || m_attributes->hasKey("otp"); return !m_data.totpSettings.isNull();
} }
QString Entry::totp() const QString Entry::totp() const
{ {
if (hasTotp()) { if (hasTotp()) {
QString seed = totpSeed(); return Totp::generateTotp(m_data.totpSettings);
quint64 time = QDateTime::currentDateTime().toTime_t();
QString output = Totp::generateTotp(seed.toLatin1(), time, m_data.totpDigits, m_data.totpStep);
return QString(output);
} }
return {}; return {};
} }
void Entry::setTotp(const QString& seed, quint8& step, quint8& digits) void Entry::setTotp(QSharedPointer<Totp::Settings> settings)
{ {
beginUpdate(); beginUpdate();
if (step == 0) { m_data.totpSettings = settings;
step = Totp::defaultStep;
}
if (digits == 0) { auto text = Totp::writeSettings(m_data.totpSettings);
digits = Totp::defaultDigits; if (m_attributes->hasKey(Totp::ATTRIBUTE_OTP)) {
} m_attributes->set(Totp::ATTRIBUTE_OTP, text, true);
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);
} else { } else {
m_attributes->set("TOTP Seed", seed, true); m_attributes->set(Totp::ATTRIBUTE_SEED, m_data.totpSettings->key, true);
if (!enc.shortName.isEmpty()) { m_attributes->set(Totp::ATTRIBUTE_SETTINGS, text);
data = QString("%1;%2").arg(step).arg(enc.shortName);
} else {
data = QString("%1;%2").arg(step).arg(digits);
}
m_attributes->set("TOTP Settings", data);
} }
endUpdate(); endUpdate();
} }
QString Entry::totpSeed() const void Entry::updateTotp()
{ {
QString secret = ""; if (m_attributes->contains(Totp::ATTRIBUTE_SETTINGS)) {
m_data.totpSettings = Totp::parseSettings(m_attributes->value(Totp::ATTRIBUTE_SETTINGS),
if (m_attributes->hasKey("otp")) { m_attributes->value(Totp::ATTRIBUTE_SEED));
secret = m_attributes->value("otp"); } else if (m_attributes->contains(Totp::ATTRIBUTE_OTP)) {
} else if (m_attributes->hasKey("TOTP Seed")) { m_data.totpSettings = Totp::parseSettings(m_attributes->value(Totp::ATTRIBUTE_OTP));
secret = m_attributes->value("TOTP Seed");
} }
return Totp::parseOtpString(secret, m_data.totpDigits, m_data.totpStep);
} }
quint8 Entry::totpStep() const QSharedPointer<Totp::Settings> Entry::totpSettings() const
{ {
return m_data.totpStep; return m_data.totpSettings;
}
quint8 Entry::totpDigits() const
{
return m_data.totpDigits;
} }
void Entry::setUuid(const QUuid& uuid) void Entry::setUuid(const QUuid& uuid)
@ -725,33 +694,6 @@ void Entry::updateModifiedSinceBegin()
m_modifiedSinceBegin = true; 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<quint8>(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<quint8>(m.captured(2).toUInt());
}
}
QString Entry::resolveMultiplePlaceholdersRecursive(const QString& str, int maxDepth) const QString Entry::resolveMultiplePlaceholdersRecursive(const QString& str, int maxDepth) const
{ {
if (maxDepth <= 0) { if (maxDepth <= 0) {

View File

@ -36,6 +36,9 @@
class Database; class Database;
class Group; class Group;
namespace Totp {
struct Settings;
}
enum class EntryReferenceType enum class EntryReferenceType
{ {
@ -61,8 +64,7 @@ struct EntryData
int autoTypeObfuscation; int autoTypeObfuscation;
QString defaultAutoTypeSequence; QString defaultAutoTypeSequence;
TimeInfo timeInfo; TimeInfo timeInfo;
mutable quint8 totpDigits; QSharedPointer<Totp::Settings> totpSettings;
mutable quint8 totpStep;
}; };
class Entry : public QObject class Entry : public QObject
@ -98,9 +100,7 @@ public:
QString password() const; QString password() const;
QString notes() const; QString notes() const;
QString totp() const; QString totp() const;
QString totpSeed() const; QSharedPointer<Totp::Settings> totpSettings() const;
quint8 totpDigits() const;
quint8 totpStep() const;
bool hasTotp() const; bool hasTotp() const;
bool isExpired() const; bool isExpired() const;
@ -135,7 +135,7 @@ public:
void setNotes(const QString& notes); void setNotes(const QString& notes);
void setExpires(const bool& value); void setExpires(const bool& value);
void setExpiryTime(const QDateTime& dateTime); void setExpiryTime(const QDateTime& dateTime);
void setTotp(const QString& seed, quint8& step, quint8& digits); void setTotp(QSharedPointer<Totp::Settings> settings);
QList<Entry*> historyItems(); QList<Entry*> historyItems();
const QList<Entry*>& historyItems() const; const QList<Entry*>& historyItems() const;

View File

@ -47,7 +47,7 @@
#include "gui/DetailsWidget.h" #include "gui/DetailsWidget.h"
#include "gui/KeePass1OpenWidget.h" #include "gui/KeePass1OpenWidget.h"
#include "gui/MessageBox.h" #include "gui/MessageBox.h"
#include "gui/SetupTotpDialog.h" #include "gui/TotpSetupDialog.h"
#include "gui/TotpDialog.h" #include "gui/TotpDialog.h"
#include "gui/UnlockDatabaseDialog.h" #include "gui/UnlockDatabaseDialog.h"
#include "gui/UnlockDatabaseWidget.h" #include "gui/UnlockDatabaseWidget.h"
@ -444,15 +444,8 @@ void DatabaseWidget::setupTotp()
return; return;
} }
auto setupTotpDialog = new SetupTotpDialog(this, currentEntry); auto setupTotpDialog = new TotpSetupDialog(this, currentEntry);
if (currentEntry->hasTotp()) { connect(setupTotpDialog, SIGNAL(totpUpdated()), SIGNAL(entrySelectionChanged()));
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());
}
setupTotpDialog->open(); setupTotpDialog->open();
} }
@ -938,6 +931,13 @@ void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::Mod
openUrlForEntry(entry); openUrlForEntry(entry);
} }
break; break;
case EntryModel::Totp:
if (entry->hasTotp()) {
setClipboardTextAndMinimize(entry->totp());
} else {
setupTotp();
}
break;
// TODO: switch to 'Notes' tab in details view/pane // TODO: switch to 'Notes' tab in details view/pane
// case EntryModel::Notes: // case EntryModel::Notes:
// break; // break;

View File

@ -39,8 +39,6 @@ DetailsWidget::DetailsWidget(QWidget* parent)
, m_locked(false) , m_locked(false)
, m_currentEntry(nullptr) , m_currentEntry(nullptr)
, m_currentGroup(nullptr) , m_currentGroup(nullptr)
, m_step(0)
, m_totpTimer(nullptr)
, m_selectedTabEntry(0) , m_selectedTabEntry(0)
, m_selectedTabGroup(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->entryTotpButton, SIGNAL(toggled(bool)), m_ui->entryTotpWidget, SLOT(setVisible(bool)));
connect(m_ui->entryCloseButton, SIGNAL(toggled(bool)), SLOT(hide())); connect(m_ui->entryCloseButton, SIGNAL(toggled(bool)), SLOT(hide()));
connect(m_ui->entryTabWidget, SIGNAL(tabBarClicked(int)), SLOT(updateTabIndexes()), Qt::QueuedConnection); connect(m_ui->entryTabWidget, SIGNAL(tabBarClicked(int)), SLOT(updateTabIndexes()), Qt::QueuedConnection);
connect(&m_totpTimer, SIGNAL(timeout()), this, SLOT(updateTotpLabel()));
// Group // Group
m_ui->groupCloseButton->setIcon(filePath()->icon("actions", "dialog-close")); m_ui->groupCloseButton->setIcon(filePath()->icon("actions", "dialog-close"));
@ -65,7 +64,6 @@ DetailsWidget::DetailsWidget(QWidget* parent)
DetailsWidget::~DetailsWidget() DetailsWidget::~DetailsWidget()
{ {
deleteTotpTimer();
} }
void DetailsWidget::setEntry(Entry* selectedEntry) void DetailsWidget::setEntry(Entry* selectedEntry)
@ -146,16 +144,11 @@ void DetailsWidget::updateEntryTotp()
m_ui->entryTotpButton->setChecked(false); m_ui->entryTotpButton->setChecked(false);
if (hasTotp) { if (hasTotp) {
deleteTotpTimer(); m_totpTimer.start(1000);
m_totpTimer = new QTimer(m_currentEntry);
connect(m_totpTimer, SIGNAL(timeout()), this, SLOT(updateTotpLabel()));
m_totpTimer->start(1000);
m_step = m_currentEntry->totpStep();
updateTotpLabel(); updateTotpLabel();
} else { } else {
m_ui->entryTotpLabel->clear(); m_ui->entryTotpLabel->clear();
stopTotpTimer(); m_totpTimer.stop();
} }
} }
@ -274,30 +267,16 @@ void DetailsWidget::updateGroupNotesTab()
m_ui->groupNotesEdit->setText(notes); 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() void DetailsWidget::updateTotpLabel()
{ {
if (!m_locked && m_currentEntry) { if (!m_locked && m_currentEntry && m_currentEntry->hasTotp()) {
const QString totpCode = m_currentEntry->totp(); const QString totpCode = m_currentEntry->totp();
const QString firstHalf = totpCode.left(totpCode.size() / 2); const QString firstHalf = totpCode.left(totpCode.size() / 2);
const QString secondHalf = totpCode.mid(totpCode.size() / 2); const QString secondHalf = totpCode.mid(totpCode.size() / 2);
m_ui->entryTotpLabel->setText(firstHalf + " " + secondHalf); m_ui->entryTotpLabel->setText(firstHalf + " " + secondHalf);
} else { } else {
m_ui->entryTotpLabel->clear(); m_ui->entryTotpLabel->clear();
stopTotpTimer(); m_totpTimer.stop();
} }
} }

View File

@ -33,7 +33,7 @@ class DetailsWidget : public QWidget
public: public:
explicit DetailsWidget(QWidget* parent = nullptr); explicit DetailsWidget(QWidget* parent = nullptr);
~DetailsWidget(); ~DetailsWidget() override;
public slots: public slots:
void setEntry(Entry* selectedEntry); void setEntry(Entry* selectedEntry);
@ -56,8 +56,6 @@ private slots:
void updateGroupGeneralTab(); void updateGroupGeneralTab();
void updateGroupNotesTab(); void updateGroupNotesTab();
void stopTotpTimer();
void deleteTotpTimer();
void updateTotpLabel(); void updateTotpLabel();
void updateTabIndexes(); void updateTabIndexes();
@ -71,8 +69,7 @@ private:
bool m_locked; bool m_locked;
Entry* m_currentEntry; Entry* m_currentEntry;
Group* m_currentGroup; Group* m_currentGroup;
quint8 m_step; QTimer m_totpTimer;
QPointer<QTimer> m_totpTimer;
quint8 m_selectedTabEntry; quint8 m_selectedTabEntry;
quint8 m_selectedTabGroup; quint8 m_selectedTabGroup;
}; };

View File

@ -1,126 +0,0 @@
/*
* Copyright (C) 2017 Weslly Honorato <weslly@protonmail.com>
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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()
{
}

View File

@ -1,167 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SetupTotpDialog</class>
<widget class="QDialog" name="SetupTotpDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>282</width>
<height>364</height>
</rect>
</property>
<property name="windowTitle">
<string>Setup TOTP</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Key:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="seedEdit"/>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QRadioButton" name="radioDefault">
<property name="text">
<string>Default RFC 6238 token settings</string>
</property>
<attribute name="buttonGroup">
<string notr="true">settingsButtonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioSteam">
<property name="text">
<string>Steam token settings</string>
</property>
<attribute name="buttonGroup">
<string notr="true">settingsButtonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioCustom">
<property name="text">
<string>Use custom settings</string>
</property>
<attribute name="buttonGroup">
<string notr="true">settingsButtonGroup</string>
</attribute>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Note: Change these settings only if you know what you are doing.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout_3">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
<property name="rowWrapPolicy">
<enum>QFormLayout::DontWrapRows</enum>
</property>
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
</property>
<item row="1" column="0">
<widget class="QLabel" name="stepLabel">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Time step:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QRadioButton" name="radio8Digits">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>8 digits</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QRadioButton" name="radio6Digits">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>6 digits</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="digitsLabel">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Code size:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="stepSpinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="suffix">
<string comment="Seconds"> sec</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>60</number>
</property>
<property name="value">
<number>30</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
<buttongroups>
<buttongroup name="settingsButtonGroup"/>
</buttongroups>
</ui>

View File

@ -22,21 +22,25 @@
#include "core/Config.h" #include "core/Config.h"
#include "gui/Clipboard.h" #include "gui/Clipboard.h"
TotpDialog::TotpDialog(DatabaseWidget* parent, Entry* entry) TotpDialog::TotpDialog(QWidget* parent, Entry* entry)
: QDialog(parent) : QDialog(parent)
, m_ui(new Ui::TotpDialog()) , m_ui(new Ui::TotpDialog())
, m_totpUpdateTimer(new QTimer(entry))
, m_entry(entry) , m_entry(entry)
{ {
if (!m_entry->hasTotp()) {
close();
return;
}
m_ui->setupUi(this); m_ui->setupUi(this);
m_step = m_entry->totpStep(); m_step = m_entry->totpSettings()->step;
uCounter = resetCounter(); resetCounter();
updateProgressBar(); updateProgressBar();
connect(m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateProgressBar())); connect(&m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateProgressBar()));
connect(m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateSeconds())); connect(&m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateSeconds()));
m_totpUpdateTimer->start(m_step * 10); m_totpUpdateTimer.start(m_step * 10);
updateTotp(); updateTotp();
setAttribute(Qt::WA_DeleteOnClose); setAttribute(Qt::WA_DeleteOnClose);
@ -47,6 +51,10 @@ TotpDialog::TotpDialog(DatabaseWidget* parent, Entry* entry)
connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(copyToClipboard())); connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(copyToClipboard()));
} }
TotpDialog::~TotpDialog()
{
}
void TotpDialog::copyToClipboard() void TotpDialog::copyToClipboard()
{ {
clipboard()->setText(m_entry->totp()); clipboard()->setText(m_entry->totp());
@ -57,13 +65,13 @@ void TotpDialog::copyToClipboard()
void TotpDialog::updateProgressBar() void TotpDialog::updateProgressBar()
{ {
if (uCounter < 100) { if (m_counter < 100) {
m_ui->progressBar->setValue(static_cast<int>(100 - uCounter)); m_ui->progressBar->setValue(100 - m_counter);
m_ui->progressBar->update(); m_ui->progressBar->update();
uCounter++; ++m_counter;
} else { } else {
updateTotp(); updateTotp();
uCounter = resetCounter(); resetCounter();
} }
} }
@ -81,16 +89,8 @@ void TotpDialog::updateTotp()
m_ui->totpLabel->setText(firstHalf + " " + secondHalf); m_ui->totpLabel->setText(firstHalf + " " + secondHalf);
} }
double TotpDialog::resetCounter() void TotpDialog::resetCounter()
{ {
uint epoch = QDateTime::currentDateTime().toTime_t(); uint epoch = QDateTime::currentDateTime().toTime_t();
double counter = qRound(static_cast<double>(epoch % m_step) / m_step * 100); m_counter = static_cast<int>(static_cast<double>(epoch % m_step) / m_step * 100);
return counter;
}
TotpDialog::~TotpDialog()
{
if (m_totpUpdateTimer) {
delete m_totpUpdateTimer;
}
} }

View File

@ -37,24 +37,23 @@ class TotpDialog : public QDialog
Q_OBJECT Q_OBJECT
public: public:
explicit TotpDialog(DatabaseWidget* parent = nullptr, Entry* entry = nullptr); explicit TotpDialog(QWidget* parent = nullptr, Entry* entry = nullptr);
~TotpDialog(); ~TotpDialog() override;
private:
double uCounter;
quint8 m_step = Totp::defaultStep;
QScopedPointer<Ui::TotpDialog> m_ui;
QPointer<QTimer> m_totpUpdateTimer;
private Q_SLOTS: private Q_SLOTS:
void updateTotp(); void updateTotp();
void updateProgressBar(); void updateProgressBar();
void updateSeconds(); void updateSeconds();
void copyToClipboard(); void copyToClipboard();
double resetCounter();
protected: private:
QScopedPointer<Ui::TotpDialog> m_ui;
void resetCounter();
Entry* m_entry; Entry* m_entry;
int m_counter;
uint m_step;
QTimer m_totpUpdateTimer;
}; };
#endif // KEEPASSX_TOTPDIALOG_H #endif // KEEPASSX_TOTPDIALOG_H

View File

@ -0,0 +1,83 @@
/*
* Copyright (C) 2017 Weslly Honorato <weslly@protonmail.com>
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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);
}
}
}
}

View File

@ -27,33 +27,28 @@
namespace Ui namespace Ui
{ {
class SetupTotpDialog; class TotpSetupDialog;
} }
class SetupTotpDialog : public QDialog class TotpSetupDialog : public QDialog
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit SetupTotpDialog(DatabaseWidget* parent = nullptr, Entry* entry = nullptr); explicit TotpSetupDialog(QWidget* parent = nullptr, Entry* entry = nullptr);
~SetupTotpDialog(); ~TotpSetupDialog() override;
void setSeed(QString value); void init();
void setStep(quint8 step);
void setDigits(quint8 digits);
void setSettings(quint8 digits);
private Q_SLOTS: signals:
void toggleDefault(bool status); void totpUpdated();
void toggleSteam(bool status);
private slots:
void toggleCustom(bool status); void toggleCustom(bool status);
void setupTotp(); void saveSettings();
private: private:
QScopedPointer<Ui::SetupTotpDialog> m_ui; QScopedPointer<Ui::TotpSetupDialog> m_ui;
protected:
Entry* m_entry; Entry* m_entry;
DatabaseWidget* m_parent;
}; };
#endif // KEEPASSX_SETUPTOTPDIALOG_H #endif // KEEPASSX_SETUPTOTPDIALOG_H

182
src/gui/TotpSetupDialog.ui Normal file
View File

@ -0,0 +1,182 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TotpSetupDialog</class>
<widget class="QDialog" name="TotpSetupDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>249</width>
<height>248</height>
</rect>
</property>
<property name="windowTitle">
<string>Setup TOTP</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Key:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="seedEdit"/>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="styleSheet">
<string notr="true">border:none</string>
</property>
<property name="title">
<string/>
</property>
<property name="flat">
<bool>false</bool>
</property>
<property name="checkable">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QRadioButton" name="radioDefault">
<property name="text">
<string>Default RFC 6238 token settings</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string notr="true">settingsButtonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioSteam">
<property name="text">
<string>Steam token settings</string>
</property>
<attribute name="buttonGroup">
<string notr="true">settingsButtonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioCustom">
<property name="text">
<string>Use custom settings</string>
</property>
<attribute name="buttonGroup">
<string notr="true">settingsButtonGroup</string>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="customGroup">
<property name="enabled">
<bool>false</bool>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="title">
<string>Custom Settings</string>
</property>
<layout class="QFormLayout" name="formLayout_3">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
<property name="rowWrapPolicy">
<enum>QFormLayout::DontWrapRows</enum>
</property>
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<item row="1" column="0">
<widget class="QLabel" name="stepLabel">
<property name="text">
<string>Time step:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="stepSpinBox">
<property name="suffix">
<string comment="Seconds"> sec</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>60</number>
</property>
<property name="value">
<number>30</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="digitsLabel">
<property name="text">
<string>Code size:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QRadioButton" name="radio6Digits">
<property name="text">
<string>6 digits</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QRadioButton" name="radio8Digits">
<property name="text">
<string>8 digits</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
<buttongroups>
<buttongroup name="settingsButtonGroup"/>
</buttongroups>
</ui>

View File

@ -127,7 +127,7 @@ int EntryModel::columnCount(const QModelIndex& parent) const
return 0; return 0;
} }
return 12; return 13;
} }
QVariant EntryModel::data(const QModelIndex& index, int role) const QVariant EntryModel::data(const QModelIndex& index, int role) const
@ -201,16 +201,20 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
case Accessed: case Accessed:
result = entry->timeInfo().lastAccessTime().toLocalTime().toString(EntryModel::DateFormat); result = entry->timeInfo().lastAccessTime().toLocalTime().toString(EntryModel::DateFormat);
return result; return result;
case Attachments: case Attachments: {
// Display comma-separated list of attachments // Display comma-separated list of attachments
QList<QString> attachments = entry->attachments()->keys(); QList<QString> attachments = entry->attachments()->keys();
for (int i = 0; i < attachments.size(); ++i) { for (int i = 0; i < attachments.size(); ++i) {
if (result.isEmpty()) { if (result.isEmpty()) {
result.append(attachments.at(i)); result.append(attachments.at(i));
continue; continue;
}
result.append(QString(", ") + attachments.at(i));
} }
result.append(QString(", ") + attachments.at(i)); return result;
} }
case Totp:
result = entry->hasTotp() ? tr("Yes") : "";
return result; return result;
} }
} else if (role == Qt::UserRole) { // Qt::UserRole is used as sort role, see EntryView::EntryView() } 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"); return tr("Accessed");
case Attachments: case Attachments:
return tr("Attachments"); return tr("Attachments");
case Totp:
return tr("TOTP");
} }
} else if (role == Qt::DecorationRole) { } else if (role == Qt::DecorationRole) {
if (section == Paperclip) { if (section == Paperclip) {

View File

@ -42,7 +42,8 @@ public:
Modified = 8, Modified = 8,
Accessed = 9, Accessed = 9,
Paperclip = 10, Paperclip = 10,
Attachments = 11 Attachments = 11,
Totp = 12
}; };
explicit EntryModel(QObject* parent = nullptr); explicit EntryModel(QObject* parent = nullptr);

View File

@ -18,6 +18,7 @@
#include "totp.h" #include "totp.h"
#include "core/Base32.h" #include "core/Base32.h"
#include <QCryptographicHash> #include <QCryptographicHash>
#include <QDateTime> #include <QDateTime>
#include <QMessageAuthenticationCode> #include <QMessageAuthenticationCode>
@ -28,114 +29,107 @@
#include <QtEndian> #include <QtEndian>
#include <cmath> #include <cmath>
const quint8 Totp::defaultStep = 30; static QList<Totp::Encoder> encoders {
const quint8 Totp::defaultDigits = 6; {"", "", "0123456789", Totp::DEFAULT_DIGITS, Totp::DEFAULT_STEP, false},
{"steam", Totp::STEAM_SHORTNAME, "23456789BCDFGHJKMNPQRTVWXY", Totp::STEAM_DIGITS, Totp::DEFAULT_STEP, true},
/**
* 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<quint8, Totp::Encoder> Totp::encoders{
{Totp::ENCODER_STEAM, {"steam", "S", "23456789BCDFGHJKMNPQRTVWXY", 5, 30, true}},
}; };
/** QSharedPointer<Totp::Settings> Totp::parseSettings(const QString& rawSettings, const QString& key)
* 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<QString, quint8> 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<QString, quint8> Totp::nameToEncoder{
{"steam", Totp::ENCODER_STEAM},
};
Totp::Totp()
{ {
} // Create default settings
auto settings = createSettings(key, DEFAULT_DIGITS, DEFAULT_STEP);
QString Totp::parseOtpString(QString key, quint8& digits, quint8& step) QUrl url(rawSettings);
{
QUrl url(key);
QString seed;
uint q_digits, q_step;
// Default OTP url format
if (url.isValid() && url.scheme() == "otpauth") { if (url.isValid() && url.scheme() == "otpauth") {
// Default OTP url format
QUrlQuery query(url); QUrlQuery query(url);
settings->otpUrl = true;
seed = query.queryItemValue("secret"); settings->key = query.queryItemValue("secret");
settings->digits = query.queryItemValue("digits").toUInt();
q_digits = query.queryItemValue("digits").toUInt(); settings->step = query.queryItemValue("period").toUInt();
if (q_digits == 6 || q_digits == 8) { if (query.hasQueryItem("encoder")) {
digits = q_digits; settings->encoder = getEncoderByName(query.queryItemValue("encoder"));
}
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];
} }
} else { } else {
// Compatibility with "KeeOtp" plugin string format QUrlQuery query(rawSettings);
QRegExp rx("key=(.+)", Qt::CaseInsensitive, QRegExp::RegExp); if (query.hasQueryItem("key")) {
// Compatibility with "KeeOtp" plugin
if (rx.exactMatch(key)) { // if settings are changed, will convert to semi-colon format
QUrlQuery query(key); settings->key = query.queryItemValue("key");
settings->digits = query.queryItemValue("size").toUInt();
seed = query.queryItemValue("key"); settings->step = query.queryItemValue("step").toUInt();
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;
}
} else { } 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) { // Bound digits and step
digits = defaultDigits; 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) { return settings;
step = defaultStep;
}
return seed;
} }
QString Totp::generateTotp(const QByteArray key, QSharedPointer<Totp::Settings> Totp::createSettings(const QString& key, const uint digits, const uint step,
quint64 time, const QString& encoderShortName)
const quint8 numDigits = defaultDigits,
const quint8 step = defaultStep)
{ {
quint64 current = qToBigEndian(time / step); bool isCustom = digits != DEFAULT_DIGITS || step != DEFAULT_STEP;
return QSharedPointer<Totp::Settings>(new Totp::Settings {
getEncoderByShortName(encoderShortName), key, false, isCustom, digits, step
});
}
QVariant secret = Base32::decode(Base32::sanitizeInput(key)); QString Totp::writeSettings(const QSharedPointer<Totp::Settings> 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<Totp::Settings> 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()) { if (secret.isNull()) {
return "Invalid TOTP secret key"; return "Invalid TOTP secret key";
} }
@ -155,9 +149,6 @@ QString Totp::generateTotp(const QByteArray key,
| (hmac[offset + 3] & 0xff); | (hmac[offset + 3] & 0xff);
// clang-format on // 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 direction = -1;
int startpos = digits - 1; int startpos = digits - 1;
if (encoder.reverse) { if (encoder.reverse) {
@ -175,26 +166,34 @@ QString Totp::generateTotp(const QByteArray key,
return retval; return retval;
} }
// See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format Totp::Encoder& Totp::defaultEncoder()
QUrl Totp::generateOtpString(const QString& secret,
const QString& type,
const QString& issuer,
const QString& username,
const QString& algorithm,
quint8 digits,
quint8 step)
{ {
QUrl keyUri; // The first encoder is always the default
keyUri.setScheme("otpauth"); Q_ASSERT(!encoders.empty());
keyUri.setHost(type); return encoders[0];
keyUri.setPath(QString("/%1:%2").arg(issuer).arg(username)); }
QUrlQuery parameters;
parameters.addQueryItem("secret", secret); Totp::Encoder& Totp::steamEncoder()
parameters.addQueryItem("issuer", issuer); {
parameters.addQueryItem("algorithm", algorithm); return getEncoderByShortName("S");
parameters.addQueryItem("digits", QString::number(digits)); }
parameters.addQueryItem("period", QString::number(step));
keyUri.setQuery(parameters); Totp::Encoder& Totp::getEncoderByShortName(QString shortName)
{
return keyUri; 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();
} }

View File

@ -22,39 +22,52 @@
#include <QMap> #include <QMap>
#include <QString> #include <QString>
#include <QtCore/qglobal.h> #include <QtCore/qglobal.h>
#include <QtCore/QSharedPointer>
class QUrl; class QUrl;
class Totp namespace Totp {
struct Encoder
{ {
public: QString name;
Totp(); QString shortName;
static QString parseOtpString(QString rawSecret, quint8& digits, quint8& step); QString alphabet;
static QString generateTotp(const QByteArray key, quint64 time, const quint8 numDigits, const quint8 step); uint digits;
static QUrl generateOtpString(const QString& secret, uint step;
const QString& type, bool reverse;
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<quint8, Encoder> encoders;
static const QMap<QString, quint8> shortNameToEncoder;
static const QMap<QString, quint8> nameToEncoder;
}; };
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<Totp::Settings> parseSettings(const QString& rawSettings, const QString& key = {});
QSharedPointer<Totp::Settings> createSettings(const QString& key, const uint digits, const uint step,
const QString& encoderShortName = {});
QString writeSettings(const QSharedPointer<Totp::Settings> settings);
QString generateTotp(const QSharedPointer<Totp::Settings> settings, const quint64 time = 0ull);
Encoder& defaultEncoder();
Encoder& steamEncoder();
Encoder& getEncoderByShortName(QString shortName);
Encoder& getEncoderByName(QString name);
}
#endif // QTOTP_H #endif // QTOTP_H

View File

@ -291,11 +291,11 @@ void TestEntryModel::testProxyModel()
* @author Fonic <https://github.com/fonic> * @author Fonic <https://github.com/fonic>
* Update comparison value of modelProxy->columnCount() to account for * Update comparison value of modelProxy->columnCount() to account for
* additional columns 'Password', 'Notes', 'Expires', 'Created', 'Modified', * additional columns 'Password', 'Notes', 'Expires', 'Created', 'Modified',
* 'Accessed', 'Paperclip' and 'Attachments' * 'Accessed', 'Paperclip', 'Attachments', and TOTP
*/ */
QSignalSpy spyColumnRemove(modelProxy, SIGNAL(columnsAboutToBeRemoved(QModelIndex, int, int))); QSignalSpy spyColumnRemove(modelProxy, SIGNAL(columnsAboutToBeRemoved(QModelIndex, int, int)));
modelProxy->hideColumn(0, true); modelProxy->hideColumn(0, true);
QCOMPARE(modelProxy->columnCount(), 11); QCOMPARE(modelProxy->columnCount(), 12);
QVERIFY(spyColumnRemove.size() >= 1); QVERIFY(spyColumnRemove.size() >= 1);
int oldSpyColumnRemoveSize = spyColumnRemove.size(); int oldSpyColumnRemoveSize = spyColumnRemove.size();
@ -313,11 +313,11 @@ void TestEntryModel::testProxyModel()
* @author Fonic <https://github.com/fonic> * @author Fonic <https://github.com/fonic>
* Update comparison value of modelProxy->columnCount() to account for * Update comparison value of modelProxy->columnCount() to account for
* additional columns 'Password', 'Notes', 'Expires', 'Created', 'Modified', * additional columns 'Password', 'Notes', 'Expires', 'Created', 'Modified',
* 'Accessed', 'Paperclip' and 'Attachments' * 'Accessed', 'Paperclip', 'Attachments', and TOTP
*/ */
QSignalSpy spyColumnInsert(modelProxy, SIGNAL(columnsAboutToBeInserted(QModelIndex, int, int))); QSignalSpy spyColumnInsert(modelProxy, SIGNAL(columnsAboutToBeInserted(QModelIndex, int, int)));
modelProxy->hideColumn(0, false); modelProxy->hideColumn(0, false);
QCOMPARE(modelProxy->columnCount(), 12); QCOMPARE(modelProxy->columnCount(), 13);
QVERIFY(spyColumnInsert.size() >= 1); QVERIFY(spyColumnInsert.size() >= 1);
int oldSpyColumnInsertSize = spyColumnInsert.size(); int oldSpyColumnInsertSize = spyColumnInsert.size();

View File

@ -31,138 +31,107 @@ void TestTotp::initTestCase()
void TestTotp::testParseSecret() void TestTotp::testParseSecret()
{ {
quint8 digits = 0; // OTP URL Parsing
quint8 step = 0;
QString secret = "otpauth://totp/" QString secret = "otpauth://totp/"
"ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=" "ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm="
"SHA1&digits=6&period=30"; "SHA1&digits=6&period=30";
QCOMPARE(Totp::parseOtpString(secret, digits, step), QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ")); auto settings = Totp::parseSettings(secret);
QCOMPARE(digits, quint8(6)); QVERIFY(!settings.isNull());
QCOMPARE(step, quint8(30)); QCOMPARE(settings->key, QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ"));
QCOMPARE(settings->custom, false);
QCOMPARE(settings->digits, 6u);
QCOMPARE(settings->step, 30u);
digits = Totp::defaultDigits; // KeeOTP Parsing
step = Totp::defaultStep;
secret = "key=HXDMVJECJJWSRBY%3d&step=25&size=8"; secret = "key=HXDMVJECJJWSRBY%3d&step=25&size=8";
QCOMPARE(Totp::parseOtpString(secret, digits, step), QString("HXDMVJECJJWSRBY=")); settings = Totp::parseSettings(secret);
QCOMPARE(digits, quint8(8)); QVERIFY(!settings.isNull());
QCOMPARE(step, quint8(25)); QCOMPARE(settings->key, QString("HXDMVJECJJWSRBY="));
QCOMPARE(settings->custom, true);
QCOMPARE(settings->digits, 8u);
QCOMPARE(settings->step, 25u);
digits = 0; // Semi-colon delineated "TOTP Settings"
step = 0;
secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq"; secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq";
QCOMPARE(Totp::parseOtpString(secret, digits, step), QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq")); settings = Totp::parseSettings("30;8", secret);
QCOMPARE(digits, quint8(6)); QVERIFY(!settings.isNull());
QCOMPARE(step, quint8(30)); 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() void TestTotp::testTotpCode()
{ {
// Test vectors from RFC 6238 // Test vectors from RFC 6238
// https://tools.ietf.org/html/rfc6238#appendix-B // 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; quint64 time = 1234567890;
QString output = Totp::generateTotp(seed, time, 6, 30); QCOMPARE(Totp::generateTotp(settings, time), QString("005924"));
QCOMPARE(output, QString("005924"));
time = 1111111109; time = 1111111109;
output = Totp::generateTotp(seed, time, 6, 30); QCOMPARE(Totp::generateTotp(settings, time), QString("081804"));
QCOMPARE(output, QString("081804"));
// Test 8 digit TOTP (custom)
settings->digits = 8;
settings->custom = true;
time = 1111111111; time = 1111111111;
output = Totp::generateTotp(seed, time, 8, 30); QCOMPARE(Totp::generateTotp(settings, time), QString("14050471"));
QCOMPARE(output, QString("14050471"));
time = 2000000000; time = 2000000000;
output = Totp::generateTotp(seed, time, 8, 30); QCOMPARE(Totp::generateTotp(settings, time), QString("69279037"));
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)));
}
} }
void TestTotp::testSteamTotp() void TestTotp::testSteamTotp()
{ {
quint8 digits = 0; // OTP URL Parsing
quint8 step = 0;
QString secret = "otpauth://totp/" QString secret = "otpauth://totp/"
"test:test@example.com?secret=63BEDWCQZKTQWPESARIERL5DTTQFCJTK&issuer=Valve&algorithm=" "test:test@example.com?secret=63BEDWCQZKTQWPESARIERL5DTTQFCJTK&issuer=Valve&algorithm="
"SHA1&digits=5&period=30&encoder=steam"; "SHA1&digits=5&period=30&encoder=steam";
QCOMPARE(Totp::parseOtpString(secret, digits, step), QString("63BEDWCQZKTQWPESARIERL5DTTQFCJTK")); auto settings = Totp::parseSettings(secret);
QCOMPARE(digits, quint8(Totp::ENCODER_STEAM));
QCOMPARE(step, quint8(30));
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 // 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 // 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. // from the Steam app's data for use in testing here.
quint64 time = 1511200518; quint64 time = 1511200518;
QCOMPARE(Totp::generateTotp(seed, time, Totp::ENCODER_STEAM, 30), QString("FR8RV")); QCOMPARE(Totp::generateTotp(settings, time), QString("FR8RV"));
time = 1511200714; time = 1511200714;
QCOMPARE(Totp::generateTotp(seed, time, Totp::ENCODER_STEAM, 30), QString("9P3VP")); QCOMPARE(Totp::generateTotp(settings, time), QString("9P3VP"));
} }
void TestTotp::testEntryHistory() void TestTotp::testEntryHistory()
{ {
Entry entry; Entry entry;
quint8 step = 16; uint step = 16;
quint8 digits = 6; uint digits = 6;
auto settings = Totp::createSettings("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", digits, step);
// Test that entry starts without TOTP
QCOMPARE(entry.historyItems().size(), 0); 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); 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.historyItems().size(), 2);
QCOMPARE(entry.totpSettings()->key, QString("foo"));
} }

View File

@ -21,8 +21,6 @@
#include <QObject> #include <QObject>
class Totp;
class TestTotp : public QObject class TestTotp : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -31,7 +29,6 @@ private slots:
void initTestCase(); void initTestCase();
void testParseSecret(); void testParseSecret();
void testTotpCode(); void testTotpCode();
void testEncoderData();
void testSteamTotp(); void testSteamTotp();
void testEntryHistory(); void testEntryHistory();
}; };

View File

@ -56,7 +56,7 @@
#include "gui/MessageBox.h" #include "gui/MessageBox.h"
#include "gui/PasswordEdit.h" #include "gui/PasswordEdit.h"
#include "gui/SearchWidget.h" #include "gui/SearchWidget.h"
#include "gui/SetupTotpDialog.h" #include "gui/TotpSetupDialog.h"
#include "gui/TotpDialog.h" #include "gui/TotpDialog.h"
#include "gui/entry/EditEntryWidget.h" #include "gui/entry/EditEntryWidget.h"
#include "gui/entry/EntryView.h" #include "gui/entry/EntryView.h"
@ -636,7 +636,7 @@ void TestGui::testTotp()
triggerAction("actionEntrySetupTotp"); triggerAction("actionEntrySetupTotp");
SetupTotpDialog* setupTotpDialog = m_dbWidget->findChild<SetupTotpDialog*>("SetupTotpDialog"); TotpSetupDialog* setupTotpDialog = m_dbWidget->findChild<TotpSetupDialog*>("TotpSetupDialog");
Tools::wait(100); Tools::wait(100);