Skip to content

Commit 7811f10

Browse files
authored
Support for wordlists in user configuration directory (#6799)
This commit allows users to put alternative wordlists in a `wordlists` subdirectory below their KeePassXC directory (e.g., under Linux, `~/.config/keepassxc/wordlists`). These wordlists will then appear in the dropdown menu in the *Password Generator* widget. In order to differentiate between lists shipped with KeePassXC and user-provided lists, the former appears with a (SYSTEM) prefix.
1 parent bb88ad6 commit 7811f10

File tree

9 files changed

+213
-21
lines changed

9 files changed

+213
-21
lines changed

COPYING

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ Files: share/icons/application/scalable/actions/chevron-double-down.svg
189189
share/icons/application/scalable/actions/statistics.svg
190190
share/icons/application/scalable/actions/system-help.svg
191191
share/icons/application/scalable/actions/system-search.svg
192+
share/icons/application/scalable/actions/trash.svg
192193
share/icons/application/scalable/actions/url-copy.svg
193194
share/icons/application/scalable/actions/username-copy.svg
194195
share/icons/application/scalable/actions/view-history.svg
Lines changed: 1 addition & 0 deletions
Loading

share/icons/icons.qrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
<file>application/scalable/actions/system-help.svg</file>
7171
<file>application/scalable/actions/system-search.svg</file>
7272
<file>application/scalable/actions/system-software-update.svg</file>
73+
<file>application/scalable/actions/trash.svg</file>
7374
<file>application/scalable/actions/url-copy.svg</file>
7475
<file>application/scalable/actions/user-guide.svg</file>
7576
<file>application/scalable/actions/username-copy.svg</file>

share/translations/keepassxc_en.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5690,6 +5690,34 @@ We recommend you use the AppImage available on our downloads page.</source>
56905690
<source>Wordlist:</source>
56915691
<translation>Wordlist:</translation>
56925692
</message>
5693+
<message>
5694+
<source>Delete selected wordlist</source>
5695+
<translation>Delete selected wordlist</translation>
5696+
</message>
5697+
<message>
5698+
<source>Do you really want to delete the wordlist &quot;%1&quot;?</source>
5699+
<translation>Do you really want to delete the wordlist &quot;%1&quot;?</translation>
5700+
</message>
5701+
<message>
5702+
<source>Failed to delete wordlist</source>
5703+
<translation>Failed to delete wordlist</translation>
5704+
</message>
5705+
<message>
5706+
<source>Add custom wordlist</source>
5707+
<translation>Add custom wordlist</translation>
5708+
</message>
5709+
<message>
5710+
<source>Wordlists</source>
5711+
<translation>Wordlists</translation>
5712+
</message>
5713+
<message>
5714+
<source>All files</source>
5715+
<translation>All files</translation>
5716+
</message>
5717+
<message>
5718+
<source>Failed to add wordlist</source>
5719+
<translation>Failed to add wordlist</translation>
5720+
</message>
56935721
<message>
56945722
<source>Word Separator:</source>
56955723
<translation>Word Separator:</translation>
@@ -5874,6 +5902,27 @@ We recommend you use the AppImage available on our downloads page.</source>
58745902
<source>character</source>
58755903
<translation type="unfinished"></translation>
58765904
</message>
5905+
<message>
5906+
<source>(SYSTEM)</source>
5907+
<translation type="unfinished"></translation>
5908+
</message>
5909+
<message>
5910+
<source>Confirm Delete Wordlist</source>
5911+
<translation type="unfinished"></translation>
5912+
</message>
5913+
<message>
5914+
<source>Select Custom Wordlist</source>
5915+
<translation type="unfinished"></translation>
5916+
</message>
5917+
<message>
5918+
<source>Overwrite Wordlist?</source>
5919+
<translation type="unfinished"></translation>
5920+
</message>
5921+
<message>
5922+
<source>Wordlist &quot;%1&quot; already exists as a custom wordlist.
5923+
Do you want to overwrite it?</source>
5924+
<translation type="unfinished"></translation>
5925+
</message>
58775926
</context>
58785927
<context>
58795928
<name>PickcharsDialog</name>

src/core/Resources.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <QLibrary>
2424

2525
#include "config-keepassx.h"
26+
#include "core/Config.h"
2627
#include "core/Global.h"
2728

2829
Resources* Resources::m_instance(nullptr);
@@ -91,6 +92,12 @@ QString Resources::wordlistPath(const QString& name) const
9192
return dataPath(QStringLiteral("wordlists/%1").arg(name));
9293
}
9394

