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 @@ + + + false + + + Timed one-time password + + + + + + @@ -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();