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:
+
+
+ Delete selected wordlist
+
+
+
+ Do you really want to delete the wordlist "%1"?
+
+
+
+ Failed to delete wordlist
+
+
+
+ Add custom wordlist
+
+
+
+ Wordlists
+
+
+
+ All files
+
+
+
+ Failed to add wordlist
+
Word Separator:
@@ -5861,6 +5889,27 @@ We recommend you use the AppImage available on our downloads page.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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