diff --git a/src/utils/gnupg.cpp b/src/utils/gnupg.cpp index 9ce7989c9..e8117ed0c 100644 --- a/src/utils/gnupg.cpp +++ b/src/utils/gnupg.cpp @@ -1,724 +1,721 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/gnupg.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-FileCopyrightText: 2020-2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "gnupg.h" #include "assuan.h" #include "compat.h" #include "compliance.h" #include "cryptoconfig.h" #include "hex.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include "gnupg-registry.h" #endif // Q_OS_WIN #include #include using namespace GpgME; QString Kleo::gnupgHomeDirectory() { static const QString homeDir = QString::fromUtf8(GpgME::dirInfo("homedir")); return homeDir; } QString Kleo::gnupgPrivateKeysDirectory() { static const QString dir = QDir{gnupgHomeDirectory()}.filePath(QStringLiteral("private-keys-v1.d")); return dir; } int Kleo::makeGnuPGError(int code) { return gpg_error(static_cast(code)); } static QString findGpgExe(GpgME::Engine engine, const QString &exe) { const GpgME::EngineInfo info = GpgME::engineInfo(engine); return info.fileName() ? QFile::decodeName(info.fileName()) : QStandardPaths::findExecutable(exe); } QString Kleo::gpgConfPath() { static const auto path = findGpgExe(GpgME::GpgConfEngine, QStringLiteral("gpgconf")); return path; } QString Kleo::gpgSmPath() { static const auto path = findGpgExe(GpgME::GpgSMEngine, QStringLiteral("gpgsm")); return path; } QString Kleo::gpgPath() { static const auto path = findGpgExe(GpgME::GpgEngine, QStringLiteral("gpg")); return path; } QStringList Kleo::gnupgFileWhitelist() { return { // The obvious pubring QStringLiteral("pubring.gpg"), // GnuPG 2.1 pubring QStringLiteral("pubring.kbx"), // Trust in X509 Certificates QStringLiteral("trustlist.txt"), // Trustdb controls ownertrust and thus WOT validity QStringLiteral("trustdb.gpg"), // We want to update when smartcard status changes QStringLiteral("reader*.status"), // No longer used in 2.1 but for 2.0 we want this QStringLiteral("secring.gpg"), // Secret keys (living under private-keys-v1.d/) QStringLiteral("*.key"), // Changes to the trustmodel / compliance mode might // affect validity so we check this, too. // Globbing for gpg.conf* here will trigger too often // as gpgconf creates files like gpg.conf.bak or // gpg.conf.tmp12312.gpgconf that should not trigger // a change. QStringLiteral("gpg.conf"), QStringLiteral("gpg.conf-?"), QStringLiteral("gpg.conf-?.?"), }; } QStringList Kleo::gnupgFolderWhitelist() { static const QDir gnupgHome{gnupgHomeDirectory()}; return { gnupgHome.path(), gnupgPrivateKeysDirectory(), }; } namespace { class Gpg4win { public: static const Gpg4win *instance() { // We use singleton to do the signature check only once. static Gpg4win *inst = nullptr; if (!inst) { inst = new Gpg4win(); } return inst; } private: QString mVersion; QString mDescription; QString mDescLong; QString mBrandingWindowTitle; QString mBrandingIcon; bool mSignedVersion; Gpg4win() : mVersion(QStringLiteral("Unknown Windows Version")) , mDescription(i18n("Certificate Manager and Unified Crypto GUI")) , mDescLong(QStringLiteral("Visit the Gpg4win homepage")) , mSignedVersion(false) { const QString instPath = Kleo::gpg4winInstallPath(); const QString verPath = instPath + QStringLiteral("/../VERSION"); QFile versionFile(verPath); // Open the file first to avoid a verify and then read issue where // "auditors" might say its an issue,... if (!versionFile.open(QIODevice::ReadOnly)) { return; } // Expect a three line format of three HTML strings. const auto versVersion = QString::fromUtf8(versionFile.readLine()).trimmed(); const auto versDescription = QString::fromUtf8(versionFile.readLine()).trimmed(); const auto versDescLong = QString::fromUtf8(versionFile.readLine()).trimmed(); // read optional two branding strings const auto brandingWindowTitle = QString::fromUtf8(versionFile.readLine()).trimmed(); const auto brandingIcon = QString::fromUtf8(versionFile.readLine()).trimmed(); const QString sigPath = verPath + QStringLiteral(".sig"); QFileInfo versionSig(instPath + QStringLiteral("/../VERSION.sig")); if (versionSig.exists()) { /* We have a signed version so let us check it against the GnuPG * release keys. */ QProcess gpgv; gpgv.setProgram(Kleo::gpgPath().replace(QStringLiteral("gpg.exe"), QStringLiteral("gpgv.exe"))); const QString keyringPath(QStringLiteral("%1/../share/gnupg/distsigkey.gpg").arg(Kleo::gnupgInstallPath())); gpgv.setArguments(QStringList() << QStringLiteral("--keyring") << keyringPath << QStringLiteral("--") << sigPath << verPath); gpgv.start(); gpgv.waitForFinished(); if (gpgv.exitStatus() == QProcess::NormalExit && !gpgv.exitCode()) { qCDebug(LIBKLEO_LOG) << "Valid Version: " << versVersion; mVersion = versVersion; mDescription = versDescription; mDescLong = versDescLong; mBrandingWindowTitle = brandingWindowTitle; mBrandingIcon = brandingIcon; mSignedVersion = true; } else { qCDebug(LIBKLEO_LOG) << "gpgv failed with stderr: " << gpgv.readAllStandardError(); qCDebug(LIBKLEO_LOG) << "gpgv stdout" << gpgv.readAllStandardOutput(); } } else { qCDebug(LIBKLEO_LOG) << "No signed VERSION file found."; } // Also take Version information from unsigned Versions. mVersion = versVersion; } public: const QString &version() const { return mVersion; } const QString &description() const { return mDescription; } const QString &longDescription() const { return mDescLong; } bool isSignedVersion() const { return mSignedVersion; } const QString &brandingWindowTitle() const { return mBrandingWindowTitle; } const QString &brandingIcon() const { return mBrandingIcon; } }; } // namespace bool Kleo::gpg4winSignedversion() { return Gpg4win::instance()->isSignedVersion(); } QString Kleo::gpg4winVersionNumber() { // extract the actual version number from the string returned by Gpg4win::version(); // we assume that Gpg4win::version() returns a version number (conforming to the semantic // versioning spec) optionally prefixed with some text followed by a dash, // e.g. "Gpg4win-3.1.15-beta15"; see https://dev.gnupg.org/T5663 static const QRegularExpression catchSemVerRegExp{QLatin1String{R"(-([0-9]+(?:\.[0-9]+)*(?:-[.0-9A-Za-z-]+)?(?:\+[.0-9a-zA-Z-]+)?)$)"}}; QString ret; const auto match = catchSemVerRegExp.match(gpg4winVersion()); if (match.hasMatch()) { ret = match.captured(1); } else { ret = gpg4winVersion(); } qCDebug(LIBKLEO_LOG) << __func__ << "returns" << ret; return ret; } QString Kleo::gpg4winVersion() { return Gpg4win::instance()->version(); } QString Kleo::gpg4winDescription() { return Gpg4win::instance()->description(); } QString Kleo::gpg4winLongDescription() { return Gpg4win::instance()->longDescription(); } QString Kleo::brandingWindowTitle() { return Gpg4win::instance()->brandingWindowTitle(); } QString Kleo::brandingIcon() { return Gpg4win::instance()->brandingIcon(); } QString Kleo::gpg4winInstallPath() { #ifdef Q_OS_WIN // QApplication::applicationDirPath is only used as a fallback // to support the case where Kleopatra is not installed from // Gpg4win but Gpg4win is also installed. char *instDir = read_w32_registry_string("HKEY_LOCAL_MACHINE", "Software\\GPG4Win", "Install Directory"); if (!instDir) { // Fallback to HKCU instDir = read_w32_registry_string("HKEY_CURRENT_USER", "Software\\GPG4Win", "Install Directory"); } if (instDir) { QString ret = QString::fromLocal8Bit(instDir) + QStringLiteral("/bin"); free(instDir); return ret; } qCDebug(LIBKLEO_LOG) << "Gpg4win not found. Falling back to Kleopatra instdir."; #endif return QCoreApplication::applicationDirPath(); } QString Kleo::gnupgInstallPath() { #ifdef Q_OS_WIN // QApplication::applicationDirPath is only used as a fallback // to support the case where Kleopatra is not installed from // Gpg4win but Gpg4win is also installed. char *instDir = read_w32_registry_string("HKEY_LOCAL_MACHINE", "Software\\GnuPG", "Install Directory"); if (!instDir) { // Fallback to HKCU instDir = read_w32_registry_string("HKEY_CURRENT_USER", "Software\\GnuPG", "Install Directory"); } if (instDir) { QString ret = QString::fromLocal8Bit(instDir) + QStringLiteral("/bin"); free(instDir); return ret; } qCDebug(LIBKLEO_LOG) << "GnuPG not found. Falling back to gpgconf list dir."; #endif return gpgConfListDir("bindir"); } QString Kleo::gpgConfListDir(const char *which) { if (!which || !*which) { return QString(); } const QString gpgConfPath = Kleo::gpgConfPath(); if (gpgConfPath.isEmpty()) { return QString(); } QProcess gpgConf; qCDebug(LIBKLEO_LOG) << "gpgConfListDir: starting " << qPrintable(gpgConfPath) << " --list-dirs"; gpgConf.start(gpgConfPath, QStringList() << QStringLiteral("--list-dirs")); if (!gpgConf.waitForFinished()) { qCDebug(LIBKLEO_LOG) << "gpgConfListDir(): failed to execute gpgconf: " << qPrintable(gpgConf.errorString()); qCDebug(LIBKLEO_LOG) << "output was:\n" << gpgConf.readAllStandardError().constData(); return QString(); } const QList lines = gpgConf.readAllStandardOutput().split('\n'); for (const QByteArray &line : lines) { if (line.startsWith(which) && line[qstrlen(which)] == ':') { const int begin = qstrlen(which) + 1; int end = line.size(); while (end && (line[end - 1] == '\n' || line[end - 1] == '\r')) { --end; } const QString result = QDir::fromNativeSeparators(QFile::decodeName(hexdecode(line.mid(begin, end - begin)))); qCDebug(LIBKLEO_LOG) << "gpgConfListDir: found " << qPrintable(result) << " for '" << which << "'entry"; return result; } } qCDebug(LIBKLEO_LOG) << "gpgConfListDir(): didn't find '" << which << "'" << "entry in output:\n" << gpgConf.readAllStandardError().constData(); return QString(); } static std::array getVersionFromString(const char *actual, bool &ok) { std::array ret; ok = false; if (!actual) { return ret; } QString versionString = QString::fromLatin1(actual); // Try to fix it up QRegExp rx(QLatin1String(R"((\d+)\.(\d+)\.(\d+)(?:-svn\d+)?.*)")); for (int i = 0; i < 3; i++) { if (!rx.exactMatch(versionString)) { versionString += QStringLiteral(".0"); } else { ok = true; break; } } if (!ok) { qCDebug(LIBKLEO_LOG) << "Can't parse version " << actual; return ret; } for (int i = 0; i < 3; ++i) { ret[i] = rx.cap(i + 1).toUInt(&ok); if (!ok) { return ret; } } ok = true; return ret; } bool Kleo::versionIsAtLeast(const char *minimum, const char *actual) { if (!minimum || !actual) { return false; } bool ok; const auto minimum_version = getVersionFromString(minimum, ok); if (!ok) { return false; } const auto actual_version = getVersionFromString(actual, ok); if (!ok) { return false; } return !std::lexicographical_compare(std::begin(actual_version), std::end(actual_version), std::begin(minimum_version), std::end(minimum_version)); } bool Kleo::engineIsVersion(int major, int minor, int patch, GpgME::Engine engine) { static QMap> cachedVersions; const int required_version[] = {major, minor, patch}; // Gpgconf means spawning processes which is expensive on windows. std::array actual_version; if (!cachedVersions.contains(engine)) { const Error err = checkEngine(engine); if (err.code() == GPG_ERR_INV_ENGINE) { qCDebug(LIBKLEO_LOG) << "isVersion: invalid engine. '"; return false; } const char *actual = GpgME::engineInfo(engine).version(); bool ok; actual_version = getVersionFromString(actual, ok); qCDebug(LIBKLEO_LOG) << "Parsed" << actual << "as: " << actual_version[0] << '.' << actual_version[1] << '.' << actual_version[2] << '.'; if (!ok) { return false; } cachedVersions.insert(engine, actual_version); } else { actual_version = cachedVersions.value(engine); } // return ! ( actual_version < required_version ) return !std::lexicographical_compare(std::begin(actual_version), std::end(actual_version), std::begin(required_version), std::end(required_version)); } const QString &Kleo::paperKeyInstallPath() { static const QString pkPath = (QStandardPaths::findExecutable(QStringLiteral("paperkey"), QStringList() << QCoreApplication::applicationDirPath()).isEmpty() ? QStandardPaths::findExecutable(QStringLiteral("paperkey")) : QStandardPaths::findExecutable(QStringLiteral("paperkey"), QStringList() << QCoreApplication::applicationDirPath())); return pkPath; } bool Kleo::haveKeyserverConfigured() { if (engineIsVersion(2, 1, 19)) { // since 2.1.19 there is a builtin keyserver return true; } return !Kleo::keyserver().isEmpty(); } QString Kleo::keyserver() { QString result = getCryptoConfigStringValue("gpg", "keyserver"); if (result.isEmpty()) { result = getCryptoConfigStringValue("dirmngr", "keyserver"); } return result; } bool Kleo::haveX509DirectoryServerConfigured() { return !getCryptoConfigUrlList("dirmngr", "ldapserver").empty() // || !getCryptoConfigUrlList("dirmngr", "LDAP Server").empty() // || !getCryptoConfigUrlList("gpgsm", "keyserver").empty(); } bool Kleo::gpgComplianceP(const char *mode) { const auto conf = QGpgME::cryptoConfig(); const auto entry = getCryptoConfigEntry(conf, "gpg", "compliance"); return entry && entry->stringValue() == QString::fromLatin1(mode); } bool Kleo::gnupgUsesDeVsCompliance() { return DeVSCompliance::isActive(); } bool Kleo::gnupgIsDeVsCompliant() { return DeVSCompliance::isCompliant(); } #ifdef Q_OS_WIN -static unsigned int gpgConfGetConsoleOutputCodePage() +static unsigned int guessConsoleOutputCodePage() { - // calls `gpgconf --show-codepages` to determine the console output codepage used by gpg on Windows; - // for other OSs gpgconf returns nothing - const auto gpgConfPath = Kleo::gpgConfPath(); - if (gpgConfPath.isEmpty()) { - return 0; + /* Qt on Windows uses GetACP while GnuPG prefers + * GetConsoleOutputCP. + * + * As we are not a console application GetConsoleOutputCP + * usually returns 0. + * From experience the closest thing that let's us guess + * what GetConsoleOutputCP returns for a console application + * it appears to be the OEMCP. + */ + unsigned int cpno = GetConsoleOutputCP(); + if (!cpno) { + cpno = GetOEMCP(); } - QProcess gpgConf; - qCDebug(LIBKLEO_LOG) << __func__ << "starting" << gpgConfPath << "--show-codepages"; - gpgConf.start(gpgConfPath, {QStringLiteral("--show-codepages")}); - if (!gpgConf.waitForFinished()) { - qCDebug(LIBKLEO_LOG) << __func__ << "failed to execute gpgconf:" << gpgConf.errorString() << "\nstderr:" << gpgConf.readAllStandardError(); - return 0; + if (!cpno) { + cpno = GetACP(); } - - unsigned int cpno = 0; - const auto lines = gpgConf.readAllStandardOutput().split('\n'); - // look for a line of the form "Console: CP%u" or "Console: CP%u/CP%u" - for (const auto &l : lines) { - if (l.startsWith("Console: CP")) { - // the console output codepage is the second CP value if there are two - cpno = l.mid(l.lastIndexOf("CP") + 2).toUInt(); - break; - } - } - // if ConsoleOutputCP is 0 fall back to ACP (as gpg does in set_native_charset) - if (cpno == 0) { - qCDebug(LIBKLEO_LOG) << __func__ << "ConsoleOutputCP is" << cpno << "- use ACP"; - // look for a line of the form "ANSI: CP%u" - for (const auto &l : lines) { - if (l.startsWith("ANSI: CP")) { - cpno = l.mid(l.indexOf("CP") + 2).toUInt(); - break; - } - } + if (!cpno) { + qCDebug(LIBKLEO_LOG) << __func__ << "Failed to find native codepage"; } qCDebug(LIBKLEO_LOG) << __func__ << "returns" << cpno; return cpno; } static QString fromEncoding(unsigned int src_encoding, const char *data) { if (!data || !*data) { return {}; } // returns necessary buffer size including the terminating null character int n = MultiByteToWideChar(src_encoding, 0, data, -1, NULL, 0); if (n <= 0) { qCDebug(LIBKLEO_LOG) << __func__ << "determining necessary buffer size failed with error code" << GetLastError(); return QString(); } wchar_t *result = (wchar_t *)malloc((n + 1) * sizeof *result); n = MultiByteToWideChar(src_encoding, 0, data, -1, result, n); if (n <= 0) { free(result); qCDebug(LIBKLEO_LOG) << __func__ << "conversion failed with error code" << GetLastError(); return QString(); } const auto ret = QString::fromWCharArray(result, n - 1); free(result); return ret; } -#endif -QString Kleo::stringFromGpgOutput(const QByteArray &ba) +static QString stringFromGpgOutput_legacy(const QByteArray &ba) { -#ifdef Q_OS_WIN - static const unsigned int cpno = gpgConfGetConsoleOutputCodePage(); + static const unsigned int cpno = guessConsoleOutputCodePage(); if (cpno) { qCDebug(LIBKLEO_LOG) << __func__ << "trying to decode" << ba << "using codepage" << cpno; const auto rawData = QByteArray{ba}.replace("\r\n", "\n"); const auto s = fromEncoding(cpno, rawData.constData()); if (!s.isEmpty() || ba.isEmpty()) { return s; } qCDebug(LIBKLEO_LOG) << __func__ << "decoding output failed; falling back to QString::fromLocal8Bit()"; } + qCDebug(LIBKLEO_LOG) << __func__ << "decoding from local encoding:" << ba; + return QString::fromLocal8Bit(ba); +} #endif + +QString Kleo::stringFromGpgOutput(const QByteArray &ba) +{ +#ifdef Q_OS_WIN + // since 2.2.28, GnuPG always uses UTF-8 for console output (and input) + if (Kleo::engineIsVersion(2, 2, 28, GpgME::GpgEngine)) { + return QString::fromUtf8(ba); + } else { + return stringFromGpgOutput_legacy(ba); + } +#else return QString::fromLocal8Bit(ba); +#endif } QStringList Kleo::backendVersionInfo() { QStringList versions; if (Kleo::engineIsVersion(2, 2, 24, GpgME::GpgConfEngine)) { QProcess p; qCDebug(LIBKLEO_LOG) << "Running gpgconf --show-versions ..."; p.start(Kleo::gpgConfPath(), {QStringLiteral("--show-versions")}); // wait at most 1 second if (!p.waitForFinished(1000)) { qCDebug(LIBKLEO_LOG) << "Running gpgconf --show-versions timed out after 1 second."; } else if (p.exitStatus() != QProcess::NormalExit || p.exitCode() != 0) { qCDebug(LIBKLEO_LOG) << "Running gpgconf --show-versions failed:" << p.errorString(); qCDebug(LIBKLEO_LOG) << "gpgconf stderr:" << p.readAllStandardError(); qCDebug(LIBKLEO_LOG) << "gpgconf stdout:" << p.readAllStandardOutput(); } else { const QByteArray output = p.readAllStandardOutput().replace("\r\n", "\n"); qCDebug(LIBKLEO_LOG) << "gpgconf stdout:" << p.readAllStandardOutput(); const auto lines = output.split('\n'); for (const auto &line : lines) { if (line.startsWith("* GnuPG") || line.startsWith("* Libgcrypt")) { const auto components = line.split(' '); versions.push_back(QString::fromLatin1(components.at(1) + ' ' + components.value(2))); } } } } return versions; } namespace { template auto startGpgConf(const QStringList &arguments, Function1 onSuccess, Function2 onFailure) { auto process = new QProcess; process->setProgram(Kleo::gpgConfPath()); process->setArguments(arguments); QObject::connect(process, &QProcess::started, [process]() { qCDebug(LIBKLEO_LOG).nospace() << "gpgconf (" << process << ") was started successfully"; }); QObject::connect(process, &QProcess::errorOccurred, [process, onFailure](auto error) { qCDebug(LIBKLEO_LOG).nospace() << "Error while running gpgconf (" << process << "): " << error; process->deleteLater(); onFailure(); }); QObject::connect(process, &QProcess::readyReadStandardError, [process]() { for (const auto &line : process->readAllStandardError().trimmed().split('\n')) { qCDebug(LIBKLEO_LOG).nospace() << "gpgconf (" << process << ") stderr: " << line; } }); QObject::connect(process, &QProcess::readyReadStandardOutput, [process]() { (void)process->readAllStandardOutput(); /* ignore stdout */ }); QObject::connect(process, qOverload(&QProcess::finished), [process, onSuccess, onFailure](int exitCode, QProcess::ExitStatus exitStatus) { if (exitStatus == QProcess::NormalExit) { qCDebug(LIBKLEO_LOG).nospace() << "gpgconf (" << process << ") exited (exit code: " << exitCode << ")"; if (exitCode == 0) { onSuccess(); } else { onFailure(); } } else { qCDebug(LIBKLEO_LOG).nospace() << "gpgconf (" << process << ") crashed (exit code: " << exitCode << ")"; onFailure(); } process->deleteLater(); }); qCDebug(LIBKLEO_LOG).nospace() << "Starting gpgconf (" << process << ") with arguments " << process->arguments().join(QLatin1Char(' ')) << " ..."; process->start(); return process; } static auto startGpgConf(const QStringList &arguments) { return startGpgConf( arguments, []() {}, []() {}); } } void Kleo::launchGpgAgent() { static QPointer process; static qint64 mSecsSinceEpochOfLastLaunch = 0; static int numberOfFailedLaunches = 0; if (Kleo::Assuan::agentIsRunning()) { qCDebug(LIBKLEO_LOG) << __func__ << ": gpg-agent is already running"; return; } if (process) { qCDebug(LIBKLEO_LOG) << __func__ << ": gpg-agent is already being launched"; return; } const auto now = QDateTime::currentMSecsSinceEpoch(); if (now - mSecsSinceEpochOfLastLaunch < 1000) { // reduce attempts to launch the agent to 1 attempt per second return; } mSecsSinceEpochOfLastLaunch = now; if (numberOfFailedLaunches > 5) { qCWarning(LIBKLEO_LOG) << __func__ << ": Launching gpg-agent failed" << numberOfFailedLaunches << "times in a row. Giving up."; return; } process = startGpgConf( {QStringLiteral("--launch"), QStringLiteral("gpg-agent")}, []() { numberOfFailedLaunches = 0; }, []() { numberOfFailedLaunches++; }); } void Kleo::killDaemons() { static QPointer process; if (process) { qCDebug(LIBKLEO_LOG) << __func__ << ": The daemons are already being shut down"; return; } process = startGpgConf({QStringLiteral("--kill"), QStringLiteral("all")}); }