95+
QString Resources::userWordlistPath(const QString& name) const
96+
{
97+
QString configPath = QFileInfo(config()->getFileName()).absolutePath();
98+
return configPath + QStringLiteral("/wordlists/%1").arg(name);
99+
}
100+
94101
Resources::Resources()
95102
{
96103
const QString appDirPath = QCoreApplication::applicationDirPath();

src/core/Resources.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class Resources
2727
QString dataPath(const QString& name) const;
2828
QString pluginPath(const QString& name) const;
2929
QString wordlistPath(const QString& name) const;
30+
QString userWordlistPath(const QString& name) const;
3031

3132
static Resources* instance();
3233

src/gui/PasswordGeneratorWidget.cpp

Lines changed: 110 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
#include "core/PasswordHealth.h"
2929
#include "core/Resources.h"
3030
#include "gui/Clipboard.h"
31+
#include "gui/FileDialog.h"
3132
#include "gui/Icons.h"
33+
#include "gui/MessageBox.h"
3234
#include "gui/styles/StateColorPalette.h"
3335

3436
PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent)
@@ -43,6 +45,8 @@ PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent)
4345
m_ui->buttonGenerate->setToolTip(
4446
tr("Regenerate password (%1)").arg(m_ui->buttonGenerate->shortcut().toString(QKeySequence::NativeText)));
4547
m_ui->buttonCopy->setIcon(icons()->icon("clipboard-text"));
48+
m_ui->buttonDeleteWordList->setIcon(icons()->icon("trash"));
49+
m_ui->buttonAddWordList->setIcon(icons()->icon("document-new"));
4650
m_ui->buttonClose->setShortcut(Qt::Key_Escape);
4751

4852
// Add two shortcuts to save the form CTRL+Enter and CTRL+S
@@ -60,6 +64,8 @@ PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent)
6064
connect(m_ui->buttonApply, SIGNAL(clicked()), SLOT(applyPassword()));
6165
connect(m_ui->buttonCopy, SIGNAL(clicked()), SLOT(copyPassword()));
6266
connect(m_ui->buttonGenerate, SIGNAL(clicked()), SLOT(regeneratePassword()));
67+
connect(m_ui->buttonDeleteWordList, SIGNAL(clicked()), SLOT(deleteWordList()));
68+
connect(m_ui->buttonAddWordList, SIGNAL(clicked()), SLOT(addWordList()));
6369
connect(m_ui->buttonClose, SIGNAL(clicked()), SIGNAL(closed()));
6470

6571
connect(m_ui->sliderLength, SIGNAL(valueChanged(int)), SLOT(passwordLengthChanged(int)));
@@ -92,15 +98,18 @@ PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent)
9298
m_ui->wordCaseComboBox->addItem(tr("UPPER CASE"), PassphraseGenerator::UPPERCASE);
9399
m_ui->wordCaseComboBox->addItem(tr("Title Case"), PassphraseGenerator::TITLECASE);
94100

101+
// load system-wide wordlists
95102
QDir path(resources()->wordlistPath(""));
96-
QStringList files = path.entryList(QDir::Files);
97-
m_ui->comboBoxWordList->addItems(files);
98-
if (files.size() > 1) {
99-
m_ui->comboBoxWordList->setVisible(true);
100-
m_ui->labelWordList->setVisible(true);
101-
} else {
102-
m_ui->comboBoxWordList->setVisible(false);
103-
m_ui->labelWordList->setVisible(false);
103+
for (const auto& fileName : path.entryList(QDir::Files)) {
104+
m_ui->comboBoxWordList->addItem(tr("(SYSTEM)") + " " + fileName, fileName);
105+
}
106+
107+
m_firstCustomWordlistIndex = m_ui->comboBoxWordList->count();
108+
109+
// load user-provided wordlists
110+
path = QDir(resources()->userWordlistPath(""));
111+
for (const auto& fileName : path.entryList(QDir::Files)) {
112+
m_ui->comboBoxWordList->addItem(fileName, path.absolutePath() + QDir::separator() + fileName);
104113
}
105114

106115
loadSettings();
@@ -164,7 +173,10 @@ void PasswordGeneratorWidget::loadSettings()
164173
// Diceware config
165174
m_ui->spinBoxWordCount->setValue(config()->get(Config::PasswordGenerator_WordCount).toInt());
166175
m_ui->editWordSeparator->setText(config()->get(Config::PasswordGenerator_WordSeparator).toString());
167-
m_ui->comboBoxWordList->setCurrentText(config()->get(Config::PasswordGenerator_WordList).toString());
176+
int i = m_ui->comboBoxWordList->findData(config()->get(Config::PasswordGenerator_WordList).toString());
177+
if (i > -1) {
178+
m_ui->comboBoxWordList->setCurrentIndex(i);
179+
}
168180
m_ui->wordCaseComboBox->setCurrentIndex(config()->get(Config::PasswordGenerator_WordCase).toInt());
169181

170182
// Password or diceware?
@@ -205,7 +217,7 @@ void PasswordGeneratorWidget::saveSettings()
205217
// Diceware config
206218
config()->set(Config::PasswordGenerator_WordCount, m_ui->spinBoxWordCount->value());
207219
config()->set(Config::PasswordGenerator_WordSeparator, m_ui->editWordSeparator->text());
208-
config()->set(Config::PasswordGenerator_WordList, m_ui->comboBoxWordList->currentText());
220+
config()->set(Config::PasswordGenerator_WordList, m_ui->comboBoxWordList->currentData());
209221
config()->set(Config::PasswordGenerator_WordCase, m_ui->wordCaseComboBox->currentIndex());
210222

211223
// Password or diceware?
@@ -329,6 +341,86 @@ bool PasswordGeneratorWidget::isPasswordVisible() const
329341
return m_ui->editNewPassword->isPasswordVisible();
330342
}
331343

