From a0a063b57f6f577bed505ccd652763eeadd1b876 Mon Sep 17 00:00:00 2001 From: Patrick Klein <42714034+libklein@users.noreply.github.com> Date: Wed, 8 Dec 2021 05:40:09 +0100 Subject: [PATCH] Add -i/--include option to "generate" CLI command. (#7112) --- share/translations/keepassxc_en.ts | 4 + src/cli/Generate.cpp | 19 +- src/cli/Generate.h | 1 + src/core/PasswordGenerator.cpp | 121 +++++----- src/core/PasswordGenerator.h | 27 ++- src/gui/PasswordGeneratorWidget.cpp | 48 +--- tests/TestCli.cpp | 4 + tests/TestPasswordGenerator.cpp | 332 ++++++++++++++++++++-------- tests/TestPasswordGenerator.h | 16 +- 9 files changed, 359 insertions(+), 213 deletions(-) diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index 6e6f6919f..2ea2c0f72 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -7557,6 +7557,10 @@ Please consider generating a new key file. AES-KDF (KDBX 3) AES-KDF (KDBX 3.1) {3)?} + + Use custom character set + + QtIOCompressor diff --git a/src/cli/Generate.cpp b/src/cli/Generate.cpp index 9c2e57e09..8ce1a6116 100644 --- a/src/cli/Generate.cpp +++ b/src/cli/Generate.cpp @@ -53,6 +53,12 @@ const QCommandLineOption Generate::ExcludeCharsOption = QCommandLineOption(QStri QObject::tr("Exclude character set"), QObject::tr("chars")); +const QCommandLineOption Generate::CustomCharacterSetOption = + QCommandLineOption(QStringList() << "c" + << "custom", + QObject::tr("Use custom character set"), + QObject::tr("chars")); + const QCommandLineOption Generate::ExcludeSimilarCharsOption = QCommandLineOption(QStringList() << "exclude-similar", QObject::tr("Exclude similar looking characters")); @@ -71,6 +77,7 @@ Generate::Generate() options.append(Generate::ExcludeCharsOption); options.append(Generate::ExcludeSimilarCharsOption); options.append(Generate::IncludeEveryGroupOption); + options.append(Generate::CustomCharacterSetOption); } /** @@ -120,9 +127,15 @@ QSharedPointer Generate::createGenerator(QSharedPointersetCharClasses(classes); - passwordGenerator->setFlags(flags); - passwordGenerator->setExcludedChars(parser->value(Generate::ExcludeCharsOption)); + if (flags != 0x0) { + passwordGenerator->setFlags(flags); + } + QString customCharacterSet = parser->value(Generate::CustomCharacterSetOption); + if (classes != 0x0 || !customCharacterSet.isNull()) { + passwordGenerator->setCharClasses(classes); + } + passwordGenerator->setCustomCharacterSet(customCharacterSet); + passwordGenerator->setExcludedCharacterSet(parser->value(Generate::ExcludeCharsOption)); if (!passwordGenerator->isValid()) { err << QObject::tr("Invalid password generator after applying all options") << endl; diff --git a/src/cli/Generate.h b/src/cli/Generate.h index d77e1a087..6b3e81651 100644 --- a/src/cli/Generate.h +++ b/src/cli/Generate.h @@ -39,6 +39,7 @@ public: static const QCommandLineOption ExcludeCharsOption; static const QCommandLineOption ExcludeSimilarCharsOption; static const QCommandLineOption IncludeEveryGroupOption; + static const QCommandLineOption CustomCharacterSetOption; }; #endif // KEEPASSXC_GENERATE_H diff --git a/src/core/PasswordGenerator.cpp b/src/core/PasswordGenerator.cpp index bd9dcc67b..1051dba2b 100644 --- a/src/core/PasswordGenerator.cpp +++ b/src/core/PasswordGenerator.cpp @@ -20,51 +20,43 @@ #include "crypto/Random.h" -const char* PasswordGenerator::DefaultAdditionalChars = ""; +const int PasswordGenerator::DefaultLength = 32; +const char* PasswordGenerator::DefaultCustomCharacterSet = ""; const char* PasswordGenerator::DefaultExcludedChars = ""; PasswordGenerator::PasswordGenerator() - : m_length(0) - , m_classes(nullptr) - , m_flags(nullptr) - , m_additional(PasswordGenerator::DefaultAdditionalChars) + : m_length(PasswordGenerator::DefaultLength) + , m_classes(PasswordGenerator::CharClass::DefaultCharset) + , m_flags(PasswordGenerator::GeneratorFlag::DefaultFlags) + , m_custom(PasswordGenerator::DefaultCustomCharacterSet) , m_excluded(PasswordGenerator::DefaultExcludedChars) { } void PasswordGenerator::setLength(int length) { - if (length <= 0) { - m_length = DefaultLength; - return; - } m_length = length; } -void PasswordGenerator::setCharClasses(const CharClasses& classes) +void PasswordGenerator::setCharClasses(const PasswordGenerator::CharClasses& classes) { - if (classes == 0) { - m_classes = DefaultCharset; - return; - } m_classes = classes; } +void PasswordGenerator::setCustomCharacterSet(const QString& customCharacterSet) +{ + m_custom = customCharacterSet; +} +void PasswordGenerator::setExcludedCharacterSet(const QString& excludedCharacterSet) +{ + m_excluded = excludedCharacterSet; +} + void PasswordGenerator::setFlags(const GeneratorFlags& flags) { m_flags = flags; } -void PasswordGenerator::setAdditionalChars(const QString& chars) -{ - m_additional = chars; -} - -void PasswordGenerator::setExcludedChars(const QString& chars) -{ - m_excluded = chars; -} - QString PasswordGenerator::generatePassword() const { Q_ASSERT(isValid()); @@ -114,9 +106,9 @@ QString PasswordGenerator::generatePassword() const bool PasswordGenerator::isValid() const { - if (m_classes == 0 && m_additional.isEmpty()) { + if (m_classes == CharClass::NoClass && m_custom.isEmpty()) { return false; - } else if (m_length == 0) { + } else if (m_length <= 0) { return false; } @@ -266,10 +258,10 @@ QVector PasswordGenerator::passwordGroups() const passwordGroups.append(group); } - if (!m_additional.isEmpty()) { + if (!m_custom.isEmpty()) { PasswordGroup group; - for (auto ch : m_additional) { + for (auto ch : m_custom) { group.append(ch); } @@ -302,38 +294,43 @@ QVector PasswordGenerator::passwordGroups() const int PasswordGenerator::numCharClasses() const { - int numClasses = 0; - - if (m_classes & LowerLetters) { - numClasses++; - } - if (m_classes & UpperLetters) { - numClasses++; - } - if (m_classes & Numbers) { - numClasses++; - } - if (m_classes & Braces) { - numClasses++; - } - if (m_classes & Punctuation) { - numClasses++; - } - if (m_classes & Quotes) { - numClasses++; - } - if (m_classes & Dashes) { - numClasses++; - } - if (m_classes & Math) { - numClasses++; - } - if (m_classes & Logograms) { - numClasses++; - } - if (m_classes & EASCII) { - numClasses++; - } - - return numClasses; + // Actually compute the non empty password groups + auto non_empty_groups = passwordGroups(); + return non_empty_groups.size(); +} + +int PasswordGenerator::getMinLength() const +{ + if ((m_flags & CharFromEveryGroup)) { + return numCharClasses(); + } + return 1; +} +void PasswordGenerator::reset() +{ + m_classes = CharClass::DefaultCharset; + m_flags = GeneratorFlag::DefaultFlags; + m_custom = DefaultCustomCharacterSet; + m_excluded = DefaultExcludedChars; + m_length = DefaultLength; +} +int PasswordGenerator::getLength() const +{ + return m_length; +} +const PasswordGenerator::GeneratorFlags& PasswordGenerator::getFlags() const +{ + return m_flags; +} +const PasswordGenerator::CharClasses& PasswordGenerator::getActiveClasses() const +{ + return m_classes; +} +const QString& PasswordGenerator::getCustomCharacterSet() const +{ + return m_custom; +} +const QString& PasswordGenerator::getExcludedCharacterSet() const +{ + return m_excluded; } diff --git a/src/core/PasswordGenerator.h b/src/core/PasswordGenerator.h index 7ea7eb155..34807bc05 100644 --- a/src/core/PasswordGenerator.h +++ b/src/core/PasswordGenerator.h @@ -19,6 +19,7 @@ #ifndef KEEPASSX_PASSWORDGENERATOR_H #define KEEPASSX_PASSWORDGENERATOR_H +#include #include typedef QVector PasswordGroup; @@ -28,6 +29,7 @@ class PasswordGenerator public: enum CharClass { + NoClass = 0, LowerLetters = (1 << 0), UpperLetters = (1 << 1), Numbers = (1 << 2), @@ -41,10 +43,11 @@ public: EASCII = (1 << 9), DefaultCharset = LowerLetters | UpperLetters | Numbers }; - Q_DECLARE_FLAGS(CharClasses, CharClass) + Q_DECLARE_FLAGS(CharClasses, CharClass); enum GeneratorFlag { + NoFlags = 0, ExcludeLookAlike = (1 << 0), CharFromEveryGroup = (1 << 1), AdvancedMode = (1 << 2), @@ -56,17 +59,25 @@ public: PasswordGenerator(); void setLength(int length); - void setCharClasses(const CharClasses& classes); void setFlags(const GeneratorFlags& flags); - void setAdditionalChars(const QString& chars); - void setExcludedChars(const QString& chars); + void setCharClasses(const CharClasses& classes); + void setCustomCharacterSet(const QString& customCharacterSet); + void setExcludedCharacterSet(const QString& excludedCharacterSet); + void reset(); bool isValid() const; + int getMinLength() const; + + int getLength() const; + const GeneratorFlags& getFlags() const; + const CharClasses& getActiveClasses() const; + const QString& getCustomCharacterSet() const; + const QString& getExcludedCharacterSet() const; QString generatePassword() const; - static const int DefaultLength = 32; - static const char* DefaultAdditionalChars; + static const int DefaultLength; + static const char* DefaultCustomCharacterSet; static const char* DefaultExcludedChars; private: @@ -76,10 +87,8 @@ private: int m_length; CharClasses m_classes; GeneratorFlags m_flags; - QString m_additional; + QString m_custom; QString m_excluded; - - Q_DISABLE_COPY(PasswordGenerator) }; Q_DECLARE_OPERATORS_FOR_FLAGS(PasswordGenerator::CharClasses) diff --git a/src/gui/PasswordGeneratorWidget.cpp b/src/gui/PasswordGeneratorWidget.cpp index faa6777af..32e559418 100644 --- a/src/gui/PasswordGeneratorWidget.cpp +++ b/src/gui/PasswordGeneratorWidget.cpp @@ -576,51 +576,15 @@ void PasswordGeneratorWidget::updateGenerator() auto classes = charClasses(); auto flags = generatorFlags(); - int length = 0; - if (flags.testFlag(PasswordGenerator::CharFromEveryGroup)) { - if (classes.testFlag(PasswordGenerator::LowerLetters)) { - ++length; - } - if (classes.testFlag(PasswordGenerator::UpperLetters)) { - ++length; - } - if (classes.testFlag(PasswordGenerator::Numbers)) { - ++length; - } - if (classes.testFlag(PasswordGenerator::Braces)) { - ++length; - } - if (classes.testFlag(PasswordGenerator::Punctuation)) { - ++length; - } - if (classes.testFlag(PasswordGenerator::Quotes)) { - ++length; - } - if (classes.testFlag(PasswordGenerator::Dashes)) { - ++length; - } - if (classes.testFlag(PasswordGenerator::Math)) { - ++length; - } - if (classes.testFlag(PasswordGenerator::Logograms)) { - ++length; - } - if (classes.testFlag(PasswordGenerator::EASCII)) { - ++length; - } - } - - length = qMax(length, m_ui->spinBoxLength->value()); - m_passwordGenerator->setLength(length); - m_passwordGenerator->setCharClasses(classes); - m_passwordGenerator->setFlags(flags); + m_passwordGenerator->setLength(m_ui->spinBoxLength->value()); if (m_ui->buttonAdvancedMode->isChecked()) { - m_passwordGenerator->setAdditionalChars(m_ui->editAdditionalChars->text()); - m_passwordGenerator->setExcludedChars(m_ui->editExcludedChars->text()); + m_passwordGenerator->setCharClasses(classes); + m_passwordGenerator->setCustomCharacterSet(m_ui->editAdditionalChars->text()); + m_passwordGenerator->setCustomCharacterSet(m_ui->editExcludedChars->text()); } else { - m_passwordGenerator->setAdditionalChars(""); - m_passwordGenerator->setExcludedChars(""); + m_passwordGenerator->setCharClasses(classes); } + m_passwordGenerator->setFlags(flags); if (m_passwordGenerator->isValid()) { m_ui->buttonGenerate->setEnabled(true); diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index 1639e8a39..49b21dfa6 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -1195,6 +1195,10 @@ void TestCli::testGenerate_data() << QStringList{"generate", "-L", "2", "--upper", "-l", "--every-group"} << "^[a-z][A-Z]|[A-Z][a-z]$"; QTest::newRow("numbers + lowercase (every)") << QStringList{"generate", "-L", "2", "-n", "-l", "--every-group"} << "^[a-z][0-9]|[0-9][a-z]$"; + QTest::newRow("custom character set") + << QStringList{"generate", "-L", "200", "-n", "-c", "abc"} << "^[abc0-9]{200}$"; + QTest::newRow("custom character set without extra options uses only custom chars") + << QStringList{"generate", "-L", "200", "-c", "a"} << "^a{200}$"; } void TestCli::testGenerate() diff --git a/tests/TestPasswordGenerator.cpp b/tests/TestPasswordGenerator.cpp index 2ed2974a8..10b91f6af 100644 --- a/tests/TestPasswordGenerator.cpp +++ b/tests/TestPasswordGenerator.cpp @@ -16,7 +16,6 @@ */ #include "TestPasswordGenerator.h" -#include "core/PasswordGenerator.h" #include "crypto/Crypto.h" #include @@ -24,120 +23,261 @@ QTEST_GUILESS_MAIN(TestPasswordGenerator) +Q_DECLARE_METATYPE(PasswordGenerator::CharClasses) +Q_DECLARE_METATYPE(PasswordGenerator::GeneratorFlags) + +namespace +{ + PasswordGenerator::CharClasses to_flags(PasswordGenerator::CharClass x) + { + return x; + } + + PasswordGenerator::GeneratorFlags to_flags(PasswordGenerator::GeneratorFlag x) + { + return x; + } +} // namespace + void TestPasswordGenerator::initTestCase() { QVERIFY(Crypto::init()); } -void TestPasswordGenerator::testAdditionalChars() +void TestPasswordGenerator::init() { - PasswordGenerator generator; - QVERIFY(!generator.isValid()); - generator.setAdditionalChars("aql"); - generator.setLength(2000); - QVERIFY(generator.isValid()); - QString password = generator.generatePassword(); + m_generator.reset(); +} + +void TestPasswordGenerator::testCustomCharacterSet_data() +{ + QTest::addColumn("activeCharacterClasses"); + QTest::addColumn("customCharacterSet"); + QTest::addColumn("expected"); + + QTest::addRow("With active classes") << to_flags(PasswordGenerator::CharClass::UpperLetters) << "abc" + << QRegularExpression("^[abcA-Z]{2000}$"); + QTest::addRow("Without any active class") + << to_flags(PasswordGenerator::CharClass::NoClass) << "abc" << QRegularExpression("^[abc]{2000}$"); +} + +void TestPasswordGenerator::testCustomCharacterSet() +{ + QFETCH(PasswordGenerator::CharClasses, activeCharacterClasses); + QFETCH(QString, customCharacterSet); + QFETCH(QRegularExpression, expected); + + m_generator.setCharClasses(activeCharacterClasses); + m_generator.setCustomCharacterSet(customCharacterSet); + m_generator.setLength(2000); + + QVERIFY(m_generator.isValid()); + QString password = m_generator.generatePassword(); QCOMPARE(password.size(), 2000); - QRegularExpression regex(R"(^[aql]+$)"); - QVERIFY(regex.match(password).hasMatch()); + QVERIFY(expected.match(password).hasMatch()); +} + +void TestPasswordGenerator::testCharClasses_data() +{ + QTest::addColumn("activeCharacterClasses"); + QTest::addColumn("expected"); + + QTest::addRow("Lower Letters") << to_flags(PasswordGenerator::CharClass::LowerLetters) + << QRegularExpression(R"(^[a-z]{2000}$)"); + QTest::addRow("Upper Letters") << to_flags(PasswordGenerator::CharClass::UpperLetters) + << QRegularExpression(R"(^[A-Z]{2000}$)"); + QTest::addRow("Numbers") << to_flags(PasswordGenerator::CharClass::Numbers) << QRegularExpression(R"(^\d{2000}$)"); + QTest::addRow("Braces") << to_flags(PasswordGenerator::CharClass::Braces) + << QRegularExpression(R"(^[\(\)\[\]\{\}]{2000}$)"); + QTest::addRow("Punctuation") << to_flags(PasswordGenerator::CharClass::Punctuation) + << QRegularExpression(R"(^[\.,:;]{2000}$)"); + QTest::addRow("Quotes") << to_flags(PasswordGenerator::CharClass::Quotes) << QRegularExpression(R"(^["']{2000}$)"); + QTest::addRow("Dashes") << to_flags(PasswordGenerator::CharClass::Dashes) + << QRegularExpression(R"(^[\-/\\_|]{2000}$)"); + QTest::addRow("Math") << to_flags(PasswordGenerator::CharClass::Math) << QRegularExpression(R"(^[!\*\+\-<=>\?]+$)"); + QTest::addRow("Logograms") << to_flags(PasswordGenerator::CharClass::Logograms) + << QRegularExpression(R"(^[#`~%&^$@]{2000}$)"); + QTest::addRow("Extended ASCII") << to_flags(PasswordGenerator::CharClass::EASCII) + << QRegularExpression(R"(^[^a-zA-Z0-9\.,:;"'\-/\\_|!\*\+\-<=>\?#`~%&^$@]{2000}$)"); + QTest::addRow("Combinations 1") << (PasswordGenerator::CharClass::LowerLetters + | PasswordGenerator::CharClass::UpperLetters + | PasswordGenerator::CharClass::Braces) + << QRegularExpression(R"(^[a-zA-Z\(\)\[\]\{\}]{2000}$)"); + QTest::addRow("Combinations 2") << (PasswordGenerator::CharClass::Quotes | PasswordGenerator::CharClass::Numbers + | PasswordGenerator::CharClass::Dashes) + << QRegularExpression(R"(^["'\d\-/\\_|]{2000}$)"); } void TestPasswordGenerator::testCharClasses() { - PasswordGenerator generator; - QVERIFY(!generator.isValid()); - generator.setCharClasses(PasswordGenerator::CharClass::LowerLetters); - generator.setLength(16); - QVERIFY(generator.isValid()); - QCOMPARE(generator.generatePassword().size(), 16); - generator.setLength(2000); - QString password = generator.generatePassword(); + QFETCH(PasswordGenerator::CharClasses, activeCharacterClasses); + QFETCH(QRegularExpression, expected); + + m_generator.setCharClasses(activeCharacterClasses); + m_generator.setLength(2000); + + QVERIFY(m_generator.isValid()); + QString password = m_generator.generatePassword(); QCOMPARE(password.size(), 2000); - QRegularExpression regex(R"(^[a-z]+$)"); - QVERIFY(regex.match(password).hasMatch()); + QVERIFY(expected.match(password).hasMatch()); +} - generator.setCharClasses(PasswordGenerator::CharClass::UpperLetters); - password = generator.generatePassword(); - regex.setPattern(R"(^[A-Z]+$)"); - QVERIFY(regex.match(password).hasMatch()); +void TestPasswordGenerator::testLookalikeExclusion_data() +{ + QTest::addColumn("activeCharacterClasses"); + QTest::addColumn("expected"); + QTest::addRow("Upper Letters") << (PasswordGenerator::CharClass::LowerLetters + | PasswordGenerator::CharClass::UpperLetters) + << QRegularExpression("^[^lBGIO]{2000}$"); - generator.setCharClasses(PasswordGenerator::CharClass::Numbers); - password = generator.generatePassword(); - regex.setPattern(R"(^\d+$)"); - QVERIFY(regex.match(password).hasMatch()); + QTest::addRow("Letters and Numbers") << (PasswordGenerator::CharClass::LowerLetters + | PasswordGenerator::CharClass::UpperLetters + | PasswordGenerator::CharClass::Numbers) + << QRegularExpression("^[^lBGIO0168]{2000}$"); - generator.setCharClasses(PasswordGenerator::CharClass::Braces); - password = generator.generatePassword(); - regex.setPattern(R"(^[\(\)\[\]\{\}]+$)"); - QVERIFY(regex.match(password).hasMatch()); - - generator.setCharClasses(PasswordGenerator::CharClass::Punctuation); - password = generator.generatePassword(); - regex.setPattern(R"(^[\.,:;]+$)"); - QVERIFY(regex.match(password).hasMatch()); - - generator.setCharClasses(PasswordGenerator::CharClass::Quotes); - password = generator.generatePassword(); - regex.setPattern(R"(^["']+$)"); - QVERIFY(regex.match(password).hasMatch()); - - generator.setCharClasses(PasswordGenerator::CharClass::Dashes); - password = generator.generatePassword(); - regex.setPattern(R"(^[\-/\\_|]+$)"); - QVERIFY(regex.match(password).hasMatch()); - - generator.setCharClasses(PasswordGenerator::CharClass::Math); - password = generator.generatePassword(); - regex.setPattern(R"(^[!\*\+\-<=>\?]+$)"); - QVERIFY(regex.match(password).hasMatch()); - - generator.setCharClasses(PasswordGenerator::CharClass::Logograms); - password = generator.generatePassword(); - regex.setPattern(R"(^[#`~%&^$@]+$)"); - QVERIFY(regex.match(password).hasMatch()); - - generator.setCharClasses(PasswordGenerator::CharClass::EASCII); - password = generator.generatePassword(); - regex.setPattern(R"(^[^a-zA-Z0-9\.,:;"'\-/\\_|!\*\+\-<=>\?#`~%&^$@]+$)"); - QVERIFY(regex.match(password).hasMatch()); - - generator.setCharClasses(PasswordGenerator::CharClass::LowerLetters | PasswordGenerator::CharClass::UpperLetters - | PasswordGenerator::CharClass::Braces); - password = generator.generatePassword(); - regex.setPattern(R"(^[a-zA-Z\(\)\[\]\{\}]+$)"); - QVERIFY(regex.match(password).hasMatch()); - - generator.setCharClasses(PasswordGenerator::CharClass::Quotes | PasswordGenerator::CharClass::Numbers - | PasswordGenerator::CharClass::Dashes); - password = generator.generatePassword(); - regex.setPattern(R"(^["'\d\-/\\_|]+$)"); - QVERIFY(regex.match(password).hasMatch()); + QTest::addRow("Letters, Numbers and extended ASCII") + << (PasswordGenerator::CharClass::LowerLetters | PasswordGenerator::CharClass::UpperLetters + | PasswordGenerator::CharClass::Numbers | PasswordGenerator::CharClass::EASCII) + << QRegularExpression("^[^lBGIO0168﹒]{2000}$"); } void TestPasswordGenerator::testLookalikeExclusion() { - PasswordGenerator generator; - generator.setLength(2000); - generator.setCharClasses(PasswordGenerator::CharClass::LowerLetters | PasswordGenerator::CharClass::UpperLetters); - QVERIFY(generator.isValid()); - QString password = generator.generatePassword(); + QFETCH(PasswordGenerator::CharClasses, activeCharacterClasses); + QFETCH(QRegularExpression, expected); + + m_generator.setFlags(PasswordGenerator::ExcludeLookAlike); + m_generator.setCharClasses(activeCharacterClasses); + m_generator.setLength(2000); + + QVERIFY(m_generator.isValid()); + QString password = m_generator.generatePassword(); QCOMPARE(password.size(), 2000); - - generator.setFlags(PasswordGenerator::GeneratorFlag::ExcludeLookAlike); - password = generator.generatePassword(); - QRegularExpression regex("^[^lBGIO]+$"); - QVERIFY(regex.match(password).hasMatch()); - - generator.setCharClasses(PasswordGenerator::CharClass::LowerLetters | PasswordGenerator::CharClass::UpperLetters - | PasswordGenerator::CharClass::Numbers); - password = generator.generatePassword(); - regex.setPattern("^[^lBGIO0168]+$"); - QVERIFY(regex.match(password).hasMatch()); - - generator.setCharClasses(PasswordGenerator::CharClass::LowerLetters | PasswordGenerator::CharClass::UpperLetters - | PasswordGenerator::CharClass::Numbers | PasswordGenerator::CharClass::EASCII); - password = generator.generatePassword(); - regex.setPattern("^[^lBGIO0168﹒]+$"); - QVERIFY(regex.match(password).hasMatch()); + QVERIFY(expected.match(password).hasMatch()); +} + +void TestPasswordGenerator::testValidity_data() +{ + QTest::addColumn("activeCharacterClasses"); + QTest::addColumn("generatorFlags"); + QTest::addColumn("customCharacterSet"); + QTest::addColumn("excludedCharacters"); + QTest::addColumn("length"); + QTest::addColumn("isValid"); + + QTest::addRow("No active class") << to_flags(PasswordGenerator::CharClass::NoClass) + << PasswordGenerator::GeneratorFlags() << QString() << QString() + << PasswordGenerator::DefaultLength << false; + QTest::addRow("0 length") << to_flags(PasswordGenerator::CharClass::DefaultCharset) + << PasswordGenerator::GeneratorFlags() << QString() << QString() << 0 << false; + QTest::addRow("All active classes excluded") + << to_flags(PasswordGenerator::CharClass::Numbers) << PasswordGenerator::GeneratorFlags() << QString() + << QString("0123456789") << PasswordGenerator::DefaultLength << false; + QTest::addRow("All active classes excluded") + << to_flags(PasswordGenerator::CharClass::NoClass) << PasswordGenerator::GeneratorFlags() << QString() + << QString("0123456789") << PasswordGenerator::DefaultLength << false; + QTest::addRow("One from every class with too few classes") + << (PasswordGenerator::CharClass::LowerLetters | PasswordGenerator::CharClass::UpperLetters) + << to_flags(PasswordGenerator::GeneratorFlag::CharFromEveryGroup) << QString() << QString() << 1 << false; + QTest::addRow("One from every class with excluded classes") + << (PasswordGenerator::CharClass::LowerLetters | PasswordGenerator::CharClass::UpperLetters + | PasswordGenerator::CharClass::Numbers) + << to_flags(PasswordGenerator::GeneratorFlag::CharFromEveryGroup) << QString() << QString("0123456789") << 2 + << true; + QTest::addRow("Defaults valid") << to_flags(PasswordGenerator::CharClass::DefaultCharset) + << to_flags(PasswordGenerator::GeneratorFlag::DefaultFlags) + << PasswordGenerator::DefaultCustomCharacterSet + << PasswordGenerator::DefaultExcludedChars << PasswordGenerator::DefaultLength + << true; + QTest::addRow("No active classes but custom charset") + << to_flags(PasswordGenerator::CharClass::NoClass) << to_flags(PasswordGenerator::GeneratorFlag::DefaultFlags) + << QString("a") << QString() << 1 << true; +} + +void TestPasswordGenerator::testValidity() +{ + QFETCH(PasswordGenerator::CharClasses, activeCharacterClasses); + QFETCH(PasswordGenerator::GeneratorFlags, generatorFlags); + QFETCH(QString, customCharacterSet); + QFETCH(QString, excludedCharacters); + QFETCH(int, length); + QFETCH(bool, isValid); + + m_generator.setCharClasses(activeCharacterClasses); + m_generator.setFlags(generatorFlags); + m_generator.setCustomCharacterSet(customCharacterSet); + m_generator.setExcludedCharacterSet(excludedCharacters); + m_generator.setLength(length); + QCOMPARE(m_generator.isValid(), isValid); +} + +void TestPasswordGenerator::testMinLength_data() +{ + QTest::addColumn("activeCharacterClasses"); + QTest::addColumn("generatorFlags"); + QTest::addColumn("customCharacterSet"); + QTest::addColumn("excludedCharacters"); + QTest::addColumn("expectedMinLength"); + + QTest::addRow("No restriction without charsFromEveryGroup") + << to_flags(PasswordGenerator::CharClass::Numbers) + << to_flags(PasswordGenerator::GeneratorFlag::CharFromEveryGroup) + << PasswordGenerator::DefaultCustomCharacterSet << PasswordGenerator::DefaultExcludedChars << 1; + + QTest::addRow("Min length should equal number of active classes") + << (PasswordGenerator::CharClass::LowerLetters | PasswordGenerator::CharClass::UpperLetters + | PasswordGenerator::CharClass::Numbers) + << to_flags(PasswordGenerator::GeneratorFlag::CharFromEveryGroup) << QString() << QString() << 3; + QTest::addRow("Classes fully excluded by excluded characters do not count towards min length") + << (PasswordGenerator::CharClass::Numbers | PasswordGenerator::LowerLetters + | PasswordGenerator::CharClass::UpperLetters) + << to_flags(PasswordGenerator::GeneratorFlag::CharFromEveryGroup) << QString() << QString("0123456789") << 2; + + QTest::addRow("Custom charset counts as class") + << to_flags(PasswordGenerator::CharClass::UpperLetters) + << to_flags(PasswordGenerator::GeneratorFlag::CharFromEveryGroup) << QString("a") << QString() << 2; + QTest::addRow("Custom characters count even if included by an active class already") + << (PasswordGenerator::CharClass::LowerLetters | PasswordGenerator::CharClass::UpperLetters + | PasswordGenerator::CharClass::Numbers) + << to_flags(PasswordGenerator::GeneratorFlag::CharFromEveryGroup) << QString("012345") << QString() << 4; +} + +void TestPasswordGenerator::testMinLength() +{ + QFETCH(PasswordGenerator::CharClasses, activeCharacterClasses); + QFETCH(PasswordGenerator::GeneratorFlags, generatorFlags); + QFETCH(QString, customCharacterSet); + QFETCH(QString, excludedCharacters); + QFETCH(int, expectedMinLength); + + m_generator.setCharClasses(activeCharacterClasses); + m_generator.setFlags(generatorFlags); + m_generator.setCustomCharacterSet(customCharacterSet); + m_generator.setExcludedCharacterSet(excludedCharacters); + QCOMPARE(m_generator.getMinLength(), expectedMinLength); +} + +void TestPasswordGenerator::testReset() +{ + PasswordGenerator default_generator; + + // Modify generator + m_generator.setCharClasses(PasswordGenerator::CharClass::NoClass); + m_generator.setFlags(PasswordGenerator::GeneratorFlag::NoFlags); + m_generator.setCustomCharacterSet("avc"); + m_generator.setExcludedCharacterSet("asdv"); + m_generator.setLength(m_generator.getLength() + 1); + + Q_ASSERT(m_generator.getActiveClasses() != default_generator.getActiveClasses()); + Q_ASSERT(m_generator.getFlags() != default_generator.getFlags()); + Q_ASSERT(m_generator.getCustomCharacterSet() != default_generator.getCustomCharacterSet()); + Q_ASSERT(m_generator.getExcludedCharacterSet() != default_generator.getExcludedCharacterSet()); + + m_generator.reset(); + QCOMPARE(m_generator.getActiveClasses(), default_generator.getActiveClasses()); + QCOMPARE(m_generator.getFlags(), default_generator.getFlags()); + QCOMPARE(m_generator.getCustomCharacterSet(), default_generator.getCustomCharacterSet()); + QCOMPARE(m_generator.getExcludedCharacterSet(), default_generator.getExcludedCharacterSet()); + QCOMPARE(m_generator.getLength(), default_generator.getLength()); } diff --git a/tests/TestPasswordGenerator.h b/tests/TestPasswordGenerator.h index 454d16e06..b5bfa7d23 100644 --- a/tests/TestPasswordGenerator.h +++ b/tests/TestPasswordGenerator.h @@ -18,17 +18,31 @@ #ifndef KEEPASSXC_TESTPASSWORDGENERATOR_H #define KEEPASSXC_TESTPASSWORDGENERATOR_H +#include "core/PasswordGenerator.h" #include class TestPasswordGenerator : public QObject { Q_OBJECT +private: + PasswordGenerator m_generator; + private slots: void initTestCase(); - void testAdditionalChars(); + void init(); + + void testCustomCharacterSet_data(); + void testCustomCharacterSet(); + void testCharClasses_data(); void testCharClasses(); + void testLookalikeExclusion_data(); void testLookalikeExclusion(); + void testMinLength_data(); + void testMinLength(); + void testValidity_data(); + void testValidity(); + void testReset(); }; #endif // KEEPASSXC_TESTPASSWORDGENERATOR_H