Skip to content

Commit

Permalink
Support for wordlists in user configuration directory
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
snipfoo authored and droidmonkey committed Oct 17, 2021
1 parent b6716bd commit 08ed9bf
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 21 deletions.
1 change: 1 addition & 0 deletions COPYING
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions share/icons/application/scalable/actions/trash.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions share/icons/icons.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
<file>application/scalable/actions/system-help.svg</file>
<file>application/scalable/actions/system-search.svg</file>
<file>application/scalable/actions/system-software-update.svg</file>
<file>application/scalable/actions/trash.svg</file>
<file>application/scalable/actions/url-copy.svg</file>
<file>application/scalable/actions/user-guide.svg</file>
<file>application/scalable/actions/username-copy.svg</file>
Expand Down
50 changes: 50 additions & 0 deletions share/translations/keepassxc_en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5677,6 +5677,56 @@ We recommend you use the AppImage available on our downloads page.</source>
<source>Wordlist:</source>
<translation>Wordlist:</translation>
</message>
<message>
<source>(CUSTOM)</source>
<translation>(CUSTOM)</translation>
</message>
<message>
<source>Delete selected wordlist</source>
<translation>Delete selected wordlist</translation>
</message>
<message>
<source>Delete the selected wordlist?</source>
<translation>Delete the selected wordlist?</translation>
</message>
<message>
<source>Do you really want to delete the wordlist &quot;%1&quot;?</source>
<translation>Do you really want to delete the wordlist &quot;%1&quot;?</translation>
</message>
<message>
<source>Failed to delete wordlist</source>
<translation>Failed to delete wordlist</translation>
</message>
<message>
<source>Cannot delete the system wordlist &quot;%1&quot;</source>
<translation>Cannot delete the system wordlist &quot;%1&quot;</translation>
</message>
<message>
<source>Add custom wordlist</source>
<translation>Add custom wordlist</translation>
</message>
<message>
<source>Wordlists</source>
<translation>Wordlists</translation>
</message>
<message>
<source>All files</source>
<translation>All files</translation>
</message>
<message>
<source>Wordlist already exists</source>
<translation>Wordlist already exists</translation>
</message>
<message>
<source>Wordlist &quot;%1&quot; already exists.
Do you want to overwrite it?</source>
<translation>Wordlist &quot;%1&quot; already exists.
Do you want to overwrite it?</translation>
</message>
<message>
<source>Failed to add wordlist</source>
<translation>Failed to add wordlist</translation>
</message>
<message>
<source>Word Separator:</source>
<translation>Word Separator:</translation>
Expand Down
7 changes: 7 additions & 0 deletions src/core/Resources.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include <QLibrary>

#include "config-keepassx.h"
#include "core/Config.h"
#include "core/Global.h"

Resources* Resources::m_instance(nullptr);
Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/core/Resources.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
123 changes: 110 additions & 13 deletions src/gui/PasswordGeneratorWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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)));
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -532,10 +624,15 @@ void PasswordGeneratorWidget::updateGenerator()
static_cast<PassphraseGenerator::PassphraseWordCase>(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()) {
Expand Down
3 changes: 3 additions & 0 deletions src/gui/PasswordGeneratorWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public slots:
void applyPassword();
void copyPassword();
void setPasswordVisible(bool visible);
void deleteWordList();
void addWordList();

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

private:
bool m_standalone = false;
int m_firstCustomWordlistIndex;

PasswordGenerator::CharClasses charClasses();
PasswordGenerator::GeneratorFlags generatorFlags();
Expand Down
48 changes: 40 additions & 8 deletions src/gui/PasswordGeneratorWidget.ui
Original file line number Diff line number Diff line change
Expand Up @@ -862,14 +862,44 @@ QProgressBar::chunk {
</layout>
</item>
<item row="0" column="2">
<widget class="QComboBox" name="comboBoxWordList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QComboBox" name="comboBoxWordList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonDeleteWordList">
<property name="focusPolicy">
<enum>Qt::TabFocus</enum>
</property>
<property name="toolTip">
<string>Delete selected wordlist</string>
</property>
<property name="accessibleDescription">
<string>Delete selected wordlist</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonAddWordList">
<property name="focusPolicy">
<enum>Qt::TabFocus</enum>
</property>
<property name="toolTip">
<string>Add custom wordlist</string>
</property>
<property name="accessibleDescription">
<string>Add custom wordlist</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="2" alignment="Qt::AlignLeft">
<widget class="QLabel" name="charactersInPassphraseLabel">
Expand Down Expand Up @@ -990,6 +1020,8 @@ QProgressBar::chunk {
<tabstop>checkBoxExcludeAlike</tabstop>
<tabstop>checkBoxEnsureEvery</tabstop>
<tabstop>comboBoxWordList</tabstop>
<tabstop>buttonDeleteWordList</tabstop>
<tabstop>buttonAddWordList</tabstop>
<tabstop>sliderWordCount</tabstop>
<tabstop>spinBoxWordCount</tabstop>
<tabstop>editWordSeparator</tabstop>
Expand Down

0 comments on commit 08ed9bf

Please sign in to comment.