344+
void PasswordGeneratorWidget::deleteWordList()
345+
{
346+
if (m_ui->comboBoxWordList->currentIndex() < m_firstCustomWordlistIndex) {
347+
return;
348+
}
349+
350+
QFile file(m_ui->comboBoxWordList->currentData().toString());
351+
if (!file.exists()) {
352+
return;
353+
}
354+
355+
auto result = MessageBox::question(this,
356+
tr("Confirm Delete Wordlist"),
357+
tr("Do you really want to delete the wordlist \"%1\"?").arg(file.fileName()),
358+
MessageBox::Delete | MessageBox::Cancel,
359+
MessageBox::Cancel);
360+
if (result != MessageBox::Delete) {
361+
return;
362+
}
363+
364+
if (!file.remove()) {
365+
MessageBox::critical(this, tr("Failed to delete wordlist"), file.errorString());
366+
return;
367+
}
368+
369+
m_ui->comboBoxWordList->removeItem(m_ui->comboBoxWordList->currentIndex());
370+
updateGenerator();
371+
}
372+
373+
void PasswordGeneratorWidget::addWordList()
374+
{
375+
auto filter = QString("%1 (*.txt *.asc *.wordlist);;%2 (*)").arg(tr("Wordlists"), tr("All files"));
376+
auto filePath = fileDialog()->getOpenFileName(this, tr("Select Custom Wordlist"), "", filter);
377+
if (filePath.isEmpty()) {
378+
return;
379+
}
380+
381+
// create directory for user-specified wordlists, if necessary
382+
QDir destDir(resources()->userWordlistPath(""));
383+
destDir.mkpath(".");
384+
385+
// check if destination wordlist already exists
386+
QString fileName = QFileInfo(filePath).fileName();
387+
QString destPath = destDir.absolutePath() + QDir::separator() + fileName;
388+
QFile dest(destPath);
389+
if (dest.exists()) {
390+
auto response = MessageBox::warning(this,
391+
tr("Overwrite Wordlist?"),
392+
tr("Wordlist \"%1\" already exists as a custom wordlist.\n"
393+
"Do you want to overwrite it?")
394+
.arg(fileName),
395+
MessageBox::Overwrite | MessageBox::Cancel,
396+
MessageBox::Cancel);
397+
if (response != MessageBox::Overwrite) {
398+
return;
399+
}
400+
if (!dest.remove()) {
401+
MessageBox::critical(this, tr("Failed to delete wordlist"), dest.errorString());
402+
return;
403+
}
404+
}
405+
406+
// copy wordlist to destination path and add corresponding item to the combo box
407+
QFile file(filePath);
408+
if (!file.copy(destPath)) {
409+
MessageBox::critical(this, tr("Failed to add wordlist"), file.errorString());
410+
return;
411+
}
412+
413+
auto index = m_ui->comboBoxWordList->findData(destPath);
414+
if (index == -1) {
415+
m_ui->comboBoxWordList->addItem(fileName, destPath);
416+
index = m_ui->comboBoxWordList->count() - 1;
417+
}
418+
m_ui->comboBoxWordList->setCurrentIndex(index);
419+
420+
// update the password generator
421+
updateGenerator();
422+
}
423+
332424
void PasswordGeneratorWidget::setAdvancedMode(bool advanced)
333425
{
334426
saveSettings();
@@ -540,10 +632,15 @@ void PasswordGeneratorWidget::updateGenerator()
540632
static_cast<PassphraseGenerator::PassphraseWordCase>(m_ui->wordCaseComboBox->currentData().toInt()));
541633

