diff --git a/share/translations/keepassx_en.ts b/share/translations/keepassx_en.ts
index 0638beb97..2fbe1d3be 100644
--- a/share/translations/keepassx_en.ts
+++ b/share/translations/keepassx_en.ts
@@ -1235,6 +1235,22 @@ This is a one-way migration. You won't be able to open the imported databas
&Clone entry
+
+ Timed one-time password
+
+
+
+ Setup TOTP
+
+
+
+ Copy &TOTP
+
+
+
+ Show TOTP
+
+
&Find
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index a87a21b2c..d50d27b69 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -100,6 +100,8 @@ set(keepassx_SOURCES
gui/SettingsWidget.cpp
gui/SearchWidget.cpp
gui/SortFilterHideProxyModel.cpp
+ gui/SetupTotpDialog.cpp
+ gui/TotpDialog.cpp
gui/UnlockDatabaseWidget.cpp
gui/UnlockDatabaseDialog.cpp
gui/WelcomeWidget.cpp
@@ -129,6 +131,8 @@ set(keepassx_SOURCES
streams/qtiocompressor.cpp
streams/StoreDataStream.cpp
streams/SymmetricCipherStream.cpp
+ totp/totp.h
+ totp/totp.cpp
)
set(keepassx_SOURCES_MAINEXE
@@ -151,6 +155,8 @@ set(keepassx_FORMS
gui/SearchWidget.ui
gui/SettingsWidgetGeneral.ui
gui/SettingsWidgetSecurity.ui
+ gui/SetupTotpDialog.ui
+ gui/TotpDialog.ui
gui/WelcomeWidget.ui
gui/entry/EditEntryWidgetAdvanced.ui
gui/entry/EditEntryWidgetAutoType.ui
diff --git a/src/autotype/AutoType.cpp b/src/autotype/AutoType.cpp
index 1d96377f5..12367fe95 100644
--- a/src/autotype/AutoType.cpp
+++ b/src/autotype/AutoType.cpp
@@ -518,6 +518,14 @@ QList AutoType::createActionFromTemplate(const QString& tmpl, c
else if (tmplName.compare("clearfield",Qt::CaseInsensitive)==0) {
list.append(new AutoTypeClearField());
}
+ else if (tmplName.compare("totp", Qt::CaseInsensitive) == 0) {
+ QString totp = entry->totp();
+ if (!totp.isEmpty()) {
+ for (const QChar& ch : totp) {
+ list.append(new AutoTypeChar(ch));
+ }
+ }
+ }
if (!list.isEmpty()) {
return list;
diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp
index d1672c5b1..e86a7092f 100644
--- a/src/core/Entry.cpp
+++ b/src/core/Entry.cpp
@@ -14,13 +14,13 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
#include "Entry.h"
#include "core/Database.h"
#include "core/DatabaseIcons.h"
#include "core/Group.h"
#include "core/Metadata.h"
+#include "totp/totp.h"
const int Entry::DefaultIconNumber = 0;
@@ -35,6 +35,8 @@ Entry::Entry()
m_data.iconNumber = DefaultIconNumber;
m_data.autoTypeEnabled = true;
m_data.autoTypeObfuscation = 0;
+ m_data.totpStep = QTotp::defaultStep;
+ m_data.totpDigits = QTotp::defaultDigits;
connect(m_attributes, SIGNAL(modified()), this, SIGNAL(modified()));
connect(m_attributes, SIGNAL(defaultKeyModified()), SLOT(emitDataChanged()));
@@ -285,6 +287,77 @@ const EntryAttachments* Entry::attachments() const
return m_attachments;
}
+bool Entry::hasTotp() const
+{
+ return m_attributes->hasKey("TOTP Seed") || m_attributes->hasKey("otp");
+}
+
+QString Entry::totp() const
+{
+ if (hasTotp()) {
+ QString seed = totpSeed();
+ quint64 time = QDateTime::currentDateTime().toTime_t();
+ QString output = QTotp::generateTotp(seed.toLatin1(), time, m_data.totpDigits, m_data.totpStep);
+
+ return QString(output);
+ } else {
+ return QString("");
+ }
+}
+
+void Entry::setTotp(const QString& seed, quint8& step, quint8& digits)
+{
+ if (step == 0) {
+ step = QTotp::defaultStep;
+ }
+
+ if (digits == 0) {
+ digits = QTotp::defaultDigits;
+ }
+
+ if (m_attributes->hasKey("otp")) {
+ m_attributes->set("otp", QString("key=%1&step=%2&size=%3").arg(seed).arg(step).arg(digits), true);
+ } else {
+ m_attributes->set("TOTP Seed", seed, true);
+ m_attributes->set("TOTP Settings", QString("%1;%2").arg(step).arg(digits));
+ }
+}
+
+QString Entry::totpSeed() const
+{
+ QString secret = "";
+
+ if (m_attributes->hasKey("otp")) {
+ secret = m_attributes->value("otp");
+ } else {
+ secret = m_attributes->value("TOTP Seed");
+ }
+
+ m_data.totpDigits = QTotp::defaultDigits;
+ m_data.totpStep = QTotp::defaultStep;
+
+ if (m_attributes->hasKey("TOTP Settings")) {
+ QRegExp rx("(\\d+);(\\d)", Qt::CaseInsensitive, QRegExp::RegExp);
+ int pos = rx.indexIn(m_attributes->value("TOTP Settings"));
+ if (pos > -1) {
+ m_data.totpStep = rx.cap(1).toUInt();
+ m_data.totpDigits = rx.cap(2).toUInt();
+ }
+ }
+
+ return QTotp::parseOtpString(secret, m_data.totpDigits, m_data.totpStep);
+}
+
+quint8 Entry::totpStep() const
+{
+ return m_data.totpStep;
+}
+
+quint8 Entry::totpDigits() const
+{
+ return m_data.totpDigits;
+}
+
void Entry::setUuid(const Uuid& uuid)
{
Q_ASSERT(!uuid.isNull());
@@ -679,7 +752,7 @@ QString Entry::resolvePlaceholder(const QString& str) const
k.prepend("{");
}
-
+
k.append("}");
if (result.compare(k,cs)==0) {
result.replace(result,attributes()->value(key));
diff --git a/src/core/Entry.h b/src/core/Entry.h
index 25b9bc386..cdb826eca 100644
--- a/src/core/Entry.h
+++ b/src/core/Entry.h
@@ -47,6 +47,8 @@ struct EntryData
int autoTypeObfuscation;
QString defaultAutoTypeSequence;
TimeInfo timeInfo;
+ mutable quint8 totpDigits;
+ mutable quint8 totpStep;
};
class Entry : public QObject
@@ -78,6 +80,12 @@ public:
QString username() const;
QString password() const;
QString notes() const;
+ QString totp() const;
+ QString totpSeed() const;
+ quint8 totpDigits() const;
+ quint8 totpStep() const;
+
+ bool hasTotp() const;
bool isExpired() const;
bool hasReferences() const;
EntryAttributes* attributes();
@@ -105,6 +113,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);
QList historyItems();
const QList& historyItems() const;
diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp
index 115c560f0..946757e40 100644
--- a/src/gui/DatabaseWidget.cpp
+++ b/src/gui/DatabaseWidget.cpp
@@ -42,6 +42,8 @@
#include "gui/ChangeMasterKeyWidget.h"
#include "gui/Clipboard.h"
#include "gui/CloneDialog.h"
+#include "gui/SetupTotpDialog.h"
+#include "gui/TotpDialog.h"
#include "gui/DatabaseOpenWidget.h"
#include "gui/DatabaseSettingsWidget.h"
#include "gui/KeePass1OpenWidget.h"
@@ -333,6 +335,48 @@ void DatabaseWidget::cloneEntry()
return;
}
+void DatabaseWidget::showTotp()
+{
+ Entry* currentEntry = m_entryView->currentEntry();
+ if (!currentEntry) {
+ Q_ASSERT(false);
+ return;
+ }
+
+ TotpDialog* totpDialog = new TotpDialog(this, currentEntry);
+ totpDialog->open();
+}
+
+void DatabaseWidget::copyTotp()
+{
+ Entry* currentEntry = m_entryView->currentEntry();
+ if (!currentEntry) {
+ Q_ASSERT(false);
+ return;
+ }
+ setClipboardTextAndMinimize(currentEntry->totp());
+}
+
+void DatabaseWidget::setupTotp()
+{
+ Entry* currentEntry = m_entryView->currentEntry();
+ if (!currentEntry) {
+ Q_ASSERT(false);
+ return;
+ }
+
+ SetupTotpDialog* setupTotpDialog = new SetupTotpDialog(this, currentEntry);
+ if (currentEntry->hasTotp()) {
+ setupTotpDialog->setSeed(currentEntry->totpSeed());
+ setupTotpDialog->setStep(currentEntry->totpStep());
+ setupTotpDialog->setDigits(currentEntry->totpDigits());
+ }
+
+ setupTotpDialog->open();
+
+}
+
+
void DatabaseWidget::deleteEntries()
{
const QModelIndexList selected = m_entryView->selectionModel()->selectedRows();
@@ -1225,6 +1269,17 @@ bool DatabaseWidget::currentEntryHasUrl()
return !currentEntry->resolveMultiplePlaceholders(currentEntry->url()).isEmpty();
}
+
+bool DatabaseWidget::currentEntryHasTotp()
+{
+ Entry* currentEntry = m_entryView->currentEntry();
+ if (!currentEntry) {
+ Q_ASSERT(false);
+ return false;
+ }
+ return currentEntry->hasTotp();
+}
+
bool DatabaseWidget::currentEntryHasNotes()
{
Entry* currentEntry = m_entryView->currentEntry();
diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h
index 3add336f0..aa1c83443 100644
--- a/src/gui/DatabaseWidget.h
+++ b/src/gui/DatabaseWidget.h
@@ -96,6 +96,7 @@ public:
bool currentEntryHasPassword();
bool currentEntryHasUrl();
bool currentEntryHasNotes();
+ bool currentEntryHasTotp();
GroupView* groupView();
EntryView* entryView();
void showUnlockDialog();
@@ -133,6 +134,9 @@ public slots:
void copyURL();
void copyNotes();
void copyAttribute(QAction* action);
+ void showTotp();
+ void copyTotp();
+ void setupTotp();
void performAutoType();
void openUrl();
void openUrlForEntry(Entry* entry);
diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp
index 6883f8a7e..0415f2b4e 100644
--- a/src/gui/MainWindow.cpp
+++ b/src/gui/MainWindow.cpp
@@ -167,6 +167,8 @@ MainWindow::MainWindow()
m_ui->actionEntryEdit->setShortcut(Qt::CTRL + Qt::Key_E);
m_ui->actionEntryDelete->setShortcut(Qt::CTRL + Qt::Key_D);
m_ui->actionEntryClone->setShortcut(Qt::CTRL + Qt::Key_K);
+ m_ui->actionEntryTotp->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_T);
+ m_ui->actionEntryCopyTotp->setShortcut(Qt::CTRL + Qt::Key_T);
m_ui->actionEntryCopyUsername->setShortcut(Qt::CTRL + Qt::Key_B);
m_ui->actionEntryCopyPassword->setShortcut(Qt::CTRL + Qt::Key_C);
setShortcut(m_ui->actionEntryAutoType, QKeySequence::Paste, Qt::CTRL + Qt::Key_V);
@@ -275,6 +277,13 @@ MainWindow::MainWindow()
m_actionMultiplexer.connect(m_ui->actionEntryDelete, SIGNAL(triggered()),
SLOT(deleteEntries()));
+ m_actionMultiplexer.connect(m_ui->actionEntryTotp, SIGNAL(triggered()),
+ SLOT(showTotp()));
+ m_actionMultiplexer.connect(m_ui->actionEntrySetupTotp, SIGNAL(triggered()),
+ SLOT(setupTotp()));
+
+ m_actionMultiplexer.connect(m_ui->actionEntryCopyTotp, SIGNAL(triggered()),
+ SLOT(copyTotp()));
m_actionMultiplexer.connect(m_ui->actionEntryCopyTitle, SIGNAL(triggered()),
SLOT(copyTitle()));
m_actionMultiplexer.connect(m_ui->actionEntryCopyUsername, SIGNAL(triggered()),
@@ -428,8 +437,12 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->actionEntryCopyURL->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUrl());
m_ui->actionEntryCopyNotes->setEnabled(singleEntrySelected && dbWidget->currentEntryHasNotes());
m_ui->menuEntryCopyAttribute->setEnabled(singleEntrySelected);
+ m_ui->menuEntryTotp->setEnabled(true);
m_ui->actionEntryAutoType->setEnabled(singleEntrySelected);
m_ui->actionEntryOpenUrl->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUrl());
+ m_ui->actionEntryTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp());
+ m_ui->actionEntryCopyTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp());
+ m_ui->actionEntrySetupTotp->setEnabled(singleEntrySelected);
m_ui->actionGroupNew->setEnabled(groupSelected);
m_ui->actionGroupEdit->setEnabled(groupSelected);
m_ui->actionGroupDelete->setEnabled(groupSelected && dbWidget->canDeleteCurrentGroup());
@@ -463,6 +476,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->actionEntryCopyURL->setEnabled(false);
m_ui->actionEntryCopyNotes->setEnabled(false);
m_ui->menuEntryCopyAttribute->setEnabled(false);
+ m_ui->menuEntryTotp->setEnabled(false);
m_ui->actionChangeMasterKey->setEnabled(false);
m_ui->actionChangeDatabaseSettings->setEnabled(false);
@@ -495,6 +509,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->actionEntryCopyURL->setEnabled(false);
m_ui->actionEntryCopyNotes->setEnabled(false);
m_ui->menuEntryCopyAttribute->setEnabled(false);
+ m_ui->menuEntryTotp->setEnabled(false);
m_ui->actionChangeMasterKey->setEnabled(false);
m_ui->actionChangeDatabaseSettings->setEnabled(false);
diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui
index 384586e8d..2ed42d0ec 100644
--- a/src/gui/MainWindow.ui
+++ b/src/gui/MainWindow.ui
@@ -220,9 +220,21 @@
+
+
@@ -523,6 +535,21 @@
Re&pair database
+
+
+ Show TOTP
+
+
+
+
+ Setup TOTP
+
+
+
+
+ Copy &TOTP
+
+
Empty recycle bin
diff --git a/src/gui/SetupTotpDialog.cpp b/src/gui/SetupTotpDialog.cpp
new file mode 100644
index 000000000..43a042df9
--- /dev/null
+++ b/src/gui/SetupTotpDialog.cpp
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2017 Weslly Honorato <weslly@protonmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 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 "ui_SetupTotpDialog.h"
+#include "totp/totp.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->customSettingsCheckBox, SIGNAL(toggled(bool)), SLOT(toggleCustom(bool)));
+}
+
+
+void SetupTotpDialog::setupTotp()
+{
+ quint8 digits;
+
+ if (m_ui->radio8Digits->isChecked()) {
+ digits = 8;
+ } else {
+ digits = 6;
+ }
+
+ quint8 step = m_ui->stepSpinBox->value();
+ QString seed = m_ui->seedEdit->text();
+ m_entry->setTotp(seed, step, digits);
+ emit m_parent->entrySelectionChanged();
+ close();
+}
+
+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::setStep(quint8 step)
+{
+ m_ui->stepSpinBox->setValue(step);
+
+ if (step != QTotp::defaultStep) {
+ m_ui->customSettingsCheckBox->setChecked(true);
+ }
+}
+
+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);
+ }
+
+ if (digits != QTotp::defaultDigits) {
+ m_ui->customSettingsCheckBox->setChecked(true);
+ }
+}
+
+
+SetupTotpDialog::~SetupTotpDialog()
+{
+}
diff --git a/src/gui/SetupTotpDialog.h b/src/gui/SetupTotpDialog.h
new file mode 100644
index 000000000..416e19a5c
--- /dev/null
+++ b/src/gui/SetupTotpDialog.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017 Weslly Honorato <weslly@protonmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 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 .
+ */
+
+#ifndef KEEPASSX_SETUPTOTPDIALOG_H
+#define KEEPASSX_SETUPTOTPDIALOG_H
+
+#include
+#include
+#include "core/Entry.h"
+#include "core/Database.h"
+#include "gui/DatabaseWidget.h"
+
+namespace Ui {
+ class SetupTotpDialog;
+}
+
+class SetupTotpDialog : 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);
+
+private Q_SLOTS:
+ void toggleCustom(bool status);
+ void setupTotp();
+
+private:
+ QScopedPointer m_ui;
+
+protected:
+ Entry* m_entry;
+ DatabaseWidget* m_parent;
+};
+
+#endif // KEEPASSX_SETUPTOTPDIALOG_H
diff --git a/src/gui/SetupTotpDialog.ui b/src/gui/SetupTotpDialog.ui
new file mode 100644
index 000000000..a6d806287
--- /dev/null
+++ b/src/gui/SetupTotpDialog.ui
@@ -0,0 +1,137 @@
+
+
+ SetupTotpDialog
+
+
+
+ 0
+ 0
+ 282
+ 257
+
+
+
+ Setup TOTP
+
+
+ -
+
+
-
+
+
+ Key:
+
+
+
+ -
+
+
+
+
+ -
+
+
+ Use custom settings
+
+
+
+ -
+
+
+ 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
new file mode 100644
index 000000000..2eb1c9e2c
--- /dev/null
+++ b/src/gui/TotpDialog.cpp
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2017 Weslly Honorato <weslly@protonmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 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 "TotpDialog.h"
+#include "ui_TotpDialog.h"
+
+#include "core/Config.h"
+#include "core/Entry.h"
+#include "gui/DatabaseWidget.h"
+#include "gui/Clipboard.h"
+
+#include
+#include
+#include
+
+
+TotpDialog::TotpDialog(DatabaseWidget* parent, Entry* entry)
+ : QDialog(parent)
+ , m_ui(new Ui::TotpDialog())
+{
+ m_entry = entry;
+ m_parent = parent;
+ m_step = m_entry->totpStep();
+
+ m_ui->setupUi(this);
+
+ uCounter = resetCounter();
+ updateProgressBar();
+
+ QTimer *timer = new QTimer(this);
+ connect(timer, SIGNAL(timeout()), this, SLOT(updateProgressBar()));
+ connect(timer, SIGNAL(timeout()), this, SLOT(updateSeconds()));
+ timer->start(m_step * 10);
+
+ updateTotp();
+
+ setAttribute(Qt::WA_DeleteOnClose);
+
+ m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Copy"));
+
+ connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(close()));
+ connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(copyToClipboard()));
+}
+
+void TotpDialog::copyToClipboard()
+{
+ clipboard()->setText(m_entry->totp());
+ if (config()->get("MinimizeOnCopy").toBool()) {
+ m_parent->window()->showMinimized();
+ }
+}
+
+void TotpDialog::updateProgressBar()
+{
+ if (uCounter < 100) {
+ m_ui->progressBar->setValue(100 - uCounter);
+ m_ui->progressBar->update();
+ uCounter++;
+ } else {
+ updateTotp();
+ uCounter = resetCounter();
+ }
+}
+
+
+void TotpDialog::updateSeconds()
+{
+ uint epoch = QDateTime::currentDateTime().toTime_t() - 1;
+ m_ui->timerLabel->setText(tr("Expires in") + " " + QString::number(m_step - (epoch % m_step)) + " " + tr("seconds"));
+}
+
+void TotpDialog::updateTotp()
+{
+ m_ui->totpLabel->setText(m_entry->totp());
+}
+
+double TotpDialog::resetCounter()
+{
+ uint epoch = QDateTime::currentDateTime().toTime_t();
+ double counter = qRound(static_cast(epoch % m_step) / m_step * 100);
+ return counter;
+}
+
+TotpDialog::~TotpDialog()
+{
+}
diff --git a/src/gui/TotpDialog.h b/src/gui/TotpDialog.h
new file mode 100644
index 000000000..66754dd29
--- /dev/null
+++ b/src/gui/TotpDialog.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017 Weslly Honorato <weslly@protonmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 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 .
+ */
+
+#ifndef KEEPASSX_TOTPDIALOG_H
+#define KEEPASSX_TOTPDIALOG_H
+
+#include
+#include
+#include "core/Entry.h"
+#include "core/Database.h"
+#include "gui/DatabaseWidget.h"
+
+namespace Ui {
+ class TotpDialog;
+}
+
+class TotpDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit TotpDialog(DatabaseWidget* parent = nullptr, Entry* entry = nullptr);
+ ~TotpDialog();
+
+private:
+ double uCounter;
+ quint8 m_step;
+ QScopedPointer m_ui;
+
+private Q_SLOTS:
+ void updateTotp();
+ void updateProgressBar();
+ void updateSeconds();
+ void copyToClipboard();
+ double resetCounter();
+
+protected:
+ Entry* m_entry;
+ DatabaseWidget* m_parent;
+};
+
+#endif // KEEPASSX_TOTPDIALOG_H
diff --git a/src/gui/TotpDialog.ui b/src/gui/TotpDialog.ui
new file mode 100644
index 000000000..e11e761e9
--- /dev/null
+++ b/src/gui/TotpDialog.ui
@@ -0,0 +1,60 @@
+
+
+ TotpDialog
+
+
+
+ 0
+ 0
+ 264
+ 194
+
+
+
+ Timed Password
+
+
+ -
+
+
+
+ 53
+
+
+
+ 000000
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ 0
+
+
+ false
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+ QDialogButtonBox::Close|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
diff --git a/src/totp/totp.cpp b/src/totp/totp.cpp
new file mode 100644
index 000000000..45e98d38d
--- /dev/null
+++ b/src/totp/totp.cpp
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2017 Weslly Honorato <weslly@protonmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 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 "totp.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+
+const quint8 QTotp::defaultStep = 30;
+const quint8 QTotp::defaultDigits = 6;
+
+QTotp::QTotp()
+{
+}
+
+QString QTotp::parseOtpString(QString key, quint8 &digits, quint8 &step)
+{
+ QUrl url(key);
+
+ QString seed;
+ uint q_digits, q_step;
+
+ // Default OTP url format
+ if (url.isValid() && url.scheme() == "otpauth") {
+ 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;
+ }
+
+
+ } 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;
+ }
+
+ } else {
+ seed = key;
+ }
+ }
+
+ if (digits == 0) {
+ digits = defaultDigits;
+ }
+
+ if (step == 0) {
+ step = defaultStep;
+ }
+
+ return seed;
+}
+
+
+QByteArray QTotp::base32_decode(const QByteArray encoded)
+{
+ // Base32 implementation
+ // Copyright 2010 Google Inc.
+ // Author: Markus Gutschke
+ // Licensed under the Apache License, Version 2.0
+
+ QByteArray result;
+
+ int buffer = 0;
+ int bitsLeft = 0;
+
+ for (char ch : encoded) {
+ if (ch == 0 || ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '-' || ch == '=') {
+ continue;
+ }
+
+ buffer <<= 5;
+
+ // Deal with commonly mistyped characters
+ if (ch == '0') {
+ ch = 'O';
+ } else if (ch == '1') {
+ ch = 'L';
+ } else if (ch == '8') {
+ ch = 'B';
+ }
+
+ // Look up one base32 digit
+ if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')) {
+ ch = (ch & 0x1F) - 1;
+ } else if (ch >= '2' && ch <= '7') {
+ ch -= '2' - 26;
+ } else {
+ return QByteArray();
+ }
+
+ buffer |= ch;
+ bitsLeft += 5;
+
+ if (bitsLeft >= 8) {
+ result.append(static_cast (buffer >> (bitsLeft - 8)));
+ bitsLeft -= 8;
+ }
+ }
+
+ return result;
+}
+
+
+QString QTotp::generateTotp(const QByteArray key, quint64 time, const quint8 numDigits = defaultDigits, const quint8 step = defaultStep)
+{
+ quint64 current = qToBigEndian(time / step);
+
+ QByteArray secret = QTotp::base32_decode(key);
+ if (secret.isEmpty()) {
+ return "Invalid TOTP secret key";
+ }
+
+ QMessageAuthenticationCode code(QCryptographicHash::Sha1);
+ code.setKey(secret);
+ code.addData(QByteArray(reinterpret_cast(¤t), sizeof(current)));
+ QByteArray hmac = code.result();
+
+ int offset = (hmac[hmac.length() - 1] & 0xf);
+ int binary =
+ ((hmac[offset] & 0x7f) << 24)
+ | ((hmac[offset + 1] & 0xff) << 16)
+ | ((hmac[offset + 2] & 0xff) << 8)
+ | (hmac[offset + 3] & 0xff);
+
+ quint32 digitsPower = pow(10, numDigits);
+
+ quint64 password = binary % digitsPower;
+ return QString("%1").arg(password, numDigits, 10, QChar('0'));
+}
diff --git a/src/totp/totp.h b/src/totp/totp.h
new file mode 100644
index 000000000..8d7e86744
--- /dev/null
+++ b/src/totp/totp.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 Weslly Honorato <weslly@protonmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 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 .
+ */
+
+#ifndef QTOTP_H
+#define QTOTP_H
+
+#include
+
+class QTotp
+{
+public:
+ QTotp();
+ static QString parseOtpString(QString rawSecret, quint8 &digits, quint8 &step);
+ static QByteArray base32_decode(const QByteArray encoded);
+ static QString generateTotp(const QByteArray key, quint64 time, const quint8 numDigits, const quint8 step);
+ static const quint8 defaultStep;
+ static const quint8 defaultDigits;
+};
+
+#endif // QTOTP_H
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 1c9f1c0f5..67661f55c 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -158,6 +158,9 @@ endif()
add_unit_test(NAME testentry SOURCES TestEntry.cpp
LIBS ${TEST_LIBRARIES})
+add_unit_test(NAME testtotp SOURCES TestTotp.cpp
+ LIBS ${TEST_LIBRARIES})
+
add_unit_test(NAME testcsvparser SOURCES TestCsvParser.cpp
LIBS ${TEST_LIBRARIES})
diff --git a/tests/TestTotp.cpp b/tests/TestTotp.cpp
new file mode 100644
index 000000000..c1fe5943c
--- /dev/null
+++ b/tests/TestTotp.cpp
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017 Weslly Honorato <weslly@protonmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 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 "TestTotp.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "crypto/Crypto.h"
+#include "totp/totp.h"
+
+QTEST_GUILESS_MAIN(TestTotp)
+
+void TestTotp::initTestCase()
+{
+ QVERIFY(Crypto::init());
+}
+
+
+void TestTotp::testSecret()
+{
+ quint8 digits = 0;
+ quint8 step = 0;
+ QString secret = "otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30";
+ QCOMPARE(QTotp::parseOtpString(secret, digits, step), QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ"));
+ QCOMPARE(digits, quint8(6));
+ QCOMPARE(step, quint8(30));
+
+ digits = QTotp::defaultDigits;
+ step = QTotp::defaultStep;
+ secret = "key=HXDMVJECJJWSRBY%3d&step=25&size=8";
+ QCOMPARE(QTotp::parseOtpString(secret, digits, step), QString("HXDMVJECJJWSRBY="));
+ QCOMPARE(digits, quint8(8));
+ QCOMPARE(step, quint8(25));
+
+ digits = 0;
+ step = 0;
+ secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq";
+ QCOMPARE(QTotp::parseOtpString(secret, digits, step), QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq"));
+ QCOMPARE(digits, quint8(6));
+ QCOMPARE(step, quint8(30));
+}
+
+void TestTotp::testBase32()
+{
+ QByteArray key = QString("JBSW Y3DP EB3W 64TM MQXC 4LQA").toLatin1();
+ QByteArray secret = QTotp::base32_decode(key);
+ QCOMPARE(QString::fromLatin1(secret), QString("Hello world..."));
+
+ key = QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq").toLatin1();
+ secret = QTotp::base32_decode(key);
+ QCOMPARE(QString::fromLatin1(secret), QString("12345678901234567890"));
+
+ key = QString("ORSXG5A=").toLatin1();
+ secret = QTotp::base32_decode(key);
+ QCOMPARE(QString::fromLatin1(secret), QString("test"));
+
+ key = QString("MZXW6YTBOI======").toLatin1();
+ secret = QTotp::base32_decode(key);
+ QCOMPARE(QString::fromLatin1(secret), QString("foobar"));
+}
+
+void TestTotp::testTotpCode()
+{
+ // Test vectors from RFC 6238
+ // https://tools.ietf.org/html/rfc6238#appendix-B
+
+ QByteArray seed = QString("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ").toLatin1();
+
+ quint64 time = 1234567890;
+ QString output = QTotp::generateTotp(seed, time, 6, 30);
+ QCOMPARE(output, QString("005924"));
+
+ time = 1111111109;
+ output = QTotp::generateTotp(seed, time, 6, 30);
+ QCOMPARE(output, QString("081804"));
+
+ time = 1111111111;
+ output = QTotp::generateTotp(seed, time, 8, 30);
+ QCOMPARE(output, QString("14050471"));
+
+ time = 2000000000;
+ output = QTotp::generateTotp(seed, time, 8, 30);
+ QCOMPARE(output, QString("69279037"));
+}
diff --git a/tests/TestTotp.h b/tests/TestTotp.h
new file mode 100644
index 000000000..7bfa68055
--- /dev/null
+++ b/tests/TestTotp.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017 Weslly Honorato <weslly@protonmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 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 .
+ */
+
+#ifndef KEEPASSX_TESTTOTP_H
+#define KEEPASSX_TESTTOTP_H
+
+#include
+
+class Totp;
+
+class TestTotp : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void initTestCase();
+ void testSecret();
+ void testBase32();
+ void testTotpCode();
+};
+
+#endif // KEEPASSX_TESTTOTP_H
diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp
index 5516aff7c..5a4142660 100644
--- a/tests/gui/TestGui.cpp
+++ b/tests/gui/TestGui.cpp
@@ -48,6 +48,8 @@
#include "gui/DatabaseTabWidget.h"
#include "gui/DatabaseWidget.h"
#include "gui/CloneDialog.h"
+#include "gui/TotpDialog.h"
+#include "gui/SetupTotpDialog.h"
#include "gui/FileDialog.h"
#include "gui/MainWindow.h"
#include "gui/MessageBox.h"
@@ -441,6 +443,55 @@ void TestGui::testDicewareEntryEntropy()
QCOMPARE(strengthLabel->text(), QString("Password Quality: Good"));
}
+void TestGui::testTotp()
+{
+ QToolBar* toolBar = m_mainWindow->findChild("toolBar");
+ EntryView* entryView = m_dbWidget->findChild("entryView");
+
+ QCOMPARE(entryView->model()->rowCount(), 1);
+
+ QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::ViewMode);
+ QModelIndex item = entryView->model()->index(0, 1);
+ Entry* entry = entryView->entryFromIndex(item);
+
+ clickIndex(item, entryView, Qt::LeftButton);
+
+ triggerAction("actionEntrySetupTotp");
+
+ SetupTotpDialog* setupTotpDialog = m_dbWidget->findChild("SetupTotpDialog");
+
+ Tools::wait(100);
+
+ QLineEdit* seedEdit = setupTotpDialog->findChild("seedEdit");
+
+ QString exampleSeed = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq";
+ QTest::keyClicks(seedEdit, exampleSeed);
+
+ QDialogButtonBox* setupTotpButtonBox = setupTotpDialog->findChild("buttonBox");
+ QTest::mouseClick(setupTotpButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
+
+ QAction* entryEditAction = m_mainWindow->findChild("actionEntryEdit");
+ QWidget* entryEditWidget = toolBar->widgetForAction(entryEditAction);
+ QTest::mouseClick(entryEditWidget, Qt::LeftButton);
+ QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::EditMode);
+ EditEntryWidget* editEntryWidget = m_dbWidget->findChild("editEntryWidget");
+
+ editEntryWidget->setCurrentPage(1);
+ QPlainTextEdit* attrTextEdit = editEntryWidget->findChild("attributesEdit");
+ QTest::mouseClick(editEntryWidget->findChild("revealAttributeButton"), Qt::LeftButton);
+ QCOMPARE(attrTextEdit->toPlainText(), exampleSeed);
+
+ QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox");
+ QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
+
+ triggerAction("actionEntryTotp");
+
+ TotpDialog* totpDialog = m_dbWidget->findChild("TotpDialog");
+ QLabel* totpLabel = totpDialog->findChild("totpLabel");
+
+ QCOMPARE(totpLabel->text(), entry->totp());
+}
+
void TestGui::testSearch()
{
// Add canned entries for consistent testing
diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h
index fce5d69ee..e5d41fb64 100644
--- a/tests/gui/TestGui.h
+++ b/tests/gui/TestGui.h
@@ -46,6 +46,7 @@ private slots:
void testAddEntry();
void testPasswordEntryEntropy();
void testDicewareEntryEntropy();
+ void testTotp();
void testSearch();
void testDeleteEntry();
void testCloneEntry();