/* * Copyright (C) 2017 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 "Utils.h" #include "core/Database.h" #include "core/Entry.h" #include "core/EntryAttributes.h" #include "core/Global.h" #include "keys/FileKey.h" #ifdef WITH_XC_YUBIKEY #include "keys/ChallengeResponseKey.h" #endif #ifdef Q_OS_WIN #include #else #include #include #endif #include #include namespace Utils { QTextStream STDOUT; QTextStream STDERR; QTextStream STDIN; QTextStream DEVNULL; 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 // 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 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 unlockDatabase(const QString& databaseFilename, bool isPasswordProtected, const QString& keyFilename, const QString& yubiKeySlot, bool quiet) { auto& err = quiet ? DEVNULL : STDERR; auto compositeKey = QSharedPointer::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::create(); passwordKey->setPassword(line); compositeKey->addKey(passwordKey); } if (!keyFilename.isEmpty()) { auto fileKey = QSharedPointer::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); } #ifdef WITH_XC_YUBIKEY 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(new ChallengeResponseKey({serial, slot})); compositeKey->addChallengeResponseKey(key); YubiKey::instance()->findValidKeys(); } #else Q_UNUSED(yubiKeySlot); #endif // WITH_XC_YUBIKEY auto db = QSharedPointer::create(); QString error; if (db->open(databaseFilename, compositeKey, &error)) { return db; } else { err << error << Qt::endl; return {}; } } /** * 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 getConfirmedPassword() { auto& err = STDERR; auto& in = STDIN; QSharedPointer 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::create(""); } err << Qt::endl; } else { err << QObject::tr("Repeat password: "); err.flush(); auto repeat = Utils::getPassword(); if (password == repeat) { passwordKey = QSharedPointer::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> 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 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(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) { auto& err = Utils::STDERR; QString error; fileKey = QSharedPointer(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