mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-04-01 10:58:29 -04:00

* 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.
453 lines
14 KiB
C++
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
|