From 7ead8e72906a5070976a5086b1214772d6925533 Mon Sep 17 00:00:00 2001 From: shaneknysh Date: Fri, 24 May 2019 16:23:19 -0600 Subject: [PATCH] Add word case option to passphrase generator (#3172) * Closes #1933 * Adds word case options for lower, UPPER, and Title Case to passphrase generation --- src/browser/BrowserSettings.cpp | 1 - src/cli/Diceware.cpp | 2 - src/core/PassphraseGenerator.cpp | 36 +++++++++--- src/core/PassphraseGenerator.h | 9 +++ src/gui/PasswordGeneratorWidget.cpp | 12 +++- src/gui/PasswordGeneratorWidget.ui | 85 +++++++++++++++++++---------- tests/CMakeLists.txt | 3 + tests/TestPassphraseGenerator.cpp | 54 ++++++++++++++++++ tests/TestPassphraseGenerator.h | 32 +++++++++++ 9 files changed, 192 insertions(+), 42 deletions(-) create mode 100644 tests/TestPassphraseGenerator.cpp create mode 100644 tests/TestPassphraseGenerator.h diff --git a/src/browser/BrowserSettings.cpp b/src/browser/BrowserSettings.cpp index e1fc3a2d..fbb19497 100644 --- a/src/browser/BrowserSettings.cpp +++ b/src/browser/BrowserSettings.cpp @@ -510,7 +510,6 @@ QString BrowserSettings::generatePassword() m_passwordGenerator.setFlags(passwordGeneratorFlags()); return m_passwordGenerator.generatePassword(); } else { - m_passPhraseGenerator.setDefaultWordList(); m_passPhraseGenerator.setWordCount(passPhraseWordCount()); m_passPhraseGenerator.setWordSeparator(passPhraseWordSeparator()); return m_passPhraseGenerator.generatePassphrase(); diff --git a/src/cli/Diceware.cpp b/src/cli/Diceware.cpp index c663cfc3..e1448765 100644 --- a/src/cli/Diceware.cpp +++ b/src/cli/Diceware.cpp @@ -73,8 +73,6 @@ int Diceware::execute(const QStringList& arguments) if (!parser.value(wordlistFile).isEmpty()) { dicewareGenerator.setWordList(parser.value(wordlistFile)); - } else { - dicewareGenerator.setDefaultWordList(); } if (!dicewareGenerator.isValid()) { diff --git a/src/core/PassphraseGenerator.cpp b/src/core/PassphraseGenerator.cpp index f972e0ef..a94035e8 100644 --- a/src/core/PassphraseGenerator.cpp +++ b/src/core/PassphraseGenerator.cpp @@ -28,9 +28,11 @@ const char* PassphraseGenerator::DefaultSeparator = " "; const char* PassphraseGenerator::DefaultWordList = "eff_large.wordlist"; PassphraseGenerator::PassphraseGenerator() - : m_wordCount(0) - , m_separator(PassphraseGenerator::DefaultSeparator) + : m_wordCount(DefaultWordCount) + , m_wordCase(LOWERCASE) + , m_separator(DefaultSeparator) { + setDefaultWordList(); } double PassphraseGenerator::calculateEntropy(const QString& passphrase) @@ -46,12 +48,12 @@ double PassphraseGenerator::calculateEntropy(const QString& passphrase) void PassphraseGenerator::setWordCount(int wordCount) { - if (wordCount > 0) { - m_wordCount = wordCount; - } else { - // safe default if something goes wrong - m_wordCount = DefaultWordCount; - } + m_wordCount = qMax(1, wordCount); +} + +void PassphraseGenerator::setWordCase(PassphraseWordCase wordCase) +{ + m_wordCase = wordCase; } void PassphraseGenerator::setWordList(const QString& path) @@ -88,6 +90,7 @@ void PassphraseGenerator::setWordSeparator(const QString& separator) QString PassphraseGenerator::generatePassphrase() const { + QString tmpWord; Q_ASSERT(isValid()); // In case there was an error loading the wordlist @@ -98,7 +101,22 @@ QString PassphraseGenerator::generatePassphrase() const QStringList words; for (int i = 0; i < m_wordCount; ++i) { int wordIndex = randomGen()->randomUInt(static_cast(m_wordlist.length())); - words.append(m_wordlist.at(wordIndex)); + tmpWord = m_wordlist.at(wordIndex); + + // convert case + switch (m_wordCase) { + case UPPERCASE: + tmpWord = tmpWord.toUpper(); + break; + case TITLECASE: + tmpWord = tmpWord.replace(0, 1, tmpWord.left(1).toUpper()); + break; + case LOWERCASE: + default: + tmpWord = tmpWord.toLower(); + break; + } + words.append(tmpWord); } return words.join(m_separator); diff --git a/src/core/PassphraseGenerator.h b/src/core/PassphraseGenerator.h index a4e0e426..3cda1b7c 100644 --- a/src/core/PassphraseGenerator.h +++ b/src/core/PassphraseGenerator.h @@ -28,9 +28,17 @@ public: PassphraseGenerator(); Q_DISABLE_COPY(PassphraseGenerator) + enum PassphraseWordCase + { + LOWERCASE, + UPPERCASE, + TITLECASE + }; + double calculateEntropy(const QString& passphrase); void setWordCount(int wordCount); void setWordList(const QString& path); + void setWordCase(PassphraseWordCase wordCase); void setDefaultWordList(); void setWordSeparator(const QString& separator); bool isValid() const; @@ -43,6 +51,7 @@ public: private: int m_wordCount; + PassphraseWordCase m_wordCase; QString m_separator; QVector m_wordlist; }; diff --git a/src/gui/PasswordGeneratorWidget.cpp b/src/gui/PasswordGeneratorWidget.cpp index 80ea007a..5f3d4349 100644 --- a/src/gui/PasswordGeneratorWidget.cpp +++ b/src/gui/PasswordGeneratorWidget.cpp @@ -60,6 +60,7 @@ PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent) connect(m_ui->comboBoxWordList, SIGNAL(currentIndexChanged(int)), SLOT(updateGenerator())); connect(m_ui->optionButtons, SIGNAL(buttonClicked(int)), SLOT(updateGenerator())); connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(updateGenerator())); + connect(m_ui->wordCaseComboBox, SIGNAL(currentIndexChanged(int)), SLOT(updateGenerator())); // set font size of password quality and entropy labels dynamically to 80% of // the default font size, but make it no smaller than 8pt @@ -74,6 +75,11 @@ PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent) // set default separator to Space m_ui->editWordSeparator->setText(PassphraseGenerator::DefaultSeparator); + // add passphrase generator case options + m_ui->wordCaseComboBox->addItem(tr("lower case"), PassphraseGenerator::LOWERCASE); + m_ui->wordCaseComboBox->addItem(tr("UPPER CASE"), PassphraseGenerator::UPPERCASE); + m_ui->wordCaseComboBox->addItem(tr("Title Case"), PassphraseGenerator::TITLECASE); + QDir path(filePath()->wordlistPath("")); QStringList files = path.entryList(QDir::Files); m_ui->comboBoxWordList->addItems(files); @@ -85,7 +91,6 @@ PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent) m_ui->labelWordList->setVisible(false); } - m_dicewareGenerator->setDefaultWordList(); loadSettings(); reset(); } @@ -139,6 +144,7 @@ void PasswordGeneratorWidget::loadSettings() config()->get("generator/WordSeparator", PassphraseGenerator::DefaultSeparator).toString()); m_ui->comboBoxWordList->setCurrentText( config()->get("generator/WordList", PassphraseGenerator::DefaultWordList).toString()); + m_ui->wordCaseComboBox->setCurrentIndex(config()->get("generator/WordCase", 0).toInt()); // Password or diceware? m_ui->tabWidget->setCurrentIndex(config()->get("generator/Type", 0).toInt()); @@ -174,6 +180,7 @@ void PasswordGeneratorWidget::saveSettings() config()->set("generator/WordCount", m_ui->spinBoxWordCount->value()); config()->set("generator/WordSeparator", m_ui->editWordSeparator->text()); config()->set("generator/WordList", m_ui->comboBoxWordList->currentText()); + config()->set("generator/WordCase", m_ui->wordCaseComboBox->currentIndex()); // Password or diceware? config()->set("generator/Type", m_ui->tabWidget->currentIndex()); @@ -556,6 +563,9 @@ void PasswordGeneratorWidget::updateGenerator() m_updatingSpinBox = false; } + m_dicewareGenerator->setWordCase( + static_cast(m_ui->wordCaseComboBox->currentData().toInt())); + m_ui->spinBoxWordCount->setMinimum(minWordCount); m_ui->sliderWordCount->setMinimum(minWordCount); diff --git a/src/gui/PasswordGeneratorWidget.ui b/src/gui/PasswordGeneratorWidget.ui index ada94d65..ca970fbd 100644 --- a/src/gui/PasswordGeneratorWidget.ui +++ b/src/gui/PasswordGeneratorWidget.ui @@ -927,16 +927,10 @@ QProgressBar::chunk { - - - - - 0 - 0 - - + + - Wordlist: + Word Case: @@ -950,16 +944,39 @@ QProgressBar::chunk { - - + + - Word Co&unt: - - - spinBoxLength + Word Separator: + + + + + 0 + 0 + + + + Wordlist: + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + @@ -1005,10 +1022,13 @@ QProgressBar::chunk { - - + + - Word Separator: + Word Co&unt: + + + spinBoxLength @@ -1020,17 +1040,24 @@ QProgressBar::chunk { - - - Qt::Vertical - - - - 20 - 40 - - - + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a4584aee..2092151f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -190,6 +190,9 @@ add_unit_test(NAME testmerge SOURCES TestMerge.cpp add_unit_test(NAME testpasswordgenerator SOURCES TestPasswordGenerator.cpp LIBS ${TEST_LIBRARIES}) +add_unit_test(NAME testpassphrasegenerator SOURCES TestPassphraseGenerator.cpp + LIBS ${TEST_LIBRARIES}) + add_unit_test(NAME testtotp SOURCES TestTotp.cpp LIBS ${TEST_LIBRARIES}) diff --git a/tests/TestPassphraseGenerator.cpp b/tests/TestPassphraseGenerator.cpp new file mode 100644 index 00000000..9b1ed8ad --- /dev/null +++ b/tests/TestPassphraseGenerator.cpp @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2019 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 "TestPassphraseGenerator.h" +#include "core/PassphraseGenerator.h" +#include "crypto/Crypto.h" + +#include +#include + +QTEST_GUILESS_MAIN(TestPassphraseGenerator) + +void TestPassphraseGenerator::initTestCase() +{ + QVERIFY(Crypto::init()); +} + +void TestPassphraseGenerator::testWordCase() +{ + PassphraseGenerator generator; + generator.setWordSeparator(" "); + QVERIFY(generator.isValid()); + + QString passphrase; + passphrase = generator.generatePassphrase(); + QCOMPARE(passphrase, passphrase.toLower()); + + generator.setWordCase(PassphraseGenerator::LOWERCASE); + passphrase = generator.generatePassphrase(); + QCOMPARE(passphrase, passphrase.toLower()); + + generator.setWordCase(PassphraseGenerator::UPPERCASE); + passphrase = generator.generatePassphrase(); + QCOMPARE(passphrase, passphrase.toUpper()); + + generator.setWordCase(PassphraseGenerator::TITLECASE); + passphrase = generator.generatePassphrase(); + QRegularExpression regex("^([A-Z][a-z]* ?)+$"); + QVERIFY(regex.match(passphrase).hasMatch()); +} diff --git a/tests/TestPassphraseGenerator.h b/tests/TestPassphraseGenerator.h new file mode 100644 index 00000000..ca0fd066 --- /dev/null +++ b/tests/TestPassphraseGenerator.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 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_TESTPASSPHRASEGENERATOR_H +#define KEEPASSXC_TESTPASSPHRASEGENERATOR_H + +#include + +class TestPassphraseGenerator : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void testWordCase(); +}; + +#endif // KEEPASSXC_TESTPASSPHRASEGENERATOR_H