keepassxc/src/cli/Utils.cpp
Jonathan White cfa94f69f3
Refactor: Move to simple default builds
* Remove individual feature flags in favor of a single `KPXC_MINIMAL` flag that removes advanced features from the build. Basic features are no longer guarded by feature flags.
* Basic features: Auto-Type, Yubikey, KeeShare
* Advanced features include: Browser (and passkeys), SSH Agent, and Secret Service
* Networking, Documentation, and Update Checking remain as feature flags to accommodate various distro requirements.

This change also cleans up the main CMakeLists.txt by re-arranging some content and placing macros into a dedicated include file. The minimum CMake version was bumped to 3.16.0 to conform to our minimum Ubuntu support of Focal (20.04). This also allows us to default to C++20, we fall back to C++17 for Qt versions less than 5.15.0 due to lack of support.

Lastly this change removes the KEEPASSXC_BUILD_TYPE="PreRelease" which is never used. We only support "Snapshot" and "Release" now.
2025-01-02 23:38:06 -05:00

453 lines
14 KiB
C++

/*
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "Utils.h"
#include "core/Database.h"
#include "core/Entry.h"
#include "core/EntryAttributes.h"
#include "core/Global.h"
#include "keys/ChallengeResponseKey.h"
#include "keys/FileKey.h"
#ifdef Q_OS_WIN
#include <windows.h>
#else
#include <termios.h>
#include <unistd.h>
#endif
#include <QFileInfo>
#include <QProcess>
namespace Utils
{
QTextStream STDOUT;
QTextStream STDERR;
QTextStream STDIN;
QTextStream DEVNULL;
#ifdef Q_OS_WIN
UINT origCodePage;
UINT origOutputCodePage;
#endif
void setDefaultTextStreams()
{
auto fd = new QFile();
fd->open(stdout, QIODevice::WriteOnly);
STDOUT.setDevice(fd);
fd = new QFile();
fd->open(stderr, QIODevice::WriteOnly);
STDERR.setDevice(fd);
fd = new QFile();
fd->open(stdin, QIODevice::ReadOnly);
STDIN.setDevice(fd);
fd = new QFile();
#ifdef Q_OS_WIN
fd->open(fopen("nul", "w"), QIODevice::WriteOnly);
#else
fd->open(fopen("/dev/null", "w"), QIODevice::WriteOnly);
#endif
DEVNULL.setDevice(fd);
#ifdef Q_OS_WIN
origCodePage = GetConsoleCP();
origOutputCodePage = GetConsoleOutputCP();
// On Windows, we ask via keepassxc-cli.exe.manifest to use UTF-8,
// but the console code-page isn't automatically changed to match.
SetConsoleCP(GetACP());
SetConsoleOutputCP(GetACP());
#endif
}
void resetTextStreams()
{
#ifdef Q_OS_WIN
SetConsoleCP(origCodePage);
SetConsoleOutputCP(origOutputCodePage);
#endif
}
void setStdinEcho(bool enable = true)
{
#ifdef Q_OS_WIN
HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode;
GetConsoleMode(hIn, &mode);
if (enable) {
mode |= ENABLE_ECHO_INPUT;
} else {
mode &= ~ENABLE_ECHO_INPUT;
}
SetConsoleMode(hIn, mode);
#else
struct termios t;
tcgetattr(STDIN_FILENO, &t);
if (enable) {
t.c_lflag |= ECHO;
} else {
t.c_lflag &= ~ECHO;
}
tcsetattr(STDIN_FILENO, TCSANOW, &t);
#endif
}
QSharedPointer<Database> unlockDatabase(const QString& databaseFilename,
bool isPasswordProtected,
const QString& keyFilename,
const QString& yubiKeySlot,
bool quiet)
{
auto& err = quiet ? DEVNULL : STDERR;
auto compositeKey = QSharedPointer<CompositeKey>::create();
QFileInfo dbFileInfo(databaseFilename);
if (dbFileInfo.canonicalFilePath().isEmpty()) {
err << QObject::tr("Failed to open database file %1: not found").arg(databaseFilename) << Qt::endl;
return {};
}
if (!dbFileInfo.isFile()) {
err << QObject::tr("Failed to open database file %1: not a plain file").arg(databaseFilename) << Qt::endl;
return {};
}
if (!dbFileInfo.isReadable()) {
err << QObject::tr("Failed to open database file %1: not readable").arg(databaseFilename) << Qt::endl;
return {};
}
if (isPasswordProtected) {
err << QObject::tr("Enter password to unlock %1: ").arg(databaseFilename) << Qt::flush;
QString line = Utils::getPassword(quiet);
auto passwordKey = QSharedPointer<PasswordKey>::create();
passwordKey->setPassword(line);
compositeKey->addKey(passwordKey);
}
if (!keyFilename.isEmpty()) {
auto fileKey = QSharedPointer<FileKey>::create();
QString errorMessage;
// LCOV_EXCL_START
if (!fileKey->load(keyFilename, &errorMessage)) {
err << QObject::tr("Failed to load key file %1: %2").arg(keyFilename, errorMessage) << Qt::endl;
return {};
}
if (fileKey->type() != FileKey::KeePass2XMLv2 && fileKey->type() != FileKey::Hashed) {
err << QObject::tr("WARNING: You are using an old key file format which KeePassXC may\n"
"stop supporting in the future.\n\n"
"Please consider generating a new key file.")
<< Qt::endl;
}
// LCOV_EXCL_STOP
compositeKey->addKey(fileKey);
}
if (!yubiKeySlot.isEmpty()) {
unsigned int serial = 0;
int slot;
bool ok = false;
auto parts = yubiKeySlot.split(":");
slot = parts[0].toInt(&ok);
if (!ok || (slot != 1 && slot != 2)) {
err << QObject::tr("Invalid YubiKey slot %1").arg(parts[0]) << Qt::endl;
return {};
}
if (parts.size() > 1) {
serial = parts[1].toUInt(&ok, 10);
if (!ok) {
err << QObject::tr("Invalid YubiKey serial %1").arg(parts[1]) << Qt::endl;
return {};
}
}
QObject::connect(YubiKey::instance(), &YubiKey::userInteractionRequest, [&] {
err << QObject::tr("Please present or touch your YubiKey to continue.") << "\n\n" << Qt::flush;
});
auto key = QSharedPointer<ChallengeResponseKey>(new ChallengeResponseKey({serial, slot}));
compositeKey->addChallengeResponseKey(key);
YubiKey::instance()->findValidKeys();
}
auto db = QSharedPointer<Database>::create();
QString error;
if (!db->open(databaseFilename, compositeKey, &error)) {
err << error << Qt::endl;
return {};
}
return db;
}
/**
* Read a user password from STDIN or return a password previously
* set by \link setNextPassword().
*
* @return the password
*/
QString getPassword(bool quiet)
{
#ifdef __AFL_COMPILER
// Fuzz test build takes password from environment variable to
// allow non-interactive operation
const auto env = getenv("KEYPASSXC_AFL_PASSWORD");
return env ? env : "";
#else
auto& in = STDIN;
auto& out = quiet ? DEVNULL : STDERR;
setStdinEcho(false);
QString line = in.readLine();
setStdinEcho(true);
out << Qt::endl;
return line;
#endif // __AFL_COMPILER
}
/**
* Read optional password from stdin.
*
* @return Pointer to the PasswordKey or null if passwordkey is skipped
* by user
*/
QSharedPointer<PasswordKey> getConfirmedPassword()
{
auto& err = STDERR;
auto& in = STDIN;
QSharedPointer<PasswordKey> passwordKey;
err << QObject::tr("Enter password to encrypt database (optional): ");
err.flush();
auto password = Utils::getPassword();
if (password.isEmpty()) {
err << QObject::tr("Do you want to create a database with an empty password? [y/N]: ");
err.flush();
auto ans = in.readLine();
if (ans.toLower().startsWith("y")) {
passwordKey = QSharedPointer<PasswordKey>::create("");
}
err << Qt::endl;
} else {
err << QObject::tr("Repeat password: ");
err.flush();
auto repeat = Utils::getPassword();
if (password == repeat) {
passwordKey = QSharedPointer<PasswordKey>::create(password);
} else {
err << QObject::tr("Error: Passwords do not match.") << Qt::endl;
}
}
return passwordKey;
}
/**
* A valid and running event loop is needed to use the global QClipboard,
* so we need to use this from the CLI.
*/
int clipText(const QString& text)
{
auto& err = STDERR;
// List of programs and their arguments
QList<QPair<QString, QString>> clipPrograms;
#ifdef Q_OS_UNIX
if (QProcessEnvironment::systemEnvironment().contains("WAYLAND_DISPLAY")) {
clipPrograms << qMakePair(QStringLiteral("wl-copy"), QStringLiteral("-t text/plain"));
} else {
clipPrograms << qMakePair(QStringLiteral("xclip"), QStringLiteral("-selection clipboard -i"));
}
#endif
#ifdef Q_OS_MACOS
clipPrograms << qMakePair(QStringLiteral("pbcopy"), QStringLiteral(""));
#endif
#ifdef Q_OS_WIN
clipPrograms << qMakePair(QStringLiteral("clip"), QStringLiteral(""));
#endif
if (clipPrograms.isEmpty()) {
err << QObject::tr("No program defined for clipboard manipulation");
err.flush();
return EXIT_FAILURE;
}
QStringList failedProgramNames;
for (const auto& prog : clipPrograms) {
QScopedPointer<QProcess> clipProcess(new QProcess(nullptr));
// Skip empty parts, otherwise the program may clip the empty string
QStringList progArgs = prog.second.split(" ", Qt::SkipEmptyParts);
clipProcess->start(prog.first, progArgs);
clipProcess->waitForStarted();
if (clipProcess->state() != QProcess::Running) {
failedProgramNames.append(prog.first);
continue;
}
#ifdef Q_OS_WIN
// Windows clip command only understands Unicode written as UTF-16
auto data = QByteArray::fromRawData(reinterpret_cast<const char*>(text.utf16()), text.size() * 2);
if (clipProcess->write(data) == -1) {
#else
// Other platforms understand UTF-8
if (clipProcess->write(text.toUtf8()) == -1) {
#endif
qWarning("Unable to write to process : %s", qPrintable(clipProcess->errorString()));
}
clipProcess->waitForBytesWritten();
clipProcess->closeWriteChannel();
clipProcess->waitForFinished();
if (clipProcess->exitCode() == EXIT_SUCCESS) {
return EXIT_SUCCESS;
} else {
failedProgramNames.append(prog.first);
}
}
// No clipping program worked
err << QObject::tr("All clipping programs failed. Tried %1\n").arg(failedProgramNames.join(", "));
err.flush();
return EXIT_FAILURE;
}
/**
* Splits the given QString into a QString list. For example:
*
* "hello world" -> ["hello", "world"]
* "hello world" -> ["hello", "world"]
* "hello\\ world" -> ["hello world"] (i.e. backslash is an escape character
* "\"hello world\"" -> ["hello world"]
*/
QStringList splitCommandString(const QString& command)
{
QStringList result;
bool insideQuotes = false;
QString cur;
for (int i = 0; i < command.size(); ++i) {
QChar c = command[i];
if (c == '\\' && i < command.size() - 1) {
cur.append(command[i + 1]);
++i;
} else if (!insideQuotes && (c == ' ' || c == '\t')) {
if (!cur.isEmpty()) {
result.append(cur);
cur.clear();
}
} else if (c == '"' && (insideQuotes || i == 0 || command[i - 1].isSpace())) {
insideQuotes = !insideQuotes;
} else {
cur.append(c);
}
}
if (!cur.isEmpty()) {
result.append(cur);
}
return result;
}
QString getTopLevelField(const Entry* entry, const QString& fieldName)
{
if (fieldName == UuidFieldName) {
return entry->uuid().toString();
}
if (fieldName == TagsFieldName) {
return entry->tags();
}
return "";
}
QStringList findAttributes(const EntryAttributes& attributes, const QString& name)
{
QStringList result;
if (attributes.hasKey(name)) {
result.append(name);
return result;
}
for (const QString& key : attributes.keys()) {
if (key.compare(name, Qt::CaseSensitivity::CaseInsensitive) == 0) {
result.append(key);
}
}
return result;
}
/**
* Load a key file from disk. When the path specified does not exist a
* new file will be generated. No folders will be generated so the parent
* folder of the specified file needs to exist
*
* If the key file cannot be loaded or created the function will fail.
*
* @param path Path to the key file to be loaded
* @param fileKey Resulting fileKey
* @return true if the key file was loaded successfully
*/
bool loadFileKey(const QString& path, QSharedPointer<FileKey>& fileKey)
{
auto& err = Utils::STDERR;
QString error;
fileKey = QSharedPointer<FileKey>(new FileKey());
if (!QFileInfo::exists(path)) {
fileKey->create(path, &error);
if (!error.isEmpty()) {
err << QObject::tr("Creating KeyFile %1 failed: %2").arg(path, error) << Qt::endl;
return false;
}
}
if (!fileKey->load(path, &error)) {
err << QObject::tr("Loading KeyFile %1 failed: %2").arg(path, error) << Qt::endl;
return false;
}
return true;
}
} // namespace Utils