542634
m_dicewareGenerator->setWordCount(m_ui->spinBoxWordCount->value());
543-
if (!m_ui->comboBoxWordList->currentText().isEmpty()) {
544-
QString path = resources()->wordlistPath(m_ui->comboBoxWordList->currentText());
545-
m_dicewareGenerator->setWordList(path);
635+
auto path = m_ui->comboBoxWordList->currentData().toString();
636+
if (m_ui->comboBoxWordList->currentIndex() < m_firstCustomWordlistIndex) {
637+
path = resources()->wordlistPath(path);
638+
m_ui->buttonDeleteWordList->setEnabled(false);
639+
} else {
640+
m_ui->buttonDeleteWordList->setEnabled(true);
546641
}
642+
m_dicewareGenerator->setWordList(path);
643+
547644
m_dicewareGenerator->setWordSeparator(m_ui->editWordSeparator->text());
548645

549646
if (m_dicewareGenerator->isValid()) {

src/gui/PasswordGeneratorWidget.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ public slots:
6161
void applyPassword();
6262
void copyPassword();
6363
void setPasswordVisible(bool visible);
64+
void deleteWordList();
65+
void addWordList();
6466

6567
signals:
6668
void appliedPassword(const QString& password);
@@ -80,6 +82,7 @@ private slots:
8082

8183
private:
8284
bool m_standalone = false;
85+
int m_firstCustomWordlistIndex;
8386

8487
void closeEvent(QCloseEvent* event);
8588
PasswordGenerator::CharClasses charClasses();

src/gui/PasswordGeneratorWidget.ui

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -862,14 +862,44 @@ QProgressBar::chunk {
862862
</layout>
863863
</item>
864864
<item row="0" column="2">
865-
<widget class="QComboBox" name="comboBoxWordList">
866-
<property name="sizePolicy">
867-
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
868-
<horstretch>0</horstretch>
869-
<verstretch>0</verstretch>
870-
</sizepolicy>
871-
</property>
872-
</widget>
865+
<layout class="QHBoxLayout" name="horizontalLayout_10">
866+
<item>
867+
<widget class="QComboBox" name="comboBoxWordList">
868+
<property name="sizePolicy">
869+
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
870+
<horstretch>0</horstretch>
871+
<verstretch>0</verstretch>
872+
</sizepolicy>
873+
</property>
874+
</widget>
875+
</item>
876+
<item>
877+
<widget class="QPushButton" name="buttonDeleteWordList">
878+
<property name="focusPolicy">
879+
<enum>Qt::TabFocus</enum>
880+
</property>
881+
<property name="toolTip">
882+
<string>Delete selected wordlist</string>
883+
</property>
884+
<property name="accessibleDescription">
885+
<string>Delete selected wordlist</string>
886+
</property>
887+
</widget>
888+
</item>
889+
<item>
890+
<widget class="QPushButton" name="buttonAddWordList">
891+
<property name="focusPolicy">
892+
<enum>Qt::TabFocus</enum>
893+
</property>
894+
<property name="toolTip">
895+
<string>Add custom wordlist</string>
896+
</property>
897+
<property name="accessibleDescription">
898+
<string>Add custom wordlist</string>
899+
</property>
900+
</widget>
901+
</item>
902+
</layout>
873903
</item>
874904
<item row="4" column="2" alignment="Qt::AlignLeft">
875905
<widget class="QLabel" name="charactersInPassphraseLabel">
@@ -990,6 +1020,8 @@ QProgressBar::chunk {
9901020
<tabstop>checkBoxExcludeAlike</tabstop>
9911021
<tabstop>checkBoxEnsureEvery</tabstop>
9921022
<tabstop>comboBoxWordList</tabstop>
1023+
<tabstop>buttonDeleteWordList</tabstop>
1024+
<tabstop>buttonAddWordList</tabstop>
9931025
<tabstop>sliderWordCount</tabstop>
9941026
<tabstop>spinBoxWordCount</tabstop>
9951027
<tabstop>editWordSeparator</tabstop>

0 commit comments

Comments
 (0)