CLI password generation options cleanup (#3275)

Summary of changes:
* Extract function for creating password generator from options into
`Generate` command. This function is now reused in `Add` and `Edit`
commands.
* Updated manpage with missing password generation options.
* Updated manpage with missing longer forms of password generation options.
* Added unit tests for new password generation options in `Add` and
`Edit`.
* Handle case when `-g` and `-p` options are used at the same time.

This PR adds password generation functionalities while reducing
code duplication, but at the cost of 2 small breaking changes:
* The password generation option for `Add` and `Edit` for specifying
password length is now `-L` instead of `-l`, to not clash with the
`-l --lower` option.
* The `-u` shorthand for the `--upper` option has to be removed, to not
clash with the `-u --username` option.
* Add -U variant for uppercase.
This commit is contained in:
louib 2019-08-30 22:50:32 -04:00 committed by Jonathan White
parent 79bb991a61
commit eb1882453f
8 changed files with 221 additions and 88 deletions

View File

@ -2,6 +2,11 @@
========================= =========================
- Group sorting feature [#3282] - Group sorting feature [#3282]
- CLI: Add 'flatten' option to the 'ls' command [#3276] - CLI: Add 'flatten' option to the 'ls' command [#3276]
- 💥💥 CLI: The password length option `-l` for the CLI commands
`Add` and `Edit` is now `-L` [#3275]
- 💥💥 CLI: the `-u` shorthand for the `--upper` password generation option
has been renamed `-U` [#3275]
- CLI: Add password generation options to `Add` and `Edit` commands [#3275]
- Rework the Entry Preview panel [#3306] - Rework the Entry Preview panel [#3306]
- Move notes to General tab on Group Preview Panel [#3336] - Move notes to General tab on Group Preview Panel [#3336]
- Add 'Monospaced font' option to the Notes field [#3321] - Add 'Monospaced font' option to the Notes field [#3321]

View File

@ -20,6 +20,7 @@
#include "Add.h" #include "Add.h"
#include "cli/Generate.h"
#include "cli/TextStream.h" #include "cli/TextStream.h"
#include "cli/Utils.h" #include "cli/Utils.h"
#include "core/Database.h" #include "core/Database.h"
@ -44,11 +45,6 @@ const QCommandLineOption Add::GenerateOption = QCommandLineOption(QStringList()
<< "generate", << "generate",
QObject::tr("Generate a password for the entry.")); QObject::tr("Generate a password for the entry."));
const QCommandLineOption Add::PasswordLengthOption =
QCommandLineOption(QStringList() << "l"
<< "password-length",
QObject::tr("Length for the generated password."),
QObject::tr("length"));
Add::Add() Add::Add()
{ {
@ -57,9 +53,19 @@ Add::Add()
options.append(Add::UsernameOption); options.append(Add::UsernameOption);
options.append(Add::UrlOption); options.append(Add::UrlOption);
options.append(Add::PasswordPromptOption); options.append(Add::PasswordPromptOption);
options.append(Add::GenerateOption);
options.append(Add::PasswordLengthOption);
positionalArguments.append({QString("entry"), QObject::tr("Path of the entry to add."), QString("")}); positionalArguments.append({QString("entry"), QObject::tr("Path of the entry to add."), QString("")});
// Password generation options.
options.append(Add::GenerateOption);
options.append(Generate::PasswordLengthOption);
options.append(Generate::LowerCaseOption);
options.append(Generate::UpperCaseOption);
options.append(Generate::NumbersOption);
options.append(Generate::SpecialCharsOption);
options.append(Generate::ExtendedAsciiOption);
options.append(Generate::ExcludeCharsOption);
options.append(Generate::ExcludeSimilarCharsOption);
options.append(Generate::IncludeEveryGroupOption);
} }
int Add::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<QCommandLineParser> parser) int Add::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<QCommandLineParser> parser)
@ -72,14 +78,22 @@ int Add::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<Q
const QString& databasePath = args.at(0); const QString& databasePath = args.at(0);
const QString& entryPath = args.at(1); const QString& entryPath = args.at(1);
// Validating the password length here, before we actually create // Cannot use those 2 options at the same time!
// the entry. if (parser->isSet(Add::GenerateOption) && parser->isSet(Add::PasswordPromptOption)) {
QString passwordLength = parser->value(Add::PasswordLengthOption); errorTextStream << QObject::tr("Cannot generate a password and prompt at the same time!") << endl;
if (!passwordLength.isEmpty() && !passwordLength.toInt()) {
errorTextStream << QObject::tr("Invalid value for password length %1.").arg(passwordLength) << endl;
return EXIT_FAILURE; return EXIT_FAILURE;
} }
// Validating the password generator here, before we actually create
// the entry.
QSharedPointer<PasswordGenerator> passwordGenerator;
if (parser->isSet(Add::GenerateOption)) {
passwordGenerator = Generate::createGenerator(parser);
if (passwordGenerator.isNull()) {
return EXIT_FAILURE;
}
}
Entry* entry = database->rootGroup()->addEntryWithPath(entryPath); Entry* entry = database->rootGroup()->addEntryWithPath(entryPath);
if (!entry) { if (!entry) {
errorTextStream << QObject::tr("Could not create entry with path %1.").arg(entryPath) << endl; errorTextStream << QObject::tr("Could not create entry with path %1.").arg(entryPath) << endl;
@ -101,17 +115,7 @@ int Add::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<Q
QString password = Utils::getPassword(parser->isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT); QString password = Utils::getPassword(parser->isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT);
entry->setPassword(password); entry->setPassword(password);
} else if (parser->isSet(Add::GenerateOption)) { } else if (parser->isSet(Add::GenerateOption)) {
PasswordGenerator passwordGenerator; QString password = passwordGenerator->generatePassword();
if (passwordLength.isEmpty()) {
passwordGenerator.setLength(PasswordGenerator::DefaultLength);
} else {
passwordGenerator.setLength(passwordLength.toInt());
}
passwordGenerator.setCharClasses(PasswordGenerator::DefaultCharset);
passwordGenerator.setFlags(PasswordGenerator::DefaultFlags);
QString password = passwordGenerator.generatePassword();
entry->setPassword(password); entry->setPassword(password);
} }

View File

@ -21,6 +21,7 @@
#include "Edit.h" #include "Edit.h"
#include "cli/Add.h" #include "cli/Add.h"
#include "cli/Generate.h"
#include "cli/TextStream.h" #include "cli/TextStream.h"
#include "cli/Utils.h" #include "cli/Utils.h"
#include "core/Database.h" #include "core/Database.h"
@ -41,10 +42,20 @@ Edit::Edit()
options.append(Add::UsernameOption); options.append(Add::UsernameOption);
options.append(Add::UrlOption); options.append(Add::UrlOption);
options.append(Add::PasswordPromptOption); options.append(Add::PasswordPromptOption);
options.append(Add::GenerateOption);
options.append(Add::PasswordLengthOption);
options.append(Edit::TitleOption); options.append(Edit::TitleOption);
positionalArguments.append({QString("entry"), QObject::tr("Path of the entry to edit."), QString("")}); positionalArguments.append({QString("entry"), QObject::tr("Path of the entry to edit."), QString("")});
// Password generation options.
options.append(Add::GenerateOption);
options.append(Generate::PasswordLengthOption);
options.append(Generate::LowerCaseOption);
options.append(Generate::UpperCaseOption);
options.append(Generate::NumbersOption);
options.append(Generate::SpecialCharsOption);
options.append(Generate::ExtendedAsciiOption);
options.append(Generate::ExcludeCharsOption);
options.append(Generate::ExcludeSimilarCharsOption);
options.append(Generate::IncludeEveryGroupOption);
} }
int Edit::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<QCommandLineParser> parser) int Edit::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<QCommandLineParser> parser)
@ -57,12 +68,23 @@ int Edit::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
const QString& databasePath = args.at(0); const QString& databasePath = args.at(0);
const QString& entryPath = args.at(1); const QString& entryPath = args.at(1);
QString passwordLength = parser->value(Add::PasswordLengthOption); // Cannot use those 2 options at the same time!
if (!passwordLength.isEmpty() && !passwordLength.toInt()) { if (parser->isSet(Add::GenerateOption) && parser->isSet(Add::PasswordPromptOption)) {
errorTextStream << QObject::tr("Invalid value for password length: %1").arg(passwordLength) << endl; errorTextStream << QObject::tr("Cannot generate a password and prompt at the same time!") << endl;
return EXIT_FAILURE; return EXIT_FAILURE;
} }
// Validating the password generator here, before we actually start
// the update.
QSharedPointer<PasswordGenerator> passwordGenerator;
bool generate = parser->isSet(Add::GenerateOption);
if (generate) {
passwordGenerator = Generate::createGenerator(parser);
if (passwordGenerator.isNull()) {
return EXIT_FAILURE;
}
}
Entry* entry = database->rootGroup()->findEntryByPath(entryPath); Entry* entry = database->rootGroup()->findEntryByPath(entryPath);
if (!entry) { if (!entry) {
errorTextStream << QObject::tr("Could not find entry with path %1.").arg(entryPath) << endl; errorTextStream << QObject::tr("Could not find entry with path %1.").arg(entryPath) << endl;
@ -72,7 +94,6 @@ int Edit::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
QString username = parser->value(Add::UsernameOption); QString username = parser->value(Add::UsernameOption);
QString url = parser->value(Add::UrlOption); QString url = parser->value(Add::UrlOption);
QString title = parser->value(Edit::TitleOption); QString title = parser->value(Edit::TitleOption);
bool generate = parser->isSet(Add::GenerateOption);
bool prompt = parser->isSet(Add::PasswordPromptOption); bool prompt = parser->isSet(Add::PasswordPromptOption);
if (username.isEmpty() && url.isEmpty() && title.isEmpty() && !prompt && !generate) { if (username.isEmpty() && url.isEmpty() && title.isEmpty() && !prompt && !generate) {
errorTextStream << QObject::tr("Not changing any field for entry %1.").arg(entryPath) << endl; errorTextStream << QObject::tr("Not changing any field for entry %1.").arg(entryPath) << endl;
@ -98,17 +119,7 @@ int Edit::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
QString password = Utils::getPassword(parser->isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT); QString password = Utils::getPassword(parser->isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT);
entry->setPassword(password); entry->setPassword(password);
} else if (generate) { } else if (generate) {
PasswordGenerator passwordGenerator; QString password = passwordGenerator->generatePassword();
if (passwordLength.isEmpty()) {
passwordGenerator.setLength(PasswordGenerator::DefaultLength);
} else {
passwordGenerator.setLength(static_cast<size_t>(passwordLength.toInt()));
}
passwordGenerator.setCharClasses(PasswordGenerator::DefaultCharset);
passwordGenerator.setFlags(PasswordGenerator::DefaultFlags);
QString password = passwordGenerator.generatePassword();
entry->setPassword(password); entry->setPassword(password);
} }

View File

@ -22,7 +22,6 @@
#include "cli/TextStream.h" #include "cli/TextStream.h"
#include "cli/Utils.h" #include "cli/Utils.h"
#include "core/PasswordGenerator.h"
const QCommandLineOption Generate::PasswordLengthOption = const QCommandLineOption Generate::PasswordLengthOption =
QCommandLineOption(QStringList() << "L" QCommandLineOption(QStringList() << "L"
@ -34,7 +33,7 @@ const QCommandLineOption Generate::LowerCaseOption = QCommandLineOption(QStringL
<< "lower", << "lower",
QObject::tr("Use lowercase characters")); QObject::tr("Use lowercase characters"));
const QCommandLineOption Generate::UpperCaseOption = QCommandLineOption(QStringList() << "u" const QCommandLineOption Generate::UpperCaseOption = QCommandLineOption(QStringList() << "U"
<< "upper", << "upper",
QObject::tr("Use uppercase characters")); QObject::tr("Use uppercase characters"));
@ -75,26 +74,22 @@ Generate::Generate()
options.append(Generate::IncludeEveryGroupOption); options.append(Generate::IncludeEveryGroupOption);
} }
int Generate::execute(const QStringList& arguments) /**
* Creates a password generator instance using the command line options
* of the parser object.
*/
QSharedPointer<PasswordGenerator> Generate::createGenerator(QSharedPointer<QCommandLineParser> parser)
{ {
QSharedPointer<QCommandLineParser> parser = getCommandLineParser(arguments);
if (parser.isNull()) {
return EXIT_FAILURE;
}
TextStream outputTextStream(Utils::STDOUT, QIODevice::WriteOnly);
TextStream errorTextStream(Utils::STDERR, QIODevice::WriteOnly); TextStream errorTextStream(Utils::STDERR, QIODevice::WriteOnly);
QSharedPointer<PasswordGenerator> passwordGenerator = QSharedPointer<PasswordGenerator>(new PasswordGenerator());
const QStringList args = parser->positionalArguments();
PasswordGenerator passwordGenerator;
QString passwordLength = parser->value(Generate::PasswordLengthOption); QString passwordLength = parser->value(Generate::PasswordLengthOption);
if (passwordLength.isEmpty()) { if (passwordLength.isEmpty()) {
passwordGenerator.setLength(PasswordGenerator::DefaultLength); passwordGenerator->setLength(PasswordGenerator::DefaultLength);
} else if (passwordLength.toInt() <= 0) { } else if (passwordLength.toInt() <= 0) {
errorTextStream << QObject::tr("Invalid password length %1").arg(passwordLength) << endl; errorTextStream << QObject::tr("Invalid password length %1").arg(passwordLength) << endl;
return EXIT_FAILURE; return QSharedPointer<PasswordGenerator>(nullptr);
} else { } else {
passwordGenerator.setLength(passwordLength.toInt()); passwordGenerator->setLength(passwordLength.toInt());
} }
PasswordGenerator::CharClasses classes = 0x0; PasswordGenerator::CharClasses classes = 0x0;
@ -124,16 +119,34 @@ int Generate::execute(const QStringList& arguments)
flags |= PasswordGenerator::CharFromEveryGroup; flags |= PasswordGenerator::CharFromEveryGroup;
} }
passwordGenerator.setCharClasses(classes); // The default charset will be used if no explicit class
passwordGenerator.setFlags(flags); // option was set.
passwordGenerator.setExcludedChars(parser->value(Generate::ExcludeCharsOption)); passwordGenerator->setCharClasses(classes);
passwordGenerator->setFlags(flags);
passwordGenerator->setExcludedChars(parser->value(Generate::ExcludeCharsOption));
if (!passwordGenerator.isValid()) { if (!passwordGenerator->isValid()) {
errorTextStream << QObject::tr("invalid password generator after applying all options") << endl; errorTextStream << QObject::tr("invalid password generator after applying all options") << endl;
return QSharedPointer<PasswordGenerator>(nullptr);
}
return passwordGenerator;
}
int Generate::execute(const QStringList& arguments)
{
QSharedPointer<QCommandLineParser> parser = getCommandLineParser(arguments);
if (parser.isNull()) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
QString password = passwordGenerator.generatePassword(); QSharedPointer<PasswordGenerator> passwordGenerator = Generate::createGenerator(parser);
if (passwordGenerator.isNull()) {
return EXIT_FAILURE;
}
TextStream outputTextStream(Utils::STDOUT, QIODevice::WriteOnly);
QString password = passwordGenerator->generatePassword();
outputTextStream << password << endl; outputTextStream << password << endl;
return EXIT_SUCCESS; return EXIT_SUCCESS;

View File

@ -20,12 +20,16 @@
#include "Command.h" #include "Command.h"
#include "core/PasswordGenerator.h"
class Generate : public Command class Generate : public Command
{ {
public: public:
Generate(); Generate();
int execute(const QStringList& arguments) override; int execute(const QStringList& arguments) override;
static QSharedPointer<PasswordGenerator> createGenerator(QSharedPointer<QCommandLineParser> parser);
static const QCommandLineOption PasswordLengthOption; static const QCommandLineOption PasswordLengthOption;
static const QCommandLineOption LowerCaseOption; static const QCommandLineOption LowerCaseOption;
static const QCommandLineOption UpperCaseOption; static const QCommandLineOption UpperCaseOption;

View File

@ -1,4 +1,4 @@
.TH KEEPASSXC-CLI 1 "Nov 04, 2018" .TH KEEPASSXC-CLI 1 "June 15, 2019"
.SH NAME .SH NAME
keepassxc-cli \- command line interface for the \fBKeePassXC\fP password manager. keepassxc-cli \- command line interface for the \fBKeePassXC\fP password manager.
@ -15,6 +15,7 @@ keepassxc-cli \- command line interface for the \fBKeePassXC\fP password manager
.IP "add [options] <database> <entry>" .IP "add [options] <database> <entry>"
Adds a new entry to a database. A password can be generated (\fI-g\fP option), or a prompt can be displayed to input the password (\fI-p\fP option). Adds a new entry to a database. A password can be generated (\fI-g\fP option), or a prompt can be displayed to input the password (\fI-p\fP option).
The same password generation options as documented for the generate command can be used when the \fI-g\fP option is set.
.IP "analyze [options] <database>" .IP "analyze [options] <database>"
Analyze passwords in a database for weaknesses. Analyze passwords in a database for weaknesses.
@ -30,6 +31,7 @@ Generate a random diceware passphrase.
.IP "edit [options] <database> <entry>" .IP "edit [options] <database> <entry>"
Edits a database entry. A password can be generated (\fI-g\fP option), or a prompt can be displayed to input the password (\fI-p\fP option). Edits a database entry. A password can be generated (\fI-g\fP option), or a prompt can be displayed to input the password (\fI-p\fP option).
The same password generation options as documented for the generate command can be used when the \fI-g\fP option is set.
.IP "estimate [options] [password]" .IP "estimate [options] [password]"
Estimates the entropy of a password. The password to estimate can be provided as a positional argument, or using the standard input. Estimates the entropy of a password. The password to estimate can be provided as a positional argument, or using the standard input.
@ -94,6 +96,8 @@ Use the same credentials for unlocking both database.
.SS "Add and edit options" .SS "Add and edit options"
The same password generation options as documented for the generate command can be used
with those 2 commands when the -g option is set.
.IP "-u, --username <username>" .IP "-u, --username <username>"
Specify the username of the entry. Specify the username of the entry.
@ -107,9 +111,6 @@ Use a password prompt for the entry's password.
.IP "-g, --generate" .IP "-g, --generate"
Generate a new password for the entry. Generate a new password for the entry.
.IP "-l, --password-length"
Specify the length of the password to generate.
.SS "Edit options" .SS "Edit options"
@ -176,21 +177,29 @@ Flattens the output to single lines. When this option is enabled, subgroups and
.IP "-L, --length <length>" .IP "-L, --length <length>"
Desired length for the generated password. [Default: 16] Desired length for the generated password. [Default: 16]
.IP "-l" .IP "-l --lower"
Use lowercase characters for the generated password. [Default: Enabled] Use lowercase characters for the generated password. [Default: Enabled]
.IP "-u" .IP "-U --upper"
Use uppercase characters for the generated password. [Default: Enabled] Use uppercase characters for the generated password. [Default: Enabled]
.IP "-n" .IP "-n --numeric"
Use numbers characters for the generated password. [Default: Enabled] Use numbers characters for the generated password. [Default: Enabled]
.IP "-s" .IP "-s --special"
Use special characters for the generated password. [Default: Disabled] Use special characters for the generated password. [Default: Disabled]
.IP "-e" .IP "-e --extended"
Use extended ASCII characters for the generated password. [Default: Disabled] Use extended ASCII characters for the generated password. [Default: Disabled]
.IP "-x --exclude <chars>"
Comma-separated list of characters to exclude from the generated password. None is excluded by default.
.IP "--exclude-similar"
Exclude similar looking characters. [Default: Disabled]
.IP "--every-group"
Include characters from every selected group. [Default: Disabled]
.SH REPORTING BUGS .SH REPORTING BUGS

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -193,7 +193,7 @@ void TestCli::testAdd()
"--url", "--url",
"https://example.com/", "https://example.com/",
"-g", "-g",
"-l", "-L",
"20", "20",
m_dbFile->fileName(), m_dbFile->fileName(),
"/newuser-entry"}); "/newuser-entry"});
@ -212,13 +212,17 @@ void TestCli::testAdd()
// Quiet option // Quiet option
qint64 pos = m_stdoutFile->pos(); qint64 pos = m_stdoutFile->pos();
qint64 posErr = m_stderrFile->pos();
Utils::Test::setNextPassword("a"); Utils::Test::setNextPassword("a");
addCmd.execute({"add", "-q", "-u", "newuser", "-g", "-l", "20", m_dbFile->fileName(), "/newentry-quiet"}); addCmd.execute({"add", "-q", "-u", "newuser", "-g", "-L", "20", m_dbFile->fileName(), "/newentry-quiet"});
m_stdoutFile->seek(pos); m_stdoutFile->seek(pos);
m_stderrFile->seek(posErr);
QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); QCOMPARE(m_stdoutFile->readAll(), QByteArray(""));
QCOMPARE(m_stderrFile->readAll(), QByteArray(""));
db = readTestDatabase(); db = readTestDatabase();
entry = db->rootGroup()->findEntryByPath("/newentry-quiet"); entry = db->rootGroup()->findEntryByPath("/newentry-quiet");
QVERIFY(entry); QVERIFY(entry);
QCOMPARE(entry->password().size(), 20);
Utils::Test::setNextPassword("a"); Utils::Test::setNextPassword("a");
Utils::Test::setNextPassword("newpassword"); Utils::Test::setNextPassword("newpassword");
@ -227,9 +231,6 @@ void TestCli::testAdd()
"newuser2", "newuser2",
"--url", "--url",
"https://example.net/", "https://example.net/",
"-g",
"-l",
"20",
"-p", "-p",
m_dbFile->fileName(), m_dbFile->fileName(),
"/newuser-entry2"}); "/newuser-entry2"});
@ -240,6 +241,61 @@ void TestCli::testAdd()
QCOMPARE(entry->username(), QString("newuser2")); QCOMPARE(entry->username(), QString("newuser2"));
QCOMPARE(entry->url(), QString("https://example.net/")); QCOMPARE(entry->url(), QString("https://example.net/"));
QCOMPARE(entry->password(), QString("newpassword")); QCOMPARE(entry->password(), QString("newpassword"));
// Password generation options
pos = m_stdoutFile->pos();
posErr = m_stderrFile->pos();
Utils::Test::setNextPassword("a");
addCmd.execute({"add",
"-u",
"newuser3",
"-g",
"-L",
"34",
m_dbFile->fileName(),
"/newuser-entry3"});
m_stdoutFile->seek(pos);
m_stderrFile->seek(posErr);
m_stdoutFile->readLine(); // skip password prompt
QCOMPARE(m_stdoutFile->readAll(), QByteArray("Successfully added entry newuser-entry3.\n"));
QCOMPARE(m_stderrFile->readAll(), QByteArray(""));
db = readTestDatabase();
entry = db->rootGroup()->findEntryByPath("/newuser-entry3");
QVERIFY(entry);
QCOMPARE(entry->username(), QString("newuser3"));
QCOMPARE(entry->password().size(), 34);
QRegularExpression defaultPasswordClassesRegex("^[a-zA-Z0-9]+$");
QVERIFY(defaultPasswordClassesRegex.match(entry->password()).hasMatch());
pos = m_stdoutFile->pos();
posErr = m_stderrFile->pos();
Utils::Test::setNextPassword("a");
addCmd.execute({"add",
"-u",
"newuser4",
"-g",
"-L",
"20",
"--every-group",
"-s",
"-n",
"-U",
"-l",
m_dbFile->fileName(),
"/newuser-entry4"});
m_stdoutFile->seek(pos);
m_stderrFile->seek(posErr);
m_stdoutFile->readLine(); // skip password prompt
QCOMPARE(m_stdoutFile->readAll(), QByteArray("Successfully added entry newuser-entry4.\n"));
QCOMPARE(m_stderrFile->readAll(), QByteArray(""));
db = readTestDatabase();
entry = db->rootGroup()->findEntryByPath("/newuser-entry4");
QVERIFY(entry);
QCOMPARE(entry->username(), QString("newuser4"));
QCOMPARE(entry->password().size(), 20);
QVERIFY(!defaultPasswordClassesRegex.match(entry->password()).hasMatch());
} }
void TestCli::testAnalyze() void TestCli::testAnalyze()
@ -515,15 +571,18 @@ void TestCli::testEdit()
// Quiet option // Quiet option
qint64 pos = m_stdoutFile->pos(); qint64 pos = m_stdoutFile->pos();
qint64 posErr = m_stderrFile->pos();
Utils::Test::setNextPassword("a"); Utils::Test::setNextPassword("a");
editCmd.execute({"edit", m_dbFile->fileName(), "-q", "-t", "newtitle", "/Sample Entry"}); editCmd.execute({"edit", m_dbFile->fileName(), "-q", "-t", "newertitle", "/newtitle"});
m_stdoutFile->seek(pos); m_stdoutFile->seek(pos);
m_stderrFile->seek(posErr);
QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); QCOMPARE(m_stdoutFile->readAll(), QByteArray(""));
QCOMPARE(m_stderrFile->readAll(), QByteArray(""));
Utils::Test::setNextPassword("a"); Utils::Test::setNextPassword("a");
editCmd.execute({"edit", "-g", m_dbFile->fileName(), "/newtitle"}); editCmd.execute({"edit", "-g", m_dbFile->fileName(), "/newertitle"});
db = readTestDatabase(); db = readTestDatabase();
entry = db->rootGroup()->findEntryByPath("/newtitle"); entry = db->rootGroup()->findEntryByPath("/newertitle");
QVERIFY(entry); QVERIFY(entry);
QCOMPARE(entry->username(), QString("newuser")); QCOMPARE(entry->username(), QString("newuser"));
QCOMPARE(entry->url(), QString("https://otherurl.example.com/")); QCOMPARE(entry->url(), QString("https://otherurl.example.com/"));
@ -531,20 +590,48 @@ void TestCli::testEdit()
QVERIFY(entry->password() != QString("Password")); QVERIFY(entry->password() != QString("Password"));
Utils::Test::setNextPassword("a"); Utils::Test::setNextPassword("a");
editCmd.execute({"edit", "-g", "-l", "34", "-t", "yet another title", m_dbFile->fileName(), "/newtitle"}); editCmd.execute({"edit", "-g", "-L", "34", "-t", "evennewertitle", m_dbFile->fileName(), "/newertitle"});
db = readTestDatabase(); db = readTestDatabase();
entry = db->rootGroup()->findEntryByPath("/yet another title"); entry = db->rootGroup()->findEntryByPath("/evennewertitle");
QVERIFY(entry); QVERIFY(entry);
QCOMPARE(entry->username(), QString("newuser")); QCOMPARE(entry->username(), QString("newuser"));
QCOMPARE(entry->url(), QString("https://otherurl.example.com/")); QCOMPARE(entry->url(), QString("https://otherurl.example.com/"));
QVERIFY(entry->password() != QString("Password")); QVERIFY(entry->password() != QString("Password"));
QCOMPARE(entry->password().size(), 34); QCOMPARE(entry->password().size(), 34);
QRegularExpression defaultPasswordClassesRegex("^[a-zA-Z0-9]+$");
QVERIFY(defaultPasswordClassesRegex.match(entry->password()).hasMatch());
pos = m_stdoutFile->pos();
posErr = m_stderrFile->pos();
Utils::Test::setNextPassword("a");
editCmd.execute({"edit",
"-g",
"-L",
"20",
"--every-group",
"-s",
"-n",
"--upper",
"-l",
m_dbFile->fileName(),
"/evennewertitle"});
m_stdoutFile->seek(pos);
m_stderrFile->seek(posErr);
m_stdoutFile->readLine(); // skip password prompt
QCOMPARE(m_stderrFile->readAll(), QByteArray(""));
QCOMPARE(m_stdoutFile->readAll(), QByteArray("Successfully edited entry evennewertitle.\n"));
db = readTestDatabase();
entry = db->rootGroup()->findEntryByPath("/evennewertitle");
QVERIFY(entry);
QCOMPARE(entry->password().size(), 20);
QVERIFY(!defaultPasswordClassesRegex.match(entry->password()).hasMatch());
Utils::Test::setNextPassword("a"); Utils::Test::setNextPassword("a");
Utils::Test::setNextPassword("newpassword"); Utils::Test::setNextPassword("newpassword");
editCmd.execute({"edit", "-p", m_dbFile->fileName(), "/yet another title"}); editCmd.execute({"edit", "-p", m_dbFile->fileName(), "/evennewertitle"});
db = readTestDatabase(); db = readTestDatabase();
entry = db->rootGroup()->findEntryByPath("/yet another title"); entry = db->rootGroup()->findEntryByPath("/evennewertitle");
QVERIFY(entry); QVERIFY(entry);
QCOMPARE(entry->password(), QString("newpassword")); QCOMPARE(entry->password(), QString("newpassword"));
} }
@ -697,7 +784,7 @@ void TestCli::testGenerate_data()
QTest::newRow("default") << QStringList{"generate"} << "^[^\r\n]+$"; QTest::newRow("default") << QStringList{"generate"} << "^[^\r\n]+$";
QTest::newRow("length") << QStringList{"generate", "-L", "13"} << "^.{13}$"; QTest::newRow("length") << QStringList{"generate", "-L", "13"} << "^.{13}$";
QTest::newRow("lowercase") << QStringList{"generate", "-L", "14", "-l"} << "^[a-z]{14}$"; QTest::newRow("lowercase") << QStringList{"generate", "-L", "14", "-l"} << "^[a-z]{14}$";
QTest::newRow("uppercase") << QStringList{"generate", "-L", "15", "-u"} << "^[A-Z]{15}$"; QTest::newRow("uppercase") << QStringList{"generate", "-L", "15", "--upper"} << "^[A-Z]{15}$";
QTest::newRow("numbers") << QStringList{"generate", "-L", "16", "-n"} << "^[0-9]{16}$"; QTest::newRow("numbers") << QStringList{"generate", "-L", "16", "-n"} << "^[0-9]{16}$";
QTest::newRow("special") << QStringList{"generate", "-L", "200", "-s"} QTest::newRow("special") << QStringList{"generate", "-L", "200", "-s"}
<< R"(^[\(\)\[\]\{\}\.\-*|\\,:;"'\/\_!+-<=>?#$%&^`@~]{200}$)"; << R"(^[\(\)\[\]\{\}\.\-*|\\,:;"'\/\_!+-<=>?#$%&^`@~]{200}$)";
@ -706,13 +793,13 @@ void TestCli::testGenerate_data()
QTest::newRow("extended") << QStringList{"generate", "-L", "50", "-e"} QTest::newRow("extended") << QStringList{"generate", "-L", "50", "-e"}
<< R"(^[^a-zA-Z0-9\(\)\[\]\{\}\.\-\*\|\\,:;"'\/\_!+-<=>?#$%&^`@~]{50}$)"; << R"(^[^a-zA-Z0-9\(\)\[\]\{\}\.\-\*\|\\,:;"'\/\_!+-<=>?#$%&^`@~]{50}$)";
QTest::newRow("numbers + lowercase + uppercase") QTest::newRow("numbers + lowercase + uppercase")
<< QStringList{"generate", "-L", "16", "-n", "-u", "-l"} << "^[0-9a-zA-Z]{16}$"; << QStringList{"generate", "-L", "16", "-n", "--upper", "-l"} << "^[0-9a-zA-Z]{16}$";
QTest::newRow("numbers + lowercase + uppercase (exclude)") QTest::newRow("numbers + lowercase + uppercase (exclude)")
<< QStringList{"generate", "-L", "500", "-n", "-u", "-l", "-x", "abcdefg0123@"} << "^[^abcdefg0123@]{500}$"; << QStringList{"generate", "-L", "500", "-n", "-U", "-l", "-x", "abcdefg0123@"} << "^[^abcdefg0123@]{500}$";
QTest::newRow("numbers + lowercase + uppercase (exclude similar)") QTest::newRow("numbers + lowercase + uppercase (exclude similar)")
<< QStringList{"generate", "-L", "200", "-n", "-u", "-l", "--exclude-similar"} << "^[^l1IO0]{200}$"; << QStringList{"generate", "-L", "200", "-n", "-U", "-l", "--exclude-similar"} << "^[^l1IO0]{200}$";
QTest::newRow("uppercase + lowercase (every)") QTest::newRow("uppercase + lowercase (every)")
<< QStringList{"generate", "-L", "2", "-u", "-l", "--every-group"} << "^[a-z][A-Z]|[A-Z][a-z]$"; << QStringList{"generate", "-L", "2", "--upper", "-l", "--every-group"} << "^[a-z][A-Z]|[A-Z][a-z]$";
QTest::newRow("numbers + lowercase (every)") QTest::newRow("numbers + lowercase (every)")
<< QStringList{"generate", "-L", "2", "-n", "-l", "--every-group"} << "^[a-z][0-9]|[0-9][a-z]$"; << QStringList{"generate", "-L", "2", "-n", "-l", "--every-group"} << "^[a-z][0-9]|[0-9][a-z]$";
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by