diff --git a/COPYING b/COPYING index 9806cb92ce..797bc7b07a 100644 --- a/COPYING +++ b/COPYING @@ -189,6 +189,7 @@ Files: share/icons/application/scalable/actions/chevron-double-down.svg share/icons/application/scalable/actions/statistics.svg share/icons/application/scalable/actions/system-help.svg share/icons/application/scalable/actions/system-search.svg + share/icons/application/scalable/actions/trash.svg share/icons/application/scalable/actions/url-copy.svg share/icons/application/scalable/actions/username-copy.svg share/icons/application/scalable/actions/view-history.svg diff --git a/share/icons/application/scalable/actions/trash.svg b/share/icons/application/scalable/actions/trash.svg new file mode 100644 index 0000000000..7d2c502a96 --- /dev/null +++ b/share/icons/application/scalable/actions/trash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/icons.qrc b/share/icons/icons.qrc index 776961e72c..05e880c0e0 100644 --- a/share/icons/icons.qrc +++ b/share/icons/icons.qrc @@ -70,6 +70,7 @@ application/scalable/actions/system-help.svg application/scalable/actions/system-search.svg application/scalable/actions/system-software-update.svg + application/scalable/actions/trash.svg application/scalable/actions/url-copy.svg application/scalable/actions/user-guide.svg application/scalable/actions/username-copy.svg diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index 71529937c2..77e8fb22df 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -5677,6 +5677,34 @@ We recommend you use the AppImage available on our downloads page. Wordlist: Wordlist: + + Delete selected wordlist + Delete selected wordlist + + + Do you really want to delete the wordlist "%1"? + Do you really want to delete the wordlist "%1"? + + + Failed to delete wordlist + Failed to delete wordlist + + + Add custom wordlist + Add custom wordlist + + + Wordlists + Wordlists + + + All files + All files + + + Failed to add wordlist + Failed to add wordlist + Word Separator: Word Separator: @@ -5861,6 +5889,27 @@ We recommend you use the AppImage available on our downloads page. character + + (SYSTEM) + + + + Confirm Delete Wordlist + + + + Select Custom Wordlist + + + + Overwrite Wordlist? + + + + Wordlist "%1" already exists as a custom wordlist. +Do you want to overwrite it? + + PickcharsDialog diff --git a/src/core/Resources.cpp b/src/core/Resources.cpp index c9eb4cb6c5..0cad907f6a 100644 --- a/src/core/Resources.cpp +++ b/src/core/Resources.cpp @@ -23,6 +23,7 @@ #include #include "config-keepassx.h" +#include "core/Config.h" #include "core/Global.h" Resources* Resources::m_instance(nullptr); @@ -91,6 +92,12 @@ QString Resources::wordlistPath(const QString& name) const return dataPath(QStringLiteral("wordlists/%1").arg(name)); } +QString Resources::userWordlistPath(const QString& name) const +{ + QString configPath = QFileInfo(config()->getFileName()).absolutePath(); + return configPath + QStringLiteral("/wordlists/%1").arg(name); +} + Resources::Resources() { const QString appDirPath = QCoreApplication::applicationDirPath(); diff --git a/src/core/Resources.h b/src/core/Resources.h index 0a8b85c604..3af6d7b3bc 100644 --- a/src/core/Resources.h +++ b/src/core/Resources.h @@ -27,6 +27,7 @@ class Resources QString dataPath(const QString& name) const; QString pluginPath(const QString& name) const; QString wordlistPath(const QString& name) const; + QString userWordlistPath(const QString& name) const; static Resources* instance(); diff --git a/src/gui/PasswordGeneratorWidget.cpp b/src/gui/PasswordGeneratorWidget.cpp index acb93b630d..26d5e558b8 100644 --- a/src/gui/PasswordGeneratorWidget.cpp +++ b/src/gui/PasswordGeneratorWidget.cpp @@ -27,7 +27,9 @@ #include "core/PasswordHealth.h" #include "core/Resources.h" #include "gui/Clipboard.h" +#include "gui/FileDialog.h" #include "gui/Icons.h" +#include "gui/MessageBox.h" #include "gui/styles/StateColorPalette.h" PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent) @@ -42,6 +44,8 @@ PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent) m_ui->buttonGenerate->setToolTip( tr("Regenerate password (%1)").arg(m_ui->buttonGenerate->shortcut().toString(QKeySequence::NativeText))); m_ui->buttonCopy->setIcon(icons()->icon("clipboard-text")); + m_ui->buttonDeleteWordList->setIcon(icons()->icon("trash")); + m_ui->buttonAddWordList->setIcon(icons()->icon("document-new")); m_ui->buttonClose->setShortcut(Qt::Key_Escape); // Add two shortcuts to save the form CTRL+Enter and CTRL+S @@ -59,6 +63,8 @@ PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent) connect(m_ui->buttonApply, SIGNAL(clicked()), SLOT(applyPassword())); connect(m_ui->buttonCopy, SIGNAL(clicked()), SLOT(copyPassword())); connect(m_ui->buttonGenerate, SIGNAL(clicked()), SLOT(regeneratePassword())); + connect(m_ui->buttonDeleteWordList, SIGNAL(clicked()), SLOT(deleteWordList())); + connect(m_ui->buttonAddWordList, SIGNAL(clicked()), SLOT(addWordList())); connect(m_ui->buttonClose, SIGNAL(clicked()), SIGNAL(closed())); connect(m_ui->sliderLength, SIGNAL(valueChanged(int)), SLOT(passwordLengthChanged(int))); @@ -91,15 +97,18 @@ PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent) m_ui->wordCaseComboBox->addItem(tr("UPPER CASE"), PassphraseGenerator::UPPERCASE); m_ui->wordCaseComboBox->addItem(tr("Title Case"), PassphraseGenerator::TITLECASE); + // load system-wide wordlists QDir path(resources()->wordlistPath("")); - QStringList files = path.entryList(QDir::Files); - m_ui->comboBoxWordList->addItems(files); - if (files.size() > 1) { - m_ui->comboBoxWordList->setVisible(true); - m_ui->labelWordList->setVisible(true); - } else { - m_ui->comboBoxWordList->setVisible(false); - m_ui->labelWordList->setVisible(false); + for (const auto& fileName : path.entryList(QDir::Files)) { + m_ui->comboBoxWordList->addItem(tr("(SYSTEM)") + " " + fileName, fileName); + } + + m_firstCustomWordlistIndex = m_ui->comboBoxWordList->count(); + + // load user-provided wordlists + path = QDir(resources()->userWordlistPath("")); + for (const auto& fileName : path.entryList(QDir::Files)) { + m_ui->comboBoxWordList->addItem(fileName, path.absolutePath() + QDir::separator() + fileName); } loadSettings(); @@ -156,7 +165,10 @@ void PasswordGeneratorWidget::loadSettings() // Diceware config m_ui->spinBoxWordCount->setValue(config()->get(Config::PasswordGenerator_WordCount).toInt()); m_ui->editWordSeparator->setText(config()->get(Config::PasswordGenerator_WordSeparator).toString()); - m_ui->comboBoxWordList->setCurrentText(config()->get(Config::PasswordGenerator_WordList).toString()); + int i = m_ui->comboBoxWordList->findData(config()->get(Config::PasswordGenerator_WordList).toString()); + if (i > -1) { + m_ui->comboBoxWordList->setCurrentIndex(i); + } m_ui->wordCaseComboBox->setCurrentIndex(config()->get(Config::PasswordGenerator_WordCase).toInt()); // Password or diceware? @@ -197,7 +209,7 @@ void PasswordGeneratorWidget::saveSettings() // Diceware config config()->set(Config::PasswordGenerator_WordCount, m_ui->spinBoxWordCount->value()); config()->set(Config::PasswordGenerator_WordSeparator, m_ui->editWordSeparator->text()); - config()->set(Config::PasswordGenerator_WordList, m_ui->comboBoxWordList->currentText()); + config()->set(Config::PasswordGenerator_WordList, m_ui->comboBoxWordList->currentData()); config()->set(Config::PasswordGenerator_WordCase, m_ui->wordCaseComboBox->currentIndex()); // Password or diceware? @@ -321,6 +333,86 @@ bool PasswordGeneratorWidget::isPasswordVisible() const return m_ui->editNewPassword->isPasswordVisible(); } +void PasswordGeneratorWidget::deleteWordList() +{ + if (m_ui->comboBoxWordList->currentIndex() < m_firstCustomWordlistIndex) { + return; + } + + QFile file(m_ui->comboBoxWordList->currentData().toString()); + if (!file.exists()) { + return; + } + + auto result = MessageBox::question(this, + tr("Confirm Delete Wordlist"), + tr("Do you really want to delete the wordlist \"%1\"?").arg(file.fileName()), + MessageBox::Delete | MessageBox::Cancel, + MessageBox::Cancel); + if (result != MessageBox::Delete) { + return; + } + + if (!file.remove()) { + MessageBox::critical(this, tr("Failed to delete wordlist"), file.errorString()); + return; + } + + m_ui->comboBoxWordList->removeItem(m_ui->comboBoxWordList->currentIndex()); + updateGenerator(); +} + +void PasswordGeneratorWidget::addWordList() +{ + auto filter = QString("%1 (*.txt *.asc *.wordlist);;%2 (*)").arg(tr("Wordlists"), tr("All files")); + auto filePath = fileDialog()->getOpenFileName(this, tr("Select Custom Wordlist"), "", filter); + if (filePath.isEmpty()) { + return; + } + + // create directory for user-specified wordlists, if necessary + QDir destDir(resources()->userWordlistPath("")); + destDir.mkpath("."); + + // check if destination wordlist already exists + QString fileName = QFileInfo(filePath).fileName(); + QString destPath = destDir.absolutePath() + QDir::separator() + fileName; + QFile dest(destPath); + if (dest.exists()) { + auto response = MessageBox::warning(this, + tr("Overwrite Wordlist?"), + tr("Wordlist \"%1\" already exists as a custom wordlist.\n" + "Do you want to overwrite it?") + .arg(fileName), + MessageBox::Overwrite | MessageBox::Cancel, + MessageBox::Cancel); + if (response != MessageBox::Overwrite) { + return; + } + if (!dest.remove()) { + MessageBox::critical(this, tr("Failed to delete wordlist"), dest.errorString()); + return; + } + } + + // copy wordlist to destination path and add corresponding item to the combo box + QFile file(filePath); + if (!file.copy(destPath)) { + MessageBox::critical(this, tr("Failed to add wordlist"), file.errorString()); + return; + } + + auto index = m_ui->comboBoxWordList->findData(destPath); + if (index == -1) { + m_ui->comboBoxWordList->addItem(fileName, destPath); + index = m_ui->comboBoxWordList->count() - 1; + } + m_ui->comboBoxWordList->setCurrentIndex(index); + + // update the password generator + updateGenerator(); +} + void PasswordGeneratorWidget::setAdvancedMode(bool advanced) { saveSettings(); @@ -532,10 +624,15 @@ void PasswordGeneratorWidget::updateGenerator() static_cast(m_ui->wordCaseComboBox->currentData().toInt())); m_dicewareGenerator->setWordCount(m_ui->spinBoxWordCount->value()); - if (!m_ui->comboBoxWordList->currentText().isEmpty()) { - QString path = resources()->wordlistPath(m_ui->comboBoxWordList->currentText()); - m_dicewareGenerator->setWordList(path); + auto path = m_ui->comboBoxWordList->currentData().toString(); + if (m_ui->comboBoxWordList->currentIndex() < m_firstCustomWordlistIndex) { + path = resources()->wordlistPath(path); + m_ui->buttonDeleteWordList->setEnabled(false); + } else { + m_ui->buttonDeleteWordList->setEnabled(true); } + m_dicewareGenerator->setWordList(path); + m_dicewareGenerator->setWordSeparator(m_ui->editWordSeparator->text()); if (m_dicewareGenerator->isValid()) { diff --git a/src/gui/PasswordGeneratorWidget.h b/src/gui/PasswordGeneratorWidget.h index 832025d46a..2c15372c7c 100644 --- a/src/gui/PasswordGeneratorWidget.h +++ b/src/gui/PasswordGeneratorWidget.h @@ -61,6 +61,8 @@ public slots: void applyPassword(); void copyPassword(); void setPasswordVisible(bool visible); + void deleteWordList(); + void addWordList(); signals: void appliedPassword(const QString& password); @@ -80,6 +82,7 @@ private slots: private: bool m_standalone = false; + int m_firstCustomWordlistIndex; PasswordGenerator::CharClasses charClasses(); PasswordGenerator::GeneratorFlags generatorFlags(); diff --git a/src/gui/PasswordGeneratorWidget.ui b/src/gui/PasswordGeneratorWidget.ui index 01ccedc019..547c5a0ab5 100644 --- a/src/gui/PasswordGeneratorWidget.ui +++ b/src/gui/PasswordGeneratorWidget.ui @@ -862,14 +862,44 @@ QProgressBar::chunk { - - - - 0 - 0 - - - + + + + + + 0 + 0 + + + + + + + + Qt::TabFocus + + + Delete selected wordlist + + + Delete selected wordlist + + + + + + + Qt::TabFocus + + + Add custom wordlist + + + Add custom wordlist + + + + @@ -990,6 +1020,8 @@ QProgressBar::chunk { checkBoxExcludeAlike checkBoxEnsureEvery comboBoxWordList + buttonDeleteWordList + buttonAddWordList sliderWordCount spinBoxWordCount editWordSeparator