diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ab02f32783..3b6cb2ee59 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -115,6 +115,7 @@ set(keepassx_SOURCES gui/EditWidgetProperties.cpp gui/FileDialog.cpp gui/Font.cpp + gui/GlobalTotpDialog.cpp gui/GuiTools.cpp gui/HtmlExporter.cpp gui/IconModels.cpp diff --git a/src/gui/GlobalTotpDialog.cpp b/src/gui/GlobalTotpDialog.cpp new file mode 100644 index 0000000000..8f554fe6ce --- /dev/null +++ b/src/gui/GlobalTotpDialog.cpp @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "GlobalTotpDialog.h" +#include "ui_GlobalTotpDialog.h" + +#include "core/Clock.h" +#include "core/Totp.h" +#include "gui/Clipboard.h" +#include "gui/MainWindow.h" + +#include +#include +#include + +GlobalTotpDialog::GlobalTotpDialog(QWidget* parent) + : QWidget(parent) + , m_ui(new Ui::GlobalTotpDialog()) +{ + setAttribute(Qt::WA_DeleteOnClose); + + m_ui->setupUi(this); + + connect(&m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateCounter())); + connect(&m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateTable())); + + new QShortcut(QKeySequence(QKeySequence::Copy), this, SLOT(copyToClipboard())); + + connect(m_ui->buttonBox, SIGNAL(rejected()), this, SIGNAL(closed())); +} + +GlobalTotpDialog::~GlobalTotpDialog() = default; + +void GlobalTotpDialog::setDatabaseWidget(DatabaseWidget* databaseWidget) +{ + if (!databaseWidget) { + return; + } + + m_databaseWidget = databaseWidget; + m_ui->totpTable->clear(); + + m_entries.clear(); + const auto db = m_databaseWidget->database(); + for (const auto& group : db->rootGroup()->groupsRecursive(true)) { + if (group == db->metadata()->recycleBin()) { + continue; + } + + for (const auto& entry : group->entries()) { + if (entry->hasTotp()) { + m_entries.push_back(entry); + } + } + } + + m_ui->totpTable->setRowCount(m_entries.size()); + m_ui->totpTable->setColumnCount(5); + m_ui->totpTable->setHorizontalHeaderLabels(QStringList() << tr("Title") << tr("Username") << tr("TOTP") + << tr("Expires in (s)") << tr("Copy TOTP")); + m_ui->totpTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + m_ui->totpTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + m_ui->totpTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents); + m_ui->totpTable->horizontalHeader()->setSectionResizeMode(4, QHeaderView::ResizeToContents); + + uint epoch = Clock::currentSecondsSinceEpoch() - 1; + + auto i = 0; + for (const auto& entry : m_entries) { + const auto step = entry->totpSettings()->step; + + m_ui->totpTable->setItem(i, 0, new QTableWidgetItem(entry->title())); + m_ui->totpTable->setItem(i, 1, new QTableWidgetItem(entry->username())); + m_ui->totpTable->setItem(i, 2, new QTableWidgetItem(entry->totp())); + m_ui->totpTable->setItem(i, 3, new QTableWidgetItem(QString::number(step - (epoch % step)))); + + auto button = new QToolButton(); + button->setText(tr("Copy")); + button->setProperty("row", i); + + connect(button, &QToolButton::clicked, this, [this]() { + auto btn = qobject_cast(sender()); + auto totp = m_ui->totpTable->item(btn->property("row").toInt(), 2)->text(); + + clipboard()->setText(totp); + }); + + m_ui->totpTable->setCellWidget(i, 4, button); + ++i; + } + + m_totpUpdateTimer.start(1000); +} + +void GlobalTotpDialog::copyToClipboard() +{ + const auto selectedItems = m_ui->totpTable->selectedItems(); + if (selectedItems.isEmpty()) { + return; + } + + const auto selectedRow = selectedItems.first()->row(); + auto totp = m_ui->totpTable->item(selectedRow, 2)->text(); + clipboard()->setText(totp); +} + +void GlobalTotpDialog::updateCounter() +{ + if (!isVisible()) { + m_totpUpdateTimer.stop(); + return; + } +} + +void GlobalTotpDialog::updateTable() +{ + uint epoch = Clock::currentSecondsSinceEpoch() - 1; + + for (auto i = 0; i < m_ui->totpTable->rowCount(); ++i) { + const auto totpCounter = m_ui->totpTable->item(i, 3)->text().toInt(); + const auto entry = m_entries.at(i); + const auto step = entry->totpSettings()->step; + + if (totpCounter == 1) { + m_ui->totpTable->item(i, 2)->setText(entry->totp()); + m_ui->totpTable->item(i, 3)->setText(QString::number(step)); + } else { + m_ui->totpTable->item(i, 3)->setText(QString::number(step - (epoch % step))); + } + } +} diff --git a/src/gui/GlobalTotpDialog.h b/src/gui/GlobalTotpDialog.h new file mode 100644 index 0000000000..e05fbb7346 --- /dev/null +++ b/src/gui/GlobalTotpDialog.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_GLOBALTOTPDIALOG_H +#define KEEPASSXC_GLOBALTOTPDIALOG_H + +#include "core/Database.h" +#include "core/Entry.h" +#include "gui/DatabaseWidget.h" + +namespace Ui +{ + class GlobalTotpDialog; +} + +class GlobalTotpDialog : public QWidget +{ + Q_OBJECT + +public: + explicit GlobalTotpDialog(QWidget* parent = nullptr); + ~GlobalTotpDialog() override; + + void setDatabaseWidget(DatabaseWidget* databaseWidget); + +signals: + void closed(); + +private Q_SLOTS: + void updateCounter(); + void updateTable(); + void copyToClipboard(); + +private: + QScopedPointer m_ui; + + QPointer m_databaseWidget; + QList m_entries; + QTimer m_totpUpdateTimer; +}; + +#endif // KEEPASSXC_GLOBALTOTPDIALOG_H diff --git a/src/gui/GlobalTotpDialog.ui b/src/gui/GlobalTotpDialog.ui new file mode 100644 index 0000000000..3b2854a6dc --- /dev/null +++ b/src/gui/GlobalTotpDialog.ui @@ -0,0 +1,68 @@ + + + GlobalTotpDialog + + + + 0 + 0 + 264 + 194 + + + + Timed Passwords + + + + + + Qt::NoFocus + + + QTableView::item {padding: 3px;} + + + QFrame::NoFrame + + + QAbstractItemView::NoEditTriggers + + + false + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + + false + + + false + + + true + + + true + + + + + + + QDialogButtonBox::Close + + + + + + + + diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 2b92894f3b..5519998768 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -41,6 +41,7 @@ #include "core/Tools.h" #include "gui/AboutDialog.h" #include "gui/ActionCollection.h" +#include "gui/GlobalTotpDialog.h" #include "gui/Icons.h" #include "gui/MessageBox.h" #include "gui/SearchWidget.h" @@ -398,6 +399,7 @@ MainWindow::MainWindow() m_ui->actionSettings->setIcon(icons()->icon("configure")); m_ui->actionPasswordGenerator->setIcon(icons()->icon("password-generator")); + m_ui->actionGlobalTotp->setIcon(icons()->icon("totp")); m_ui->actionAbout->setIcon(icons()->icon("help-about")); m_ui->actionDonate->setIcon(icons()->icon("donate")); @@ -529,6 +531,12 @@ MainWindow::MainWindow() }); m_ui->passwordGeneratorWidget->setStandaloneMode(true); + connect(m_ui->actionGlobalTotp, SIGNAL(toggled(bool)), SLOT(toggleGlobalTotp(bool))); + connect(m_ui->globalTotpDialog, &GlobalTotpDialog::closed, this, [this] { + toggleGlobalTotp(false); + }); + m_ui->passwordGeneratorWidget->setStandaloneMode(true); + connect(m_ui->welcomeWidget, SIGNAL(newDatabase()), SLOT(switchToNewDatabase())); connect(m_ui->welcomeWidget, SIGNAL(openDatabase()), SLOT(switchToOpenDatabase())); connect(m_ui->welcomeWidget, SIGNAL(openDatabaseFile(QString)), SLOT(switchToDatabaseFile(QString))); @@ -958,6 +966,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionExportHtml->setEnabled(true); m_ui->actionExportXML->setEnabled(true); m_ui->actionDatabaseMerge->setEnabled(m_ui->tabWidget->currentIndex() != -1); + m_ui->actionGlobalTotp->setEnabled(true); #ifdef WITH_XC_BROWSER_PASSKEYS m_ui->actionPasskeys->setEnabled(true); m_ui->actionImportPasskey->setEnabled(true); @@ -1041,6 +1050,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) #endif m_searchWidgetAction->setEnabled(false); + m_ui->actionGlobalTotp->setEnabled(false); break; } default: @@ -1083,6 +1093,10 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) bool blocked = m_ui->actionPasswordGenerator->blockSignals(true); m_ui->actionPasswordGenerator->toggle(); m_ui->actionPasswordGenerator->blockSignals(blocked); + } else if ((currentIndex == GlobalTotpScreen) != m_ui->actionGlobalTotp->isChecked()) { + bool blocked = m_ui->actionGlobalTotp->blockSignals(true); + m_ui->actionGlobalTotp->toggle(); + m_ui->actionGlobalTotp->blockSignals(blocked); } else if ((currentIndex == SettingsScreen) != m_ui->actionSettings->isChecked()) { bool blocked = m_ui->actionSettings->blockSignals(true); m_ui->actionSettings->toggle(); @@ -1273,6 +1287,16 @@ void MainWindow::togglePasswordGenerator(bool enabled) } } +void MainWindow::toggleGlobalTotp(bool enabled) +{ + if (enabled) { + m_ui->globalTotpDialog->setDatabaseWidget(m_ui->tabWidget->currentDatabaseWidget()); + m_ui->stackedWidget->setCurrentIndex(GlobalTotpScreen); + } else { + switchToDatabases(); + } +} + void MainWindow::switchToNewDatabase() { m_ui->tabWidget->newDatabase(); diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 5d6e070628..86b47024ce 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -59,7 +59,8 @@ class MainWindow : public QMainWindow DatabaseTabScreen = 0, SettingsScreen = 1, WelcomeScreen = 2, - PasswordGeneratorScreen = 3 + PasswordGeneratorScreen = 3, + GlobalTotpScreen = 4 }; signals: @@ -121,6 +122,7 @@ private slots: void switchToDatabases(); void switchToSettings(bool enabled); void togglePasswordGenerator(bool enabled); + void toggleGlobalTotp(bool enabled); void switchToNewDatabase(); void switchToOpenDatabase(); void switchToDatabaseFile(const QString& file); diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index d34d802b27..077c09299d 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -206,6 +206,25 @@ + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + @@ -371,6 +390,7 @@ &Tools + @@ -435,6 +455,7 @@ + @@ -766,6 +787,14 @@ Show Password Generator + + + true + + + &View all TOTP + + false @@ -1308,6 +1337,12 @@ QToolBar
gui/widgets/KPToolBar.h
+ + GlobalTotpDialog + QWidget +
gui/GlobalTotpDialog.h
+ 1 +