diff --git a/src/kleo/checksumdefinition.cpp b/src/kleo/checksumdefinition.cpp index e00cbef45..50660d3a0 100644 --- a/src/kleo/checksumdefinition.cpp +++ b/src/kleo/checksumdefinition.cpp @@ -1,431 +1,433 @@ /* -*- mode: c++; c-basic-offset:4 -*- checksumdefinition.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "checksumdefinition.h" #include "kleoexception.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef stdin #undef stdin // pah.. #endif using namespace Kleo; static QMutex installPathMutex; Q_GLOBAL_STATIC(QString, _installPath) QString ChecksumDefinition::installPath() { const QMutexLocker locker(&installPathMutex); QString *const ip = _installPath(); if (ip->isEmpty()) { if (QCoreApplication::instance()) { *ip = QCoreApplication::applicationDirPath(); } else { qCWarning(LIBKLEO_LOG) << "checksumdefinition.cpp: installPath() called before QCoreApplication was constructed"; } } return *ip; } void ChecksumDefinition::setInstallPath(const QString &ip) { const QMutexLocker locker(&installPathMutex); *_installPath() = ip; } // Checksum Definition #N groups static const QLatin1String ID_ENTRY("id"); static const QLatin1String NAME_ENTRY("Name"); static const QLatin1String CREATE_COMMAND_ENTRY("create-command"); static const QLatin1String VERIFY_COMMAND_ENTRY("verify-command"); static const QLatin1String FILE_PATTERNS_ENTRY("file-patterns"); static const QLatin1String OUTPUT_FILE_ENTRY("output-file"); static const QLatin1String FILE_PLACEHOLDER("%f"); static const QLatin1String INSTALLPATH_PLACEHOLDER("%I"); static const QLatin1String NULL_SEPARATED_STDIN_INDICATOR("0|"); static const QLatin1Char NEWLINE_SEPARATED_STDIN_INDICATOR('|'); // ChecksumOperations group static const QLatin1String CHECKSUM_DEFINITION_ID_ENTRY("checksum-definition-id"); namespace { class ChecksumDefinitionError : public Kleo::Exception { const QString m_id; public: ChecksumDefinitionError(const QString &id, const QString &message) : Kleo::Exception(GPG_ERR_INV_PARAMETER, i18n("Error in checksum definition %1: %2", id, message), MessageOnly) , m_id(id) { } ~ChecksumDefinitionError() throw() override { } const QString &checksumDefinitionId() const { return m_id; } }; } static QString try_extensions(const QString &path) { static const char exts[][4] = { "", "exe", "bat", "bin", "cmd", }; static const size_t numExts = sizeof exts / sizeof *exts; for (unsigned int i = 0; i < numExts; ++i) { const QFileInfo fi(path + QLatin1Char('.') + QLatin1String(exts[i])); if (fi.exists()) { return fi.filePath(); } } return QString(); } static void parse_command(QString cmdline, const QString &id, const QString &whichCommand, QString *command, QStringList *prefix, QStringList *suffix, ChecksumDefinition::ArgumentPassingMethod *method) { Q_ASSERT(prefix); Q_ASSERT(suffix); Q_ASSERT(method); KShell::Errors errors; QStringList l; if (cmdline.startsWith(NULL_SEPARATED_STDIN_INDICATOR)) { *method = ChecksumDefinition::NullSeparatedInputFile; cmdline.remove(0, 2); } else if (cmdline.startsWith(NEWLINE_SEPARATED_STDIN_INDICATOR)) { *method = ChecksumDefinition::NewlineSeparatedInputFile; cmdline.remove(0, 1); } else { *method = ChecksumDefinition::CommandLine; } if (*method != ChecksumDefinition::CommandLine && cmdline.contains(FILE_PLACEHOLDER)) { throw ChecksumDefinitionError(id, i18n("Cannot use both %f and | in '%1'", whichCommand)); } cmdline .replace(FILE_PLACEHOLDER, QLatin1String("__files_go_here__")) // .replace(INSTALLPATH_PLACEHOLDER, QStringLiteral("__path_goes_here__")); l = KShell::splitArgs(cmdline, KShell::AbortOnMeta | KShell::TildeExpand, &errors); l = l.replaceInStrings(QStringLiteral("__files_go_here__"), FILE_PLACEHOLDER); static const QRegularExpression regExpression(QLatin1String(".*__path_goes_here__.*")); if (l.indexOf(regExpression) >= 0) { l = l.replaceInStrings(QStringLiteral("__path_goes_here__"), ChecksumDefinition::installPath()); } if (errors == KShell::BadQuoting) { throw ChecksumDefinitionError(id, i18n("Quoting error in '%1' entry", whichCommand)); } if (errors == KShell::FoundMeta) { throw ChecksumDefinitionError(id, i18n("'%1' too complex (would need shell)", whichCommand)); } qCDebug(LIBKLEO_LOG) << "ChecksumDefinition[" << id << ']' << l; if (l.empty()) { throw ChecksumDefinitionError(id, i18n("'%1' entry is empty/missing", whichCommand)); } const QFileInfo fi1(l.front()); if (fi1.isAbsolute()) { *command = try_extensions(l.front()); } else { *command = QStandardPaths::findExecutable(fi1.fileName()); } if (command->isEmpty()) { throw ChecksumDefinitionError(id, i18n("'%1' empty or not found", whichCommand)); } const int idx1 = l.indexOf(FILE_PLACEHOLDER); if (idx1 < 0) { // none -> append *prefix = l.mid(1); } else { *prefix = l.mid(1, idx1 - 1); *suffix = l.mid(idx1 + 1); } switch (*method) { case ChecksumDefinition::CommandLine: qCDebug(LIBKLEO_LOG) << "ChecksumDefinition[" << id << ']' << *command << *prefix << FILE_PLACEHOLDER << *suffix; break; case ChecksumDefinition::NewlineSeparatedInputFile: qCDebug(LIBKLEO_LOG) << "ChecksumDefinition[" << id << ']' << "find | " << *command << *prefix; break; case ChecksumDefinition::NullSeparatedInputFile: qCDebug(LIBKLEO_LOG) << "ChecksumDefinition[" << id << ']' << "find -print0 | " << *command << *prefix; break; case ChecksumDefinition::NumArgumentPassingMethods: Q_ASSERT(!"Should not happen"); break; } } namespace { class KConfigBasedChecksumDefinition : public ChecksumDefinition { public: explicit KConfigBasedChecksumDefinition(const KConfigGroup &group) : ChecksumDefinition(group.readEntryUntranslated(ID_ENTRY), group.readEntry(NAME_ENTRY), group.readEntry(OUTPUT_FILE_ENTRY), group.readEntry(FILE_PATTERNS_ENTRY, QStringList())) { if (id().isEmpty()) { throw ChecksumDefinitionError(group.name(), i18n("'id' entry is empty/missing")); } if (outputFileName().isEmpty()) { throw ChecksumDefinitionError(id(), i18n("'output-file' entry is empty/missing")); } if (patterns().empty()) { throw ChecksumDefinitionError(id(), i18n("'file-patterns' entry is empty/missing")); } // create-command ArgumentPassingMethod method; parse_command(group.readEntry(CREATE_COMMAND_ENTRY), id(), CREATE_COMMAND_ENTRY, &m_createCommand, &m_createPrefixArguments, &m_createPostfixArguments, &method); setCreateCommandArgumentPassingMethod(method); // verify-command parse_command(group.readEntry(VERIFY_COMMAND_ENTRY), id(), VERIFY_COMMAND_ENTRY, &m_verifyCommand, &m_verifyPrefixArguments, &m_verifyPostfixArguments, &method); setVerifyCommandArgumentPassingMethod(method); } private: QString doGetCreateCommand() const override { return m_createCommand; } QStringList doGetCreateArguments(const QStringList &files) const override { return m_createPrefixArguments + files + m_createPostfixArguments; } QString doGetVerifyCommand() const override { return m_verifyCommand; } QStringList doGetVerifyArguments(const QStringList &files) const override { return m_verifyPrefixArguments + files + m_verifyPostfixArguments; } private: QString m_createCommand, m_verifyCommand; QStringList m_createPrefixArguments, m_createPostfixArguments; QStringList m_verifyPrefixArguments, m_verifyPostfixArguments; }; } ChecksumDefinition::ChecksumDefinition(const QString &id, const QString &label, const QString &outputFileName, const QStringList &patterns) : m_id(id) , m_label(label.isEmpty() ? id : label) , m_outputFileName(outputFileName) , m_patterns(patterns) , m_createMethod(CommandLine) , m_verifyMethod(CommandLine) { } ChecksumDefinition::~ChecksumDefinition() { } QString ChecksumDefinition::createCommand() const { return doGetCreateCommand(); } QString ChecksumDefinition::verifyCommand() const { return doGetVerifyCommand(); } #if 0 QStringList ChecksumDefinition::createCommandArguments(const QStringList &files) const { return doGetCreateArguments(files); } QStringList ChecksumDefinition::verifyCommandArguments(const QStringList &files) const { return doGetVerifyArguments(files); } #endif static QByteArray make_input(const QStringList &files, char sep) { QByteArray result; for (const QString &file : files) { result += QFile::encodeName(file); result += sep; } return result; } static bool start_command(QProcess *p, const char *functionName, const QString &cmd, const QStringList &args, const QStringList &files, ChecksumDefinition::ArgumentPassingMethod method) { if (!p) { qCWarning(LIBKLEO_LOG) << functionName << ": process == NULL"; return false; } switch (method) { case ChecksumDefinition::NumArgumentPassingMethods: Q_ASSERT(!"Should not happen"); case ChecksumDefinition::CommandLine: qCDebug(LIBKLEO_LOG) << "Starting: " << cmd << " " << args.join(QLatin1Char(' ')); p->start(cmd, args, QIODevice::ReadOnly); return true; case ChecksumDefinition::NewlineSeparatedInputFile: case ChecksumDefinition::NullSeparatedInputFile: qCDebug(LIBKLEO_LOG) << "Starting: " << cmd << " " << args.join(QLatin1Char(' ')); p->start(cmd, args, QIODevice::ReadWrite); if (!p->waitForStarted()) { return false; } const char sep = method == ChecksumDefinition::NewlineSeparatedInputFile ? '\n' : '\0'; const QByteArray stdin = make_input(files, sep); if (p->write(stdin) != stdin.size()) { return false; } p->closeWriteChannel(); return true; } return false; // make compiler happy } bool ChecksumDefinition::startCreateCommand(QProcess *p, const QStringList &files) const { return start_command(p, Q_FUNC_INFO, doGetCreateCommand(), m_createMethod == CommandLine ? doGetCreateArguments(files) : doGetCreateArguments(QStringList()), files, m_createMethod); } bool ChecksumDefinition::startVerifyCommand(QProcess *p, const QStringList &files) const { return start_command(p, Q_FUNC_INFO, doGetVerifyCommand(), m_verifyMethod == CommandLine ? doGetVerifyArguments(files) : doGetVerifyArguments(QStringList()), files, m_verifyMethod); } // static std::vector> ChecksumDefinition::getChecksumDefinitions() { QStringList errors; return getChecksumDefinitions(errors); } // static std::vector> ChecksumDefinition::getChecksumDefinitions(QStringList &errors) { std::vector> result; KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("libkleopatrarc")); const QStringList groups = config->groupList().filter(QRegularExpression(QStringLiteral("^Checksum Definition #"))); result.reserve(groups.size()); for (const QString &group : groups) { try { const std::shared_ptr ad(new KConfigBasedChecksumDefinition(KConfigGroup(config, group))); result.push_back(ad); } catch (const std::exception &e) { qDebug() << e.what(); errors.push_back(QString::fromLocal8Bit(e.what())); } catch (...) { errors.push_back(i18n("Caught unknown exception in group %1", group)); } } return result; } // static std::shared_ptr ChecksumDefinition::getDefaultChecksumDefinition(const std::vector> &checksumDefinitions) { const KConfigGroup group(KSharedConfig::openConfig(), "ChecksumOperations"); const QString checksumDefinitionId = group.readEntry(CHECKSUM_DEFINITION_ID_ENTRY, QStringLiteral("sha256sum")); if (!checksumDefinitionId.isEmpty()) { for (const std::shared_ptr &cd : checksumDefinitions) { if (cd && cd->id() == checksumDefinitionId) { return cd; } } } if (!checksumDefinitions.empty()) { return checksumDefinitions.front(); } else { return std::shared_ptr(); } } // static void ChecksumDefinition::setDefaultChecksumDefinition(const std::shared_ptr &checksumDefinition) { if (!checksumDefinition) { return; } KConfigGroup group(KSharedConfig::openConfig(), "ChecksumOperations"); group.writeEntry(CHECKSUM_DEFINITION_ID_ENTRY, checksumDefinition->id()); group.sync(); } diff --git a/src/kleo/debug.cpp b/src/kleo/debug.cpp index 049b22359..adc6b2fcd 100644 --- a/src/kleo/debug.cpp +++ b/src/kleo/debug.cpp @@ -1,48 +1,50 @@ /* kleo/debug.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2021, 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "debug.h" #include "keygroup.h" #include "utils/formatting.h" #include using namespace Kleo; QDebug operator<<(QDebug debug, const GpgME::Key &key) { const bool oldSetting = debug.autoInsertSpaces(); debug.nospace() << "GpgME::Key("; if (key.isNull()) { debug << "null"; } else if (key.primaryFingerprint()) { debug << Formatting::summaryLine(key) << ", fpr: " << key.primaryFingerprint(); } else { debug << Formatting::summaryLine(key) << ", id: " << key.keyID(); } debug << ')'; debug.setAutoInsertSpaces(oldSetting); return debug.maybeSpace(); } QDebug operator<<(QDebug debug, const Kleo::KeyGroup &group) { const bool oldSetting = debug.autoInsertSpaces(); if (group.isNull()) { debug << "Null"; } else { debug.nospace() << group.name() << " (id: " << group.id() << ", source: " << group.source() << ", keys: " << group.keys().size() << ", isImmutable: " << group.isImmutable() << ")"; } debug.setAutoInsertSpaces(oldSetting); return debug.maybeSpace(); } diff --git a/src/kleo/defaultkeyfilter.cpp b/src/kleo/defaultkeyfilter.cpp index 530f9425c..6cdf6f25a 100644 --- a/src/kleo/defaultkeyfilter.cpp +++ b/src/kleo/defaultkeyfilter.cpp @@ -1,529 +1,531 @@ /* defaultkeyfilter.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "defaultkeyfilter.h" #include "utils/formatting.h" #include #include using namespace GpgME; using namespace Kleo; static bool is_card_key(const Key &key) { const std::vector sks = key.subkeys(); return std::find_if(sks.begin(), sks.end(), std::mem_fn(&Subkey::isCardKey)) != sks.end(); } class DefaultKeyFilter::Private { public: Private() : mMatchContexts(AnyMatchContext) , mRevoked(DoesNotMatter) , mExpired(DoesNotMatter) , mInvalid(DoesNotMatter) , mDisabled(DoesNotMatter) , mRoot(DoesNotMatter) , mCanEncrypt(DoesNotMatter) , mCanSign(DoesNotMatter) , mCanCertify(DoesNotMatter) , mCanAuthenticate(DoesNotMatter) , mQualified(DoesNotMatter) , mCardKey(DoesNotMatter) , mHasSecret(DoesNotMatter) , mIsOpenPGP(DoesNotMatter) , mWasValidated(DoesNotMatter) , mIsDeVs(DoesNotMatter) , mBad(DoesNotMatter) , mOwnerTrust(LevelDoesNotMatter) , mOwnerTrustReferenceLevel(Key::Unknown) , mValidity(LevelDoesNotMatter) , mValidityReferenceLevel(UserID::Unknown) { } QColor mFgColor, mBgColor; QString mName; QString mIcon; QString mId; MatchContexts mMatchContexts; unsigned int mSpecificity = 0; bool mItalic = false; bool mBold = false; bool mStrikeOut = false; bool mUseFullFont = false; QFont mFont; TriState mRevoked; TriState mExpired; TriState mInvalid; TriState mDisabled; TriState mRoot; TriState mCanEncrypt; TriState mCanSign; TriState mCanCertify; TriState mCanAuthenticate; TriState mQualified; TriState mCardKey; TriState mHasSecret; TriState mIsOpenPGP; TriState mWasValidated; TriState mIsDeVs; TriState mBad; LevelState mOwnerTrust; GpgME::Key::OwnerTrust mOwnerTrustReferenceLevel; LevelState mValidity; GpgME::UserID::Validity mValidityReferenceLevel; }; DefaultKeyFilter::DefaultKeyFilter() : KeyFilter() , d_ptr(new Private()) { } DefaultKeyFilter::~DefaultKeyFilter() { } bool DefaultKeyFilter::matches(const Key &key, MatchContexts contexts) const { if (!(d_ptr->mMatchContexts & contexts)) { return false; } #ifdef MATCH #undef MATCH #endif #define MATCH(member, method) \ do { \ if (member != DoesNotMatter && key.method() != bool(member == Set)) { \ return false; \ } \ } while (false) #define IS_MATCH(what) MATCH(d_ptr->m##what, is##what) #define CAN_MATCH(what) MATCH(d_ptr->mCan##what, can##what) IS_MATCH(Revoked); IS_MATCH(Expired); IS_MATCH(Invalid); IS_MATCH(Disabled); IS_MATCH(Root); CAN_MATCH(Encrypt); CAN_MATCH(Sign); CAN_MATCH(Certify); CAN_MATCH(Authenticate); IS_MATCH(Qualified); if (d_ptr->mCardKey != DoesNotMatter) { if ((d_ptr->mCardKey == Set && !is_card_key(key)) || (d_ptr->mCardKey == NotSet && is_card_key(key))) { return false; } } MATCH(d_ptr->mHasSecret, hasSecret); #undef MATCH if (d_ptr->mIsOpenPGP != DoesNotMatter && bool(key.protocol() == GpgME::OpenPGP) != bool(d_ptr->mIsOpenPGP == Set)) { return false; } if (d_ptr->mWasValidated != DoesNotMatter && bool(key.keyListMode() & GpgME::Validate) != bool(d_ptr->mWasValidated == Set)) { return false; } if (d_ptr->mIsDeVs != DoesNotMatter && bool(Formatting::uidsHaveFullValidity(key) && Formatting::isKeyDeVs(key)) != bool(d_ptr->mIsDeVs == Set)) { return false; } if (d_ptr->mBad != DoesNotMatter && /* This is similar to GPGME::Key::isBad which was introduced in GPGME 1.13.0 */ bool(key.isNull() || key.isRevoked() || key.isExpired() || key.isDisabled() || key.isInvalid()) != bool(d_ptr->mBad == Set)) { return false; } switch (d_ptr->mOwnerTrust) { default: case LevelDoesNotMatter: break; case Is: if (key.ownerTrust() != d_ptr->mOwnerTrustReferenceLevel) { return false; } break; case IsNot: if (key.ownerTrust() == d_ptr->mOwnerTrustReferenceLevel) { return false; } break; case IsAtLeast: if (static_cast(key.ownerTrust()) < static_cast(d_ptr->mOwnerTrustReferenceLevel)) { return false; } break; case IsAtMost: if (static_cast(key.ownerTrust()) > static_cast(d_ptr->mOwnerTrustReferenceLevel)) { return false; } break; } const UserID uid = key.userID(0); switch (d_ptr->mValidity) { default: case LevelDoesNotMatter: break; case Is: if (uid.validity() != d_ptr->mValidityReferenceLevel) { return false; } break; case IsNot: if (uid.validity() == d_ptr->mValidityReferenceLevel) { return false; } break; case IsAtLeast: if (static_cast(uid.validity()) < static_cast(d_ptr->mValidityReferenceLevel)) { return false; } break; case IsAtMost: if (static_cast(uid.validity()) > static_cast(d_ptr->mValidityReferenceLevel)) { return false; } break; } return true; } KeyFilter::FontDescription DefaultKeyFilter::fontDescription() const { if (d_ptr->mUseFullFont) { return FontDescription::create(font(), bold(), italic(), strikeOut()); } else { return FontDescription::create(bold(), italic(), strikeOut()); } } void DefaultKeyFilter::setFgColor(const QColor &value) const { d_ptr->mFgColor = value; } void DefaultKeyFilter::setBgColor(const QColor &value) const { d_ptr->mBgColor = value; } void DefaultKeyFilter::setName(const QString &value) const { d_ptr->mName = value; } void DefaultKeyFilter::setIcon(const QString &value) const { d_ptr->mIcon = value; } void DefaultKeyFilter::setId(const QString &value) const { d_ptr->mId = value; } void DefaultKeyFilter::setMatchContexts(MatchContexts value) const { d_ptr->mMatchContexts = value; } void DefaultKeyFilter::setSpecificity(unsigned int value) const { d_ptr->mSpecificity = value; } void DefaultKeyFilter::setItalic(bool value) const { d_ptr->mItalic = value; } void DefaultKeyFilter::setBold(bool value) const { d_ptr->mBold = value; } void DefaultKeyFilter::setStrikeOut(bool value) const { d_ptr->mStrikeOut = value; } void DefaultKeyFilter::setUseFullFont(bool value) const { d_ptr->mUseFullFont = value; } void DefaultKeyFilter::setFont(const QFont &value) const { d_ptr->mFont = value; } void DefaultKeyFilter::setRevoked(DefaultKeyFilter::TriState value) const { d_ptr->mRevoked = value; } void DefaultKeyFilter::setExpired(DefaultKeyFilter::TriState value) const { d_ptr->mExpired = value; } void DefaultKeyFilter::setInvalid(DefaultKeyFilter::TriState value) const { d_ptr->mInvalid = value; } void DefaultKeyFilter::setDisabled(DefaultKeyFilter::TriState value) const { d_ptr->mDisabled = value; } void DefaultKeyFilter::setRoot(DefaultKeyFilter::TriState value) const { d_ptr->mRoot = value; } void DefaultKeyFilter::setCanEncrypt(DefaultKeyFilter::TriState value) const { d_ptr->mCanEncrypt = value; } void DefaultKeyFilter::setCanSign(DefaultKeyFilter::TriState value) const { d_ptr->mCanSign = value; } void DefaultKeyFilter::setCanCertify(DefaultKeyFilter::TriState value) const { d_ptr->mCanCertify = value; } void DefaultKeyFilter::setCanAuthenticate(DefaultKeyFilter::TriState value) const { d_ptr->mCanAuthenticate = value; } void DefaultKeyFilter::setQualified(DefaultKeyFilter::TriState value) const { d_ptr->mQualified = value; } void DefaultKeyFilter::setCardKey(DefaultKeyFilter::TriState value) const { d_ptr->mCardKey = value; } void DefaultKeyFilter::setHasSecret(DefaultKeyFilter::TriState value) const { d_ptr->mHasSecret = value; } void DefaultKeyFilter::setIsOpenPGP(DefaultKeyFilter::TriState value) const { d_ptr->mIsOpenPGP = value; } void DefaultKeyFilter::setWasValidated(DefaultKeyFilter::TriState value) const { d_ptr->mWasValidated = value; } void DefaultKeyFilter::setOwnerTrust(DefaultKeyFilter::LevelState value) const { d_ptr->mOwnerTrust = value; } void DefaultKeyFilter::setOwnerTrustReferenceLevel(GpgME::Key::OwnerTrust value) const { d_ptr->mOwnerTrustReferenceLevel = value; } void DefaultKeyFilter::setValidity(DefaultKeyFilter::LevelState value) const { d_ptr->mValidity = value; } void DefaultKeyFilter::setValidityReferenceLevel(GpgME::UserID::Validity value) const { d_ptr->mValidityReferenceLevel = value; } void DefaultKeyFilter::setIsDeVs(DefaultKeyFilter::TriState value) const { d_ptr->mIsDeVs = value; } void DefaultKeyFilter::setIsBad(DefaultKeyFilter::TriState value) const { d_ptr->mBad = value; } QColor DefaultKeyFilter::fgColor() const { return d_ptr->mFgColor; } QColor DefaultKeyFilter::bgColor() const { return d_ptr->mBgColor; } QString DefaultKeyFilter::name() const { return d_ptr->mName; } QString DefaultKeyFilter::icon() const { return d_ptr->mIcon; } QString DefaultKeyFilter::id() const { return d_ptr->mId; } QFont DefaultKeyFilter::font() const { return d_ptr->mFont; } KeyFilter::MatchContexts DefaultKeyFilter::availableMatchContexts() const { return d_ptr->mMatchContexts; } unsigned int DefaultKeyFilter::specificity() const { return d_ptr->mSpecificity; } bool DefaultKeyFilter::italic() const { return d_ptr->mItalic; } bool DefaultKeyFilter::bold() const { return d_ptr->mBold; } bool DefaultKeyFilter::strikeOut() const { return d_ptr->mStrikeOut; } bool DefaultKeyFilter::useFullFont() const { return d_ptr->mUseFullFont; } DefaultKeyFilter::TriState DefaultKeyFilter::revoked() const { return d_ptr->mRevoked; } DefaultKeyFilter::TriState DefaultKeyFilter::expired() const { return d_ptr->mExpired; } DefaultKeyFilter::TriState DefaultKeyFilter::invalid() const { return d_ptr->mInvalid; } DefaultKeyFilter::TriState DefaultKeyFilter::disabled() const { return d_ptr->mDisabled; } DefaultKeyFilter::TriState DefaultKeyFilter::root() const { return d_ptr->mRoot; } DefaultKeyFilter::TriState DefaultKeyFilter::canEncrypt() const { return d_ptr->mCanEncrypt; } DefaultKeyFilter::TriState DefaultKeyFilter::canSign() const { return d_ptr->mCanSign; } DefaultKeyFilter::TriState DefaultKeyFilter::canCertify() const { return d_ptr->mCanCertify; } DefaultKeyFilter::TriState DefaultKeyFilter::canAuthenticate() const { return d_ptr->mCanAuthenticate; } DefaultKeyFilter::TriState DefaultKeyFilter::qualified() const { return d_ptr->mQualified; } DefaultKeyFilter::TriState DefaultKeyFilter::cardKey() const { return d_ptr->mCardKey; } DefaultKeyFilter::TriState DefaultKeyFilter::hasSecret() const { return d_ptr->mHasSecret; } DefaultKeyFilter::TriState DefaultKeyFilter::isOpenPGP() const { return d_ptr->mIsOpenPGP; } DefaultKeyFilter::TriState DefaultKeyFilter::wasValidated() const { return d_ptr->mWasValidated; } DefaultKeyFilter::LevelState DefaultKeyFilter::ownerTrust() const { return d_ptr->mOwnerTrust; } GpgME::Key::OwnerTrust DefaultKeyFilter::ownerTrustReferenceLevel() const { return d_ptr->mOwnerTrustReferenceLevel; } DefaultKeyFilter::LevelState DefaultKeyFilter::validity() const { return d_ptr->mValidity; } GpgME::UserID::Validity DefaultKeyFilter::validityReferenceLevel() const { return d_ptr->mValidityReferenceLevel; } DefaultKeyFilter::TriState DefaultKeyFilter::isDeVS() const { return d_ptr->mIsDeVs; } DefaultKeyFilter::TriState DefaultKeyFilter::isBad() const { return d_ptr->mBad; } diff --git a/src/kleo/defaultkeygenerationjob.cpp b/src/kleo/defaultkeygenerationjob.cpp index 5c9a990e7..8365ef75e 100644 --- a/src/kleo/defaultkeygenerationjob.cpp +++ b/src/kleo/defaultkeygenerationjob.cpp @@ -1,121 +1,123 @@ /* This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "defaultkeygenerationjob.h" #include #include #include #include using namespace Kleo; namespace Kleo { class DefaultKeyGenerationJob::DefaultKeyGenerationJobPrivate { public: DefaultKeyGenerationJobPrivate() { } ~DefaultKeyGenerationJobPrivate() { if (job) { job->deleteLater(); } } QString passphrase; QPointer job; }; } DefaultKeyGenerationJob::DefaultKeyGenerationJob(QObject *parent) : Job(parent) , d(new DefaultKeyGenerationJob::DefaultKeyGenerationJobPrivate()) { } DefaultKeyGenerationJob::~DefaultKeyGenerationJob() = default; QString DefaultKeyGenerationJob::auditLogAsHtml() const { return d->job ? d->job->auditLogAsHtml() : QString(); } GpgME::Error DefaultKeyGenerationJob::auditLogError() const { return d->job ? d->job->auditLogError() : GpgME::Error(); } void DefaultKeyGenerationJob::slotCancel() { if (d->job) { d->job->slotCancel(); } } void DefaultKeyGenerationJob::setPassphrase(const QString &passphrase) { // null QString = ask for passphrase // empty QString = empty passphrase // non-empty QString = passphrase d->passphrase = passphrase.isNull() ? QLatin1String("") : passphrase; } namespace { QString passphraseParameter(const QString &passphrase) { if (passphrase.isNull()) { return QStringLiteral("%ask-passphrase"); } else if (passphrase.isEmpty()) { return QStringLiteral("%no-protection"); } else { return QStringLiteral("passphrase: %1").arg(passphrase); }; } } GpgME::Error DefaultKeyGenerationJob::start(const QString &email, const QString &name) { const QString passphrase = passphraseParameter(d->passphrase); const QString args = QStringLiteral( "\n" "key-type: RSA\n" "key-length: 2048\n" "key-usage: sign\n" "subkey-type: RSA\n" "subkey-length: 2048\n" "subkey-usage: encrypt\n" "%1\n" "name-email: %2\n" "name-real: %3\n" "") .arg(passphrase, email, name); d->job = QGpgME::openpgp()->keyGenerationJob(); d->job->installEventFilter(this); connect(d->job.data(), &QGpgME::KeyGenerationJob::result, this, &DefaultKeyGenerationJob::result); connect(d->job.data(), &QGpgME::KeyGenerationJob::done, this, &DefaultKeyGenerationJob::done); connect(d->job.data(), &QGpgME::KeyGenerationJob::done, this, &QObject::deleteLater); return d->job->start(args); } bool DefaultKeyGenerationJob::eventFilter(QObject *watched, QEvent *event) { // Intercept the KeyGenerationJob's deferred delete event. We want the job // to live at least as long as we do so we can delegate calls to it. We will // delete the job manually afterwards. if (watched == d->job && event->type() == QEvent::DeferredDelete) { return true; } return Job::eventFilter(watched, event); } diff --git a/src/kleo/dn.cpp b/src/kleo/dn.cpp index d464c69cd..d151a984f 100644 --- a/src/kleo/dn.cpp +++ b/src/kleo/dn.cpp @@ -1,549 +1,551 @@ /* dn.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker DN parsing: SPDX-FileCopyrightText: 2002 g10 Code GmbH SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "dn.h" #include "oidmap.h" #include #include #ifdef _MSC_VER #include #define strcasecmp _stricmp #endif namespace { static const QStringList defaultOrder = { QStringLiteral("CN"), QStringLiteral("L"), QStringLiteral("_X_"), QStringLiteral("OU"), QStringLiteral("O"), QStringLiteral("C"), }; class DNAttributeOrderStore { DNAttributeOrderStore() : mAttributeOrder{defaultOrder} { } public: static DNAttributeOrderStore *instance() { static DNAttributeOrderStore *self = new DNAttributeOrderStore(); return self; } const QStringList &attributeOrder() const { return mAttributeOrder.empty() ? defaultOrder : mAttributeOrder; } void setAttributeOrder(const QStringList &order) { mAttributeOrder = order; } private: QStringList mAttributeOrder; }; } class Kleo::DN::Private { public: Private() : mRefCount(0) { } Private(const Private &other) : attributes(other.attributes) , reorderedAttributes(other.reorderedAttributes) , mRefCount(0) { } int ref() { return ++mRefCount; } int unref() { if (--mRefCount <= 0) { delete this; return 0; } else { return mRefCount; } } int refCount() const { return mRefCount; } DN::Attribute::List attributes; DN::Attribute::List reorderedAttributes; private: int mRefCount; }; namespace { struct DnPair { char *key; char *value; }; } // copied from CryptPlug and adapted to work on DN::Attribute::List: #define digitp(p) (*(p) >= '0' && *(p) <= '9') #define hexdigitp(a) (digitp(a) || (*(a) >= 'A' && *(a) <= 'F') || (*(a) >= 'a' && *(a) <= 'f')) #define xtoi_1(p) (*(p) <= '9' ? (*(p) - '0') : *(p) <= 'F' ? (*(p) - 'A' + 10) : (*(p) - 'a' + 10)) #define xtoi_2(p) ((xtoi_1(p) * 16) + xtoi_1((p) + 1)) static char *trim_trailing_spaces(char *string) { char *p; char *mark; for (mark = nullptr, p = string; *p; p++) { if (isspace(*p)) { if (!mark) { mark = p; } } else { mark = nullptr; } } if (mark) { *mark = '\0'; } return string; } /* Parse a DN and return an array-ized one. This is not a validating parser and it does not support any old-stylish syntax; gpgme is expected to return only rfc2253 compatible strings. */ static const unsigned char *parse_dn_part(DnPair *array, const unsigned char *string) { const unsigned char *s; const unsigned char *s1; size_t n; char *p; /* parse attributeType */ for (s = string + 1; *s && *s != '='; s++) { ; } if (!*s) { return nullptr; /* error */ } n = s - string; if (!n) { return nullptr; /* empty key */ } p = (char *)malloc(n + 1); memcpy(p, string, n); p[n] = 0; trim_trailing_spaces((char *)p); // map OIDs to their names: if (const char *name = Kleo::attributeNameForOID(p)) { free(p); p = strdup(name); } array->key = p; string = s + 1; if (*string == '#') { /* hexstring */ string++; for (s = string; hexdigitp(s); s++) { s++; } n = s - string; if (!n || (n & 1)) { return nullptr; /* empty or odd number of digits */ } n /= 2; array->value = p = (char *)malloc(n + 1); for (s1 = string; n; s1 += 2, n--) { *p++ = xtoi_2(s1); } *p = 0; } else { /* regular v3 quoted string */ for (n = 0, s = string; *s; s++) { if (*s == '\\') { /* pair */ s++; if (*s == ',' || *s == '=' || *s == '+' || *s == '<' || *s == '>' || *s == '#' || *s == ';' || *s == '\\' || *s == '\"' || *s == ' ') { n++; } else if (hexdigitp(s) && hexdigitp(s + 1)) { s++; n++; } else { return nullptr; /* invalid escape sequence */ } } else if (*s == '\"') { return nullptr; /* invalid encoding */ } else if (*s == ',' || *s == '=' || *s == '+' || *s == '<' || *s == '>' || *s == '#' || *s == ';') { break; } else { n++; } } array->value = p = (char *)malloc(n + 1); for (s = string; n; s++, n--) { if (*s == '\\') { s++; if (hexdigitp(s)) { *p++ = xtoi_2(s); s++; } else { *p++ = *s; } } else { *p++ = *s; } } *p = 0; } return s; } /* Parse a DN and return an array-ized one. This is not a validating parser and it does not support any old-stylish syntax; gpgme is expected to return only rfc2253 compatible strings. */ static Kleo::DN::Attribute::List parse_dn(const unsigned char *string) { if (!string) { return QVector(); } QVector result; while (*string) { while (*string == ' ') { string++; } if (!*string) { break; /* ready */ } DnPair pair = {nullptr, nullptr}; string = parse_dn_part(&pair, string); if (!string) { goto failure; } if (pair.key && pair.value) { result.push_back(Kleo::DN::Attribute(QString::fromUtf8(pair.key), QString::fromUtf8(pair.value))); } free(pair.key); free(pair.value); while (*string == ' ') { string++; } if (*string && *string != ',' && *string != ';' && *string != '+') { goto failure; /* invalid delimiter */ } if (*string) { string++; } } return result; failure: return QVector(); } static QVector parse_dn(const QString &dn) { return parse_dn((const unsigned char *)dn.toUtf8().data()); } static QString dn_escape(const QString &s) { QString result; for (int i = 0, end = s.length(); i != end; ++i) { const QChar ch = s[i]; switch (ch.unicode()) { case ',': case '+': case '"': case '\\': case '<': case '>': case ';': result += QLatin1Char('\\'); // fall through Q_FALLTHROUGH(); default: result += ch; } } return result; } static QString serialise(const QVector &dn, const QString &sep) { QStringList result; for (QVector::const_iterator it = dn.begin(); it != dn.end(); ++it) { if (!(*it).name().isEmpty() && !(*it).value().isEmpty()) { result.push_back((*it).name().trimmed() + QLatin1Char('=') + dn_escape((*it).value().trimmed())); } } return result.join(sep); } static Kleo::DN::Attribute::List reorder_dn(const Kleo::DN::Attribute::List &dn) { const QStringList &attrOrder = Kleo::DN::attributeOrder(); Kleo::DN::Attribute::List unknownEntries; Kleo::DN::Attribute::List result; unknownEntries.reserve(dn.size()); result.reserve(dn.size()); // find all unknown entries in their order of appearance for (Kleo::DN::const_iterator it = dn.begin(); it != dn.end(); ++it) { if (!attrOrder.contains((*it).name())) { unknownEntries.push_back(*it); } } // process the known attrs in the desired order for (QStringList::const_iterator oit = attrOrder.begin(); oit != attrOrder.end(); ++oit) { if (*oit == QLatin1String("_X_")) { // insert the unknown attrs std::copy(unknownEntries.begin(), unknownEntries.end(), std::back_inserter(result)); unknownEntries.clear(); // don't produce dup's } else { for (Kleo::DN::const_iterator dnit = dn.begin(); dnit != dn.end(); ++dnit) { if ((*dnit).name() == *oit) { result.push_back(*dnit); } } } } return result; } // // // class DN // // Kleo::DN::DN() { d = new Private(); d->ref(); } Kleo::DN::DN(const QString &dn) { d = new Private(); d->ref(); d->attributes = parse_dn(dn); } Kleo::DN::DN(const char *utf8DN) { d = new Private(); d->ref(); if (utf8DN) { d->attributes = parse_dn((const unsigned char *)utf8DN); } } Kleo::DN::DN(const DN &other) : d(other.d) { if (d) { d->ref(); } } Kleo::DN::~DN() { if (d) { d->unref(); } } const Kleo::DN &Kleo::DN::operator=(const DN &that) { if (this->d == that.d) { return *this; } if (that.d) { that.d->ref(); } if (this->d) { this->d->unref(); } this->d = that.d; return *this; } // static QStringList Kleo::DN::attributeOrder() { return DNAttributeOrderStore::instance()->attributeOrder(); } // static void Kleo::DN::setAttributeOrder(const QStringList &order) { DNAttributeOrderStore::instance()->setAttributeOrder(order); } // static QStringList Kleo::DN::defaultAttributeOrder() { return defaultOrder; } QString Kleo::DN::prettyDN() const { if (!d) { return QString(); } if (d->reorderedAttributes.empty()) { d->reorderedAttributes = reorder_dn(d->attributes); } return serialise(d->reorderedAttributes, QStringLiteral(",")); } QString Kleo::DN::dn() const { return d ? serialise(d->attributes, QStringLiteral(",")) : QString(); } QString Kleo::DN::dn(const QString &sep) const { return d ? serialise(d->attributes, sep) : QString(); } // static QString Kleo::DN::escape(const QString &value) { return dn_escape(value); } void Kleo::DN::detach() { if (!d) { d = new Kleo::DN::Private(); d->ref(); } else if (d->refCount() > 1) { Kleo::DN::Private *d_save = d; d = new Kleo::DN::Private(*d); d->ref(); d_save->unref(); } } void Kleo::DN::append(const Attribute &attr) { detach(); d->attributes.push_back(attr); d->reorderedAttributes.clear(); } QString Kleo::DN::operator[](const QString &attr) const { if (!d) { return QString(); } const QString attrUpper = attr.toUpper(); for (QVector::const_iterator it = d->attributes.constBegin(); it != d->attributes.constEnd(); ++it) { if ((*it).name() == attrUpper) { return (*it).value(); } } return QString(); } static QVector empty; Kleo::DN::const_iterator Kleo::DN::begin() const { return d ? d->attributes.constBegin() : empty.constBegin(); } Kleo::DN::const_iterator Kleo::DN::end() const { return d ? d->attributes.constEnd() : empty.constEnd(); } ///////////////////// namespace { static const QMap attributeNamesAndLabels = { // clang-format off {QStringLiteral("CN"), kli18n("Common name") }, {QStringLiteral("SN"), kli18n("Surname") }, {QStringLiteral("GN"), kli18n("Given name") }, {QStringLiteral("L"), kli18n("Location") }, {QStringLiteral("T"), kli18n("Title") }, {QStringLiteral("OU"), kli18n("Organizational unit")}, {QStringLiteral("O"), kli18n("Organization") }, {QStringLiteral("PC"), kli18n("Postal code") }, {QStringLiteral("C"), kli18n("Country code") }, {QStringLiteral("SP"), kli18n("State or province") }, {QStringLiteral("DC"), kli18n("Domain component") }, {QStringLiteral("BC"), kli18n("Business category") }, {QStringLiteral("EMAIL"), kli18n("Email address") }, {QStringLiteral("MAIL"), kli18n("Mail address") }, {QStringLiteral("MOBILE"), kli18n("Mobile phone number")}, {QStringLiteral("TEL"), kli18n("Telephone number") }, {QStringLiteral("FAX"), kli18n("Fax number") }, {QStringLiteral("STREET"), kli18n("Street address") }, {QStringLiteral("UID"), kli18n("Unique ID") }, // clang-format on }; } // static QStringList Kleo::DN::attributeNames() { return attributeNamesAndLabels.keys(); } // static QString Kleo::DN::attributeNameToLabel(const QString &name) { return attributeNamesAndLabels.value(name.trimmed().toUpper()).toString(); } diff --git a/src/kleo/docaction.cpp b/src/kleo/docaction.cpp index 4257ea909..caab95ee0 100644 --- a/src/kleo/docaction.cpp +++ b/src/kleo/docaction.cpp @@ -1,60 +1,63 @@ /* kleo/docaction.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Andre Heinecke SPDX-License-Identifier: GPL-2.0-or-later */ + +#include + #include "docaction.h" #include #include #include #include #include #include #include using namespace Kleo; class Kleo::DocAction::Private { public: explicit Private(const QString &filename, const QString &pathHint); ~Private() = default; QString path; bool isEnabled = false; }; DocAction::Private::Private(const QString &filename, const QString &pathHint) { QString tmp = pathHint; if (!tmp.startsWith(QLatin1Char('/'))) { tmp.prepend(QLatin1Char('/')); } QDir datadir(QCoreApplication::applicationDirPath() + (pathHint.isNull() ? QStringLiteral("/../share/kleopatra") : tmp)); path = datadir.filePath(filename); QFileInfo fi(path); isEnabled = fi.exists(); } DocAction::DocAction(const QIcon &icon, const QString &text, const QString &filename, const QString &pathHint, QObject *parent) : QAction(icon, text, parent) , d(new Private(filename, pathHint)) { setVisible(d->isEnabled); setEnabled(d->isEnabled); connect(this, &QAction::triggered, this, [this]() { if (d->isEnabled) { qCDebug(LIBKLEO_LOG) << "Opening file:" << d->path; QDesktopServices::openUrl(QUrl::fromLocalFile(d->path)); } }); } DocAction::~DocAction() = default; diff --git a/src/kleo/enum.cpp b/src/kleo/enum.cpp index 2c03d4200..c616dc477 100644 --- a/src/kleo/enum.cpp +++ b/src/kleo/enum.cpp @@ -1,308 +1,310 @@ /* kleo/enum.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "enum.h" #include "models/keycache.h" #include #include #include #include #include #include #include #include static const struct { Kleo::CryptoMessageFormat format; const KLazyLocalizedString displayName; const char *configName; } cryptoMessageFormats[] = { // clang-format off {Kleo::InlineOpenPGPFormat, kli18n("Inline OpenPGP (deprecated)"), "inline openpgp"}, {Kleo::OpenPGPMIMEFormat, kli18n("OpenPGP/MIME"), "openpgp/mime" }, {Kleo::SMIMEFormat, kli18n("S/MIME"), "s/mime" }, {Kleo::SMIMEOpaqueFormat, kli18n("S/MIME Opaque"), "s/mime opaque" }, {Kleo::AnySMIME, kli18n("Any S/MIME"), "any s/mime" }, {Kleo::AnyOpenPGP, kli18n("Any OpenPGP"), "any openpgp" }, // clang-format on }; static const unsigned int numCryptoMessageFormats = sizeof cryptoMessageFormats / sizeof *cryptoMessageFormats; const char *Kleo::cryptoMessageFormatToString(Kleo::CryptoMessageFormat f) { if (f == AutoFormat) { return "auto"; } for (unsigned int i = 0; i < numCryptoMessageFormats; ++i) { if (f == cryptoMessageFormats[i].format) { return cryptoMessageFormats[i].configName; } } return nullptr; } QStringList Kleo::cryptoMessageFormatsToStringList(unsigned int f) { QStringList result; for (unsigned int i = 0; i < numCryptoMessageFormats; ++i) { if (f & cryptoMessageFormats[i].format) { result.push_back(QLatin1String(cryptoMessageFormats[i].configName)); } } return result; } QString Kleo::cryptoMessageFormatToLabel(Kleo::CryptoMessageFormat f) { if (f == AutoFormat) { return i18n("Any"); } for (unsigned int i = 0; i < numCryptoMessageFormats; ++i) { if (f == cryptoMessageFormats[i].format) { return KLocalizedString(cryptoMessageFormats[i].displayName).toString(); } } return QString(); } Kleo::CryptoMessageFormat Kleo::stringToCryptoMessageFormat(const QString &s) { const QString t = s.toLower(); for (unsigned int i = 0; i < numCryptoMessageFormats; ++i) { if (t == QLatin1String(cryptoMessageFormats[i].configName)) { return cryptoMessageFormats[i].format; } } return AutoFormat; } unsigned int Kleo::stringListToCryptoMessageFormats(const QStringList &sl) { unsigned int result = 0; for (QStringList::const_iterator it = sl.begin(); it != sl.end(); ++it) { result |= stringToCryptoMessageFormat(*it); } return result; } // For the config values used below, see also kaddressbook/editors/cryptowidget.cpp const char *Kleo::encryptionPreferenceToString(EncryptionPreference pref) { switch (pref) { case UnknownPreference: return nullptr; case NeverEncrypt: return "never"; case AlwaysEncrypt: return "always"; case AlwaysEncryptIfPossible: return "alwaysIfPossible"; case AlwaysAskForEncryption: return "askAlways"; case AskWheneverPossible: return "askWhenPossible"; } return nullptr; // keep the compiler happy } Kleo::EncryptionPreference Kleo::stringToEncryptionPreference(const QString &str) { if (str == QLatin1String("never")) { return NeverEncrypt; } if (str == QLatin1String("always")) { return AlwaysEncrypt; } if (str == QLatin1String("alwaysIfPossible")) { return AlwaysEncryptIfPossible; } if (str == QLatin1String("askAlways")) { return AlwaysAskForEncryption; } if (str == QLatin1String("askWhenPossible")) { return AskWheneverPossible; } return UnknownPreference; } QString Kleo::encryptionPreferenceToLabel(EncryptionPreference pref) { switch (pref) { case NeverEncrypt: return i18n("Never Encrypt"); case AlwaysEncrypt: return i18n("Always Encrypt"); case AlwaysEncryptIfPossible: return i18n("Always Encrypt If Possible"); case AlwaysAskForEncryption: return i18n("Ask"); case AskWheneverPossible: return i18n("Ask Whenever Possible"); default: return xi18nc("no specific preference", "none"); } } const char *Kleo::signingPreferenceToString(SigningPreference pref) { switch (pref) { case UnknownSigningPreference: return nullptr; case NeverSign: return "never"; case AlwaysSign: return "always"; case AlwaysSignIfPossible: return "alwaysIfPossible"; case AlwaysAskForSigning: return "askAlways"; case AskSigningWheneverPossible: return "askWhenPossible"; } return nullptr; // keep the compiler happy } Kleo::SigningPreference Kleo::stringToSigningPreference(const QString &str) { if (str == QLatin1String("never")) { return NeverSign; } if (str == QLatin1String("always")) { return AlwaysSign; } if (str == QLatin1String("alwaysIfPossible")) { return AlwaysSignIfPossible; } if (str == QLatin1String("askAlways")) { return AlwaysAskForSigning; } if (str == QLatin1String("askWhenPossible")) { return AskSigningWheneverPossible; } return UnknownSigningPreference; } QString Kleo::signingPreferenceToLabel(SigningPreference pref) { switch (pref) { case NeverSign: return i18n("Never Sign"); case AlwaysSign: return i18n("Always Sign"); case AlwaysSignIfPossible: return i18n("Always Sign If Possible"); case AlwaysAskForSigning: return i18n("Ask"); case AskSigningWheneverPossible: return i18n("Ask Whenever Possible"); default: return i18nc("no specific preference", ""); } } Kleo::TrustLevel Kleo::trustLevel(const GpgME::Key &key) { TrustLevel maxTl = Level0; for (int i = 0, c = key.numUserIDs(); i < c; ++i) { const auto tl = trustLevel(key.userID(i)); maxTl = qMax(maxTl, tl); if (maxTl == Level4) { break; } } return maxTl; } namespace { bool hasTrustedSignature(const GpgME::UserID &uid) { // lazily initialized cache static std::shared_ptr keyCache; if (!keyCache) { keyCache = Kleo::KeyCache::instance(); } if (!keyCache->initialized()) { QEventLoop el; QObject::connect(keyCache.get(), &Kleo::KeyCache::keyListingDone, &el, &QEventLoop::quit); el.exec(); } const auto signatures = uid.signatures(); std::vector sigKeyIDs; std::transform(signatures.cbegin(), signatures.cend(), std::back_inserter(sigKeyIDs), std::bind(&GpgME::UserID::Signature::signerKeyID, std::placeholders::_1)); const auto keys = keyCache->findByKeyIDOrFingerprint(sigKeyIDs); return std::any_of(keys.cbegin(), keys.cend(), [](const GpgME::Key &key) { return key.ownerTrust() == GpgME::Key::Ultimate; }); } } Kleo::TrustLevel Kleo::trustLevel(const GpgME::UserID &uid) { // Modelled after https://wiki.gnupg.org/EasyGpg2016/AutomatedEncryption, // but modified to cover all cases, unlike the pseudocode in the document. // // TODO: Check whether the key comes from a trusted source (Cert/PKA/DANE/WKD) switch (uid.validity()) { case GpgME::UserID::Unknown: case GpgME::UserID::Undefined: case GpgME::UserID::Never: // Not enough trust -> level 0 return Level0; case GpgME::UserID::Marginal: // Marginal trust without TOFU data means the key is still trusted // through the Web of Trust -> level 2 if (uid.tofuInfo().isNull()) { return Level2; } // Marginal trust with TOFU, level will depend on TOFU history switch (uid.tofuInfo().validity()) { case GpgME::TofuInfo::ValidityUnknown: case GpgME::TofuInfo::Conflict: case GpgME::TofuInfo::NoHistory: // Marginal trust, but not enough history -> level 0 return Level0; case GpgME::TofuInfo::LittleHistory: // Marginal trust, but too little history -> level 1 return Level1; case GpgME::TofuInfo::BasicHistory: case GpgME::TofuInfo::LargeHistory: // Marginal trust and enough history -> level 2 return Level2; } return Level2; // Not reached, but avoids fallthrough warnings case GpgME::UserID::Full: // Full trust, trust level depends whether the UserID is signed with // at least one key with Ultimate ownertrust. return hasTrustedSignature(uid) ? Level4 : Level3; case GpgME::UserID::Ultimate: // Ultimate trust -> leve 4 return Level4; } Q_UNREACHABLE(); } diff --git a/src/kleo/kconfigbasedkeyfilter.cpp b/src/kleo/kconfigbasedkeyfilter.cpp index 3a829d8dd..8d4d4aa98 100644 --- a/src/kleo/kconfigbasedkeyfilter.cpp +++ b/src/kleo/kconfigbasedkeyfilter.cpp @@ -1,237 +1,239 @@ /* kconfigbasedkeyfilter.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "kconfigbasedkeyfilter.h" #include #include #include #include #include using namespace Kleo; using namespace GpgME; // // // FontDescription - intuitive font property resolving // (QFont::resolve doesn't work for us) // // struct KeyFilter::FontDescription::Private { bool bold, italic, strikeOut, fullFont; QFont font; }; KeyFilter::FontDescription::FontDescription() : d(new Private) { d->bold = d->italic = d->strikeOut = d->fullFont = false; } KeyFilter::FontDescription::FontDescription(const FontDescription &other) : d(new Private(*other.d)) { } KeyFilter::FontDescription::~FontDescription() = default; KeyFilter::FontDescription KeyFilter::FontDescription::create(bool b, bool i, bool s) { FontDescription fd; fd.d->bold = b; fd.d->italic = i; fd.d->strikeOut = s; return fd; } KeyFilter::FontDescription KeyFilter::FontDescription::create(const QFont &f, bool b, bool i, bool s) { FontDescription fd; fd.d->fullFont = true; fd.d->font = f; fd.d->bold = b; fd.d->italic = i; fd.d->strikeOut = s; return fd; } QFont KeyFilter::FontDescription::font(const QFont &base) const { QFont font; if (d->fullFont) { font = d->font; font.setPointSize(base.pointSize()); } else { font = base; } if (d->bold) { font.setBold(true); } if (d->italic) { font.setItalic(true); } if (d->strikeOut) { font.setStrikeOut(true); } return font; } KeyFilter::FontDescription KeyFilter::FontDescription::resolve(const FontDescription &other) const { FontDescription fd; fd.d->fullFont = this->d->fullFont || other.d->fullFont; if (fd.d->fullFont) { fd.d->font = this->d->fullFont ? this->d->font : other.d->font; } fd.d->bold = this->d->bold || other.d->bold; fd.d->italic = this->d->italic || other.d->italic; fd.d->strikeOut = this->d->strikeOut || other.d->strikeOut; return fd; } static const struct { const char *name; Key::OwnerTrust trust; UserID::Validity validity; } ownerTrustAndValidityMap[] = { // clang-format off {"unknown", Key::Unknown, UserID::Unknown }, {"undefined", Key::Undefined, UserID::Undefined}, {"never", Key::Never, UserID::Never }, {"marginal", Key::Marginal, UserID::Marginal }, {"full", Key::Full, UserID::Full }, {"ultimate", Key::Ultimate, UserID::Ultimate }, // clang-format on }; static Key::OwnerTrust map2OwnerTrust(const QString &s) { for (unsigned int i = 0; i < sizeof ownerTrustAndValidityMap / sizeof *ownerTrustAndValidityMap; ++i) { if (s.toLower() == QLatin1String(ownerTrustAndValidityMap[i].name)) { return ownerTrustAndValidityMap[i].trust; } } return ownerTrustAndValidityMap[0].trust; } static UserID::Validity map2Validity(const QString &s) { for (unsigned int i = 0; i < sizeof ownerTrustAndValidityMap / sizeof *ownerTrustAndValidityMap; ++i) { if (s.toLower() == QLatin1String(ownerTrustAndValidityMap[i].name)) { return ownerTrustAndValidityMap[i].validity; } } return ownerTrustAndValidityMap[0].validity; } KConfigBasedKeyFilter::KConfigBasedKeyFilter(const KConfigGroup &config) : DefaultKeyFilter() { setFgColor(config.readEntry("foreground-color", QColor())); setBgColor(config.readEntry("background-color", QColor())); setName(config.readEntry("Name", config.name())); setIcon(config.readEntry("icon")); setId(config.readEntry("id", config.name())); if (config.hasKey("font")) { setUseFullFont(true); setFont(config.readEntry("font")); } else { setUseFullFont(false); setItalic(config.readEntry("font-italic", false)); setBold(config.readEntry("font-bold", false)); } setStrikeOut(config.readEntry("font-strikeout", false)); #ifdef SET #undef SET #endif #define SET(member, key) \ if (config.hasKey(key)) { \ set##member(config.readEntry(key, false) ? Set : NotSet); \ setSpecificity(specificity() + 1); \ } SET(Revoked, "is-revoked"); SET(Expired, "is-expired"); SET(Disabled, "is-disabled"); SET(Root, "is-root-certificate"); SET(CanEncrypt, "can-encrypt"); SET(CanSign, "can-sign"); SET(CanCertify, "can-certify"); SET(CanAuthenticate, "can-authenticate"); SET(Qualified, "is-qualified"); SET(CardKey, "is-cardkey"); SET(HasSecret, "has-secret-key"); SET(IsOpenPGP, "is-openpgp-key"); SET(WasValidated, "was-validated"); SET(IsDeVs, "is-de-vs"); #undef SET static const struct { const char *prefix; LevelState state; } prefixMap[] = { {"is-", Is}, {"is-not-", IsNot}, {"is-at-least-", IsAtLeast}, {"is-at-most-", IsAtMost}, }; for (unsigned int i = 0; i < sizeof prefixMap / sizeof *prefixMap; ++i) { const QString key = QLatin1String(prefixMap[i].prefix) + QLatin1String("ownertrust"); if (config.hasKey(key)) { setOwnerTrust(prefixMap[i].state); setOwnerTrustReferenceLevel(map2OwnerTrust(config.readEntry(key, QString()))); setSpecificity(specificity() + 1); break; } } for (unsigned int i = 0; i < sizeof prefixMap / sizeof *prefixMap; ++i) { const QString key = QLatin1String(prefixMap[i].prefix) + QLatin1String("validity"); if (config.hasKey(key)) { setValidity(prefixMap[i].state); setValidityReferenceLevel(map2Validity(config.readEntry(key, QString()))); setSpecificity(specificity() + 1); break; } } static const struct { const char *key; MatchContext context; } matchMap[] = { {"any", AnyMatchContext}, {"appearance", Appearance}, {"filtering", Filtering}, }; static const QRegularExpression reg(QRegularExpression(QLatin1String("[^a-zA-Z0-9_-!]+"))); const QStringList contexts = config.readEntry("match-contexts", "any").toLower().split(reg, Qt::SkipEmptyParts); setMatchContexts(NoMatchContext); for (const QString &ctx : contexts) { bool found = false; for (unsigned int i = 0; i < sizeof matchMap / sizeof *matchMap; ++i) { if (ctx == QLatin1String(matchMap[i].key)) { setMatchContexts(availableMatchContexts() |= matchMap[i].context); found = true; break; } else if (ctx.startsWith(QLatin1Char('!')) && ctx.mid(1) == QLatin1String(matchMap[i].key)) { setMatchContexts(availableMatchContexts() &= matchMap[i].context); found = true; break; } } if (!found) { qWarning() << QStringLiteral("KConfigBasedKeyFilter: found unknown match context '%1' in group '%2'").arg(ctx, config.name()); } } if (availableMatchContexts() == NoMatchContext) { qWarning() << QStringLiteral( "KConfigBasedKeyFilter: match context in group '%1' evaluates to NoMatchContext, " "replaced by AnyMatchContext") .arg(config.name()); setMatchContexts(AnyMatchContext); } } diff --git a/src/kleo/keyfiltermanager.cpp b/src/kleo/keyfiltermanager.cpp index 1d1e03b0f..d9298771f 100644 --- a/src/kleo/keyfiltermanager.cpp +++ b/src/kleo/keyfiltermanager.cpp @@ -1,456 +1,458 @@ /* keyfiltermanager.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "keyfiltermanager.h" #include "defaultkeyfilter.h" #include "kconfigbasedkeyfilter.h" #include "stl_util.h" #include "utils/algorithm.h" #include "utils/formatting.h" #include "utils/gnupg.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace GpgME; namespace { class Model : public QAbstractListModel { KeyFilterManager::Private *m_keyFilterManagerPrivate; public: explicit Model(KeyFilterManager::Private *p) : QAbstractListModel(nullptr) , m_keyFilterManagerPrivate(p) { } int rowCount(const QModelIndex &) const override; QVariant data(const QModelIndex &idx, int role) const override; /* upgrade to public */ using QAbstractListModel::beginResetModel; /* upgrade to public */ using QAbstractListModel::endResetModel; }; class AllCertificatesKeyFilter : public DefaultKeyFilter { public: AllCertificatesKeyFilter() : DefaultKeyFilter() { setSpecificity(UINT_MAX); // overly high for ordering setName(i18n("All Certificates")); setId(QStringLiteral("all-certificates")); setMatchContexts(Filtering); } }; class MyCertificatesKeyFilter : public DefaultKeyFilter { public: MyCertificatesKeyFilter() : DefaultKeyFilter() { setHasSecret(Set); setSpecificity(UINT_MAX - 1); // overly high for ordering setName(i18n("My Certificates")); setId(QStringLiteral("my-certificates")); setMatchContexts(AnyMatchContext); setBold(true); } }; class TrustedCertificatesKeyFilter : public DefaultKeyFilter { public: TrustedCertificatesKeyFilter() : DefaultKeyFilter() { setRevoked(NotSet); setValidity(IsAtLeast); setValidityReferenceLevel(UserID::Marginal); setSpecificity(UINT_MAX - 2); // overly high for ordering setName(i18n("Trusted Certificates")); setId(QStringLiteral("trusted-certificates")); setMatchContexts(Filtering); } }; class FullCertificatesKeyFilter : public DefaultKeyFilter { public: FullCertificatesKeyFilter() : DefaultKeyFilter() { setRevoked(NotSet); setValidity(IsAtLeast); setValidityReferenceLevel(UserID::Full); setSpecificity(UINT_MAX - 3); setName(i18n("Fully Trusted Certificates")); setId(QStringLiteral("full-certificates")); setMatchContexts(Filtering); } }; class OtherCertificatesKeyFilter : public DefaultKeyFilter { public: OtherCertificatesKeyFilter() : DefaultKeyFilter() { setHasSecret(NotSet); setValidity(IsAtMost); setValidityReferenceLevel(UserID::Never); setSpecificity(UINT_MAX - 4); // overly high for ordering setName(i18n("Other Certificates")); setId(QStringLiteral("other-certificates")); setMatchContexts(Filtering); } }; /* This filter selects uncertified OpenPGP keys, i.e. "good" OpenPGP keys with * unrevoked user IDs that are not fully valid. */ class UncertifiedOpenPGPKeysFilter : public DefaultKeyFilter { public: UncertifiedOpenPGPKeysFilter() : DefaultKeyFilter() { setSpecificity(UINT_MAX - 6); // overly high for ordering setName(i18n("Not Certified Certificates")); setId(QStringLiteral("not-certified-certificates")); setMatchContexts(Filtering); setIsOpenPGP(Set); setIsBad(NotSet); } bool matches(const Key &key, MatchContexts contexts) const override { return DefaultKeyFilter::matches(key, contexts) && !Formatting::uidsHaveFullValidity(key); } }; /* This filter selects only invalid keys (i.e. those where not all * UIDs are at least fully valid). */ class KeyNotValidFilter : public DefaultKeyFilter { public: KeyNotValidFilter() : DefaultKeyFilter() { setSpecificity(UINT_MAX - 7); // overly high for ordering setName(i18n("Not Validated Certificates")); setId(QStringLiteral("not-validated-certificates")); setMatchContexts(Filtering); } bool matches(const Key &key, MatchContexts contexts) const override { return DefaultKeyFilter::matches(key, contexts) && !Formatting::uidsHaveFullValidity(key); } }; } static std::vector> defaultFilters() { std::vector> result; result.reserve(6); result.push_back(std::shared_ptr(new MyCertificatesKeyFilter)); result.push_back(std::shared_ptr(new TrustedCertificatesKeyFilter)); result.push_back(std::shared_ptr(new FullCertificatesKeyFilter)); result.push_back(std::shared_ptr(new OtherCertificatesKeyFilter)); result.push_back(std::shared_ptr(new AllCertificatesKeyFilter)); result.push_back(std::shared_ptr(new UncertifiedOpenPGPKeysFilter)); result.push_back(std::shared_ptr(new KeyNotValidFilter)); return result; } class KeyFilterManager::Private { public: Private() : filters() , model(this) { } void clear() { model.beginResetModel(); filters.clear(); model.endResetModel(); } std::vector> filters; Model model; GpgME::Protocol protocol = GpgME::UnknownProtocol; }; KeyFilterManager *KeyFilterManager::mSelf = nullptr; KeyFilterManager::KeyFilterManager(QObject *parent) : QObject(parent) , d(new Private) { mSelf = this; // ### DF: doesn't a KStaticDeleter work more reliably? if (QCoreApplication *app = QCoreApplication::instance()) { connect(app, &QCoreApplication::aboutToQuit, this, &QObject::deleteLater); } reload(); } KeyFilterManager::~KeyFilterManager() { mSelf = nullptr; if (d) { d->clear(); } } KeyFilterManager *KeyFilterManager::instance() { if (!mSelf) { mSelf = new KeyFilterManager(); } return mSelf; } void KeyFilterManager::alwaysFilterByProtocol(GpgME::Protocol protocol) { if (protocol != d->protocol) { d->protocol = protocol; reload(); } } const std::shared_ptr &KeyFilterManager::filterMatching(const Key &key, KeyFilter::MatchContexts contexts) const { const auto it = std::find_if(d->filters.cbegin(), d->filters.cend(), [&key, contexts](const std::shared_ptr &filter) { return filter->matches(key, contexts); }); if (it != d->filters.cend()) { return *it; } static const std::shared_ptr null; return null; } std::vector> KeyFilterManager::filtersMatching(const Key &key, KeyFilter::MatchContexts contexts) const { std::vector> result; result.reserve(d->filters.size()); std::remove_copy_if(d->filters.begin(), d->filters.end(), std::back_inserter(result), [&key, contexts](const std::shared_ptr &filter) { return !filter->matches(key, contexts); }); return result; } namespace { static const auto byDecreasingSpecificity = [](const std::shared_ptr &lhs, const std::shared_ptr &rhs) { return lhs->specificity() > rhs->specificity(); }; } void KeyFilterManager::reload() { d->clear(); d->filters = defaultFilters(); KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("libkleopatrarc")); const QStringList groups = config->groupList().filter(QRegularExpression(QStringLiteral("^Key Filter #\\d+$"))); const bool ignoreDeVs = !Kleo::gnupgIsDeVsCompliant(); for (QStringList::const_iterator it = groups.begin(); it != groups.end(); ++it) { const KConfigGroup cfg(config, *it); if (cfg.hasKey("is-de-vs") && ignoreDeVs) { /* Don't show de-vs filters in other compliance modes */ continue; } d->filters.push_back(std::shared_ptr(new KConfigBasedKeyFilter(cfg))); } std::stable_sort(d->filters.begin(), d->filters.end(), byDecreasingSpecificity); if (d->protocol != GpgME::UnknownProtocol) { // remove filters with conflicting isOpenPGP rule const auto conflictingValue = (d->protocol == GpgME::OpenPGP) ? DefaultKeyFilter::NotSet : DefaultKeyFilter::Set; Kleo::erase_if(d->filters, [conflictingValue](const auto &f) { const auto filter = std::dynamic_pointer_cast(f); Q_ASSERT(filter); return filter->isOpenPGP() == conflictingValue; }); // add isOpenPGP rule to all filters const auto isOpenPGPValue = (d->protocol == GpgME::OpenPGP) ? DefaultKeyFilter::Set : DefaultKeyFilter::NotSet; std::for_each(std::begin(d->filters), std::end(d->filters), [isOpenPGPValue](auto &f) { const auto filter = std::dynamic_pointer_cast(f); Q_ASSERT(filter); return filter->setIsOpenPGP(isOpenPGPValue); }); } qCDebug(LIBKLEO_LOG) << "KeyFilterManager::" << __func__ << "final filter count is" << d->filters.size(); } QAbstractItemModel *KeyFilterManager::model() const { return &d->model; } const std::shared_ptr &KeyFilterManager::keyFilterByID(const QString &id) const { const auto it = std::find_if(d->filters.begin(), d->filters.end(), [id](const std::shared_ptr &filter) { return filter->id() == id; }); if (it != d->filters.end()) { return *it; } static const std::shared_ptr null; return null; } const std::shared_ptr &KeyFilterManager::fromModelIndex(const QModelIndex &idx) const { if (!idx.isValid() || idx.model() != &d->model || idx.row() < 0 || static_cast(idx.row()) >= d->filters.size()) { static const std::shared_ptr null; return null; } return d->filters[idx.row()]; } QModelIndex KeyFilterManager::toModelIndex(const std::shared_ptr &kf) const { if (!kf) { return {}; } const auto pair = std::equal_range(d->filters.cbegin(), d->filters.cend(), kf, byDecreasingSpecificity); const auto it = std::find(pair.first, pair.second, kf); if (it != pair.second) { return d->model.index(it - d->filters.begin()); } else { return QModelIndex(); } } int Model::rowCount(const QModelIndex &) const { return m_keyFilterManagerPrivate->filters.size(); } QVariant Model::data(const QModelIndex &idx, int role) const { if (!idx.isValid() || idx.model() != this || idx.row() < 0 || static_cast(idx.row()) > m_keyFilterManagerPrivate->filters.size()) { return QVariant(); } const auto filter = m_keyFilterManagerPrivate->filters[idx.row()]; switch (role) { case Qt::DecorationRole: return filter->icon(); case Qt::DisplayRole: case Qt::EditRole: case Qt::ToolTipRole: /* Most useless tooltip ever. */ return filter->name(); case KeyFilterManager::FilterIdRole: return filter->id(); case KeyFilterManager::FilterMatchContextsRole: return QVariant::fromValue(filter->availableMatchContexts()); default: return QVariant(); } } static KeyFilter::FontDescription get_fontdescription(const std::vector> &filters, const Key &key, const KeyFilter::FontDescription &initial) { return kdtools::accumulate_if( filters.begin(), filters.end(), [&key](const std::shared_ptr &filter) { return filter->matches(key, KeyFilter::Appearance); }, initial, [](const KeyFilter::FontDescription &lhs, const std::shared_ptr &rhs) { return lhs.resolve(rhs->fontDescription()); }); } QFont KeyFilterManager::font(const Key &key, const QFont &baseFont) const { const KeyFilter::FontDescription fd = get_fontdescription(d->filters, key, KeyFilter::FontDescription()); return fd.font(baseFont); } static QColor get_color(const std::vector> &filters, const Key &key, QColor (KeyFilter::*fun)() const) { const auto it = std::find_if(filters.cbegin(), filters.cend(), [&fun, &key](const std::shared_ptr &filter) { return filter->matches(key, KeyFilter::Appearance) && (filter.get()->*fun)().isValid(); }); if (it == filters.cend()) { return {}; } else { return (it->get()->*fun)(); } } static QString get_string(const std::vector> &filters, const Key &key, QString (KeyFilter::*fun)() const) { const auto it = std::find_if(filters.cbegin(), filters.cend(), [&fun, &key](const std::shared_ptr &filter) { return filter->matches(key, KeyFilter::Appearance) && !(filter.get()->*fun)().isEmpty(); }); if (it == filters.cend()) { return QString(); } else { return (*it)->icon(); } } QColor KeyFilterManager::bgColor(const Key &key) const { return get_color(d->filters, key, &KeyFilter::bgColor); } QColor KeyFilterManager::fgColor(const Key &key) const { return get_color(d->filters, key, &KeyFilter::fgColor); } QIcon KeyFilterManager::icon(const Key &key) const { const QString icon = get_string(d->filters, key, &KeyFilter::icon); return icon.isEmpty() ? QIcon() : QIcon::fromTheme(icon); } diff --git a/src/kleo/keygroup.cpp b/src/kleo/keygroup.cpp index ec29361f4..51c13ad63 100644 --- a/src/kleo/keygroup.cpp +++ b/src/kleo/keygroup.cpp @@ -1,140 +1,142 @@ /* kleo/keygroup.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "keygroup.h" #include #include using namespace Kleo; using namespace GpgME; class KeyGroup::Private { public: explicit Private(const Id &id, const QString &name, const std::vector &keys, Source source); Id id; QString name; Keys keys; Source source; bool isImmutable = true; }; KeyGroup::Private::Private(const Id &id, const QString &name, const std::vector &keys, Source source) : id(id) , name(name) , keys(keys.cbegin(), keys.cend()) , source(source) { } KeyGroup::KeyGroup() : KeyGroup(QString(), QString(), {}, UnknownSource) { } KeyGroup::~KeyGroup() = default; KeyGroup::KeyGroup(const Id &id, const QString &name, const std::vector &keys, Source source) : d(new Private(id, name, keys, source)) { } KeyGroup::KeyGroup(const KeyGroup &other) : d(new Private(*other.d)) { } KeyGroup &KeyGroup::operator=(const KeyGroup &other) { *d = *other.d; return *this; } KeyGroup::KeyGroup(KeyGroup &&other) = default; KeyGroup &KeyGroup::operator=(KeyGroup &&other) = default; bool KeyGroup::isNull() const { return !d || d->id.isEmpty(); } KeyGroup::Id KeyGroup::id() const { return d ? d->id : QString(); } void KeyGroup::setName(const QString &name) { if (d) { d->name = name; } } QString KeyGroup::name() const { return d ? d->name : QString(); } void KeyGroup::setKeys(const KeyGroup::Keys &keys) { if (d) { d->keys = keys; } } void KeyGroup::setKeys(const std::vector &keys) { if (d) { d->keys = Keys(keys.cbegin(), keys.cend()); } } const KeyGroup::Keys &KeyGroup::keys() const { static const Keys empty; return d ? d->keys : empty; } KeyGroup::Source KeyGroup::source() const { return d ? d->source : UnknownSource; } void KeyGroup::setIsImmutable(bool isImmutable) { if (d) { d->isImmutable = isImmutable; } } bool KeyGroup::isImmutable() const { return d ? d->isImmutable : true; } bool KeyGroup::insert(const GpgME::Key &key) { if (!d || key.isNull()) { return false; } return d->keys.insert(key).second; } bool KeyGroup::erase(const GpgME::Key &key) { if (!d || key.isNull()) { return false; } return d->keys.erase(key) > 0; } diff --git a/src/kleo/keygroupconfig.cpp b/src/kleo/keygroupconfig.cpp index f0ee1c612..b151468ed 100644 --- a/src/kleo/keygroupconfig.cpp +++ b/src/kleo/keygroupconfig.cpp @@ -1,175 +1,177 @@ /* kleo/keygroupconfig.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "keygroupconfig.h" #include "debug.h" #include "keygroup.h" #include "models/keycache.h" #include "utils/keyhelpers.h" #include "utils/qtstlhelpers.h" #include #include #include #include #include using namespace Kleo; using namespace GpgME; static const QString groupNamePrefix = QStringLiteral("Group-"); class KeyGroupConfig::Private { public: explicit Private(const QString &filename); std::vector readGroups() const; KeyGroup writeGroup(const KeyGroup &group); bool removeGroup(const KeyGroup &group); private: KeyGroup readGroup(const KSharedConfigPtr &groupsConfig, const QString &groupId) const; private: QString filename; }; KeyGroupConfig::Private::Private(const QString &filename) : filename{filename} { if (filename.isEmpty()) { qCWarning(LIBKLEO_LOG) << __func__ << "Warning: name of configuration file is empty"; } } KeyGroup KeyGroupConfig::Private::readGroup(const KSharedConfigPtr &groupsConfig, const QString &groupId) const { const KConfigGroup configGroup = groupsConfig->group(groupNamePrefix + groupId); const QString groupName = configGroup.readEntry("Name", QString()); const auto fingerprints = toStdStrings(configGroup.readEntry("Keys", QStringList())); const std::vector groupKeys = KeyCache::instance()->findByFingerprint(fingerprints); // treat group as immutable if any of its entries is immutable const QStringList entries = configGroup.keyList(); const bool isImmutable = (configGroup.isImmutable() // || std::any_of(entries.begin(), entries.end(), [configGroup](const QString &entry) { return configGroup.isEntryImmutable(entry); })); KeyGroup g(groupId, groupName, groupKeys, KeyGroup::ApplicationConfig); g.setIsImmutable(isImmutable); qCDebug(LIBKLEO_LOG) << "Read group" << g; return g; } std::vector KeyGroupConfig::Private::readGroups() const { std::vector groups; if (filename.isEmpty()) { return groups; } const KSharedConfigPtr groupsConfig = KSharedConfig::openConfig(filename); const QStringList configGroups = groupsConfig->groupList(); for (const QString &configGroupName : configGroups) { qCDebug(LIBKLEO_LOG) << "Reading config group" << configGroupName; if (configGroupName.startsWith(groupNamePrefix)) { const QString keyGroupId = configGroupName.mid(groupNamePrefix.size()); if (keyGroupId.isEmpty()) { qCWarning(LIBKLEO_LOG) << "Config group" << configGroupName << "has empty group id"; continue; } KeyGroup group = readGroup(groupsConfig, keyGroupId); groups.push_back(group); } } return groups; } KeyGroup KeyGroupConfig::Private::writeGroup(const KeyGroup &group) { if (filename.isEmpty()) { return {}; } if (group.isNull()) { qCDebug(LIBKLEO_LOG) << __func__ << "Error: group is null"; return group; } KSharedConfigPtr groupsConfig = KSharedConfig::openConfig(filename); KConfigGroup configGroup = groupsConfig->group(groupNamePrefix + group.id()); qCDebug(LIBKLEO_LOG) << __func__ << "Writing config group" << configGroup.name(); configGroup.writeEntry("Name", group.name()); configGroup.writeEntry("Keys", Kleo::getFingerprints(group.keys())); // reread group to ensure that it reflects the saved group in case of immutable entries return readGroup(groupsConfig, group.id()); } bool KeyGroupConfig::Private::removeGroup(const KeyGroup &group) { if (filename.isEmpty()) { return false; } if (group.isNull()) { qCDebug(LIBKLEO_LOG) << __func__ << "Error: group is null"; return false; } KSharedConfigPtr groupsConfig = KSharedConfig::openConfig(filename); KConfigGroup configGroup = groupsConfig->group(groupNamePrefix + group.id()); qCDebug(LIBKLEO_LOG) << __func__ << "Removing config group" << configGroup.name(); configGroup.deleteGroup(); return true; } KeyGroupConfig::KeyGroupConfig(const QString &filename) : d{std::make_unique(filename)} { } KeyGroupConfig::~KeyGroupConfig() = default; std::vector KeyGroupConfig::readGroups() const { return d->readGroups(); } KeyGroup KeyGroupConfig::writeGroup(const KeyGroup &group) { return d->writeGroup(group); } void KeyGroupConfig::writeGroups(const std::vector &groups) { std::for_each(std::begin(groups), std::end(groups), [this](const auto &group) { d->writeGroup(group); }); } bool KeyGroupConfig::removeGroup(const KeyGroup &group) { return d->removeGroup(group); } diff --git a/src/kleo/keyresolver.cpp b/src/kleo/keyresolver.cpp index 534aab437..e6a70e208 100644 --- a/src/kleo/keyresolver.cpp +++ b/src/kleo/keyresolver.cpp @@ -1,162 +1,164 @@ /* -*- c++ -*- keyresolver.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2018 Intevation GmbH SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker Based on kpgp.cpp SPDX-FileCopyrightText: 2001, 2002 the KPGP authors See file libkdenetwork/AUTHORS.kpgp for details SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "keyresolver.h" #include "keyresolvercore.h" #include "models/keycache.h" #include "ui/newkeyapprovaldialog.h" #include "utils/formatting.h" #include #include using namespace Kleo; using namespace GpgME; class KeyResolver::Private { public: Private(KeyResolver *qq, bool enc, bool sig, Protocol fmt, bool allowMixed) : q(qq) , mCore(enc, sig, fmt) , mFormat(fmt) , mEncrypt(enc) , mSign(sig) , mAllowMixed(allowMixed) , mCache(KeyCache::instance()) , mDialogWindowFlags(Qt::WindowFlags()) , mPreferredProtocol(UnknownProtocol) { mCore.setAllowMixedProtocols(allowMixed); } ~Private() = default; void showApprovalDialog(KeyResolverCore::Result result, QWidget *parent); void dialogAccepted(); KeyResolver *const q; KeyResolverCore mCore; Solution mResult; Protocol mFormat; bool mEncrypt; bool mSign; bool mAllowMixed; // The cache is needed as a member variable to avoid rebuilding // it between calls if we are the only user. std::shared_ptr mCache; std::unique_ptr mDialog; Qt::WindowFlags mDialogWindowFlags; Protocol mPreferredProtocol; }; void KeyResolver::Private::showApprovalDialog(KeyResolverCore::Result result, QWidget *parent) { const QString sender = mCore.normalizedSender(); mDialog = std::make_unique(mEncrypt, mSign, sender, std::move(result.solution), std::move(result.alternative), mAllowMixed, mFormat, parent, mDialogWindowFlags); connect(mDialog.get(), &QDialog::accepted, q, [this]() { dialogAccepted(); }); connect(mDialog.get(), &QDialog::rejected, q, [this]() { Q_EMIT q->keysResolved(false, false); }); mDialog->open(); } void KeyResolver::Private::dialogAccepted() { mResult = mDialog->result(); Q_EMIT q->keysResolved(true, false); } void KeyResolver::start(bool showApproval, QWidget *parentWidget) { qCDebug(LIBKLEO_LOG) << "Starting "; if (!d->mSign && !d->mEncrypt) { // nothing to do return Q_EMIT keysResolved(true, true); } const auto result = d->mCore.resolve(); const bool success = (result.flags & KeyResolverCore::AllResolved); if (success && !showApproval) { d->mResult = std::move(result.solution); Q_EMIT keysResolved(true, false); return; } else if (success) { qCDebug(LIBKLEO_LOG) << "No need for the user showing approval anyway."; } d->showApprovalDialog(std::move(result), parentWidget); } KeyResolver::KeyResolver(bool encrypt, bool sign, Protocol fmt, bool allowMixed) : d(new Private(this, encrypt, sign, fmt, allowMixed)) { } Kleo::KeyResolver::~KeyResolver() = default; void KeyResolver::setRecipients(const QStringList &addresses) { d->mCore.setRecipients(addresses); } void KeyResolver::setSender(const QString &address) { d->mCore.setSender(address); } void KeyResolver::setOverrideKeys(const QMap> &overrides) { d->mCore.setOverrideKeys(overrides); } void KeyResolver::setSigningKeys(const QStringList &fingerprints) { d->mCore.setSigningKeys(fingerprints); } KeyResolver::Solution KeyResolver::result() const { return d->mResult; } void KeyResolver::setDialogWindowFlags(Qt::WindowFlags flags) { d->mDialogWindowFlags = flags; } void KeyResolver::setPreferredProtocol(Protocol proto) { d->mCore.setPreferredProtocol(proto); } void KeyResolver::setMinimumValidity(int validity) { d->mCore.setMinimumValidity(validity); } diff --git a/src/kleo/keyresolvercore.cpp b/src/kleo/keyresolvercore.cpp index 70c137191..96edc12c0 100644 --- a/src/kleo/keyresolvercore.cpp +++ b/src/kleo/keyresolvercore.cpp @@ -1,785 +1,787 @@ /* -*- c++ -*- kleo/keyresolvercore.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2018 Intevation GmbH SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker Based on kpgp.cpp SPDX-FileCopyrightText: 2001, 2002 the KPGP authors See file libkdenetwork/AUTHORS.kpgp for details SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "keyresolvercore.h" #include "kleo/enum.h" #include "kleo/keygroup.h" #include "models/keycache.h" #include "utils/formatting.h" #include "utils/gnupg.h" #include #include using namespace Kleo; using namespace GpgME; namespace { QDebug operator<<(QDebug debug, const GpgME::Key &key) { if (key.isNull()) { debug << "Null"; } else { debug << Formatting::summaryLine(key); } return debug.maybeSpace(); } static inline bool ValidEncryptionKey(const Key &key) { if (key.isNull() || key.isRevoked() || key.isExpired() || key.isDisabled() || !key.canEncrypt()) { return false; } return true; } static inline bool ValidSigningKey(const Key &key) { if (key.isNull() || key.isRevoked() || key.isExpired() || key.isDisabled() || !key.canSign() || !key.hasSecret()) { return false; } return true; } static int keyValidity(const Key &key, const QString &address) { // returns the validity of the UID matching the address or, if no UID matches, the maximal validity of all UIDs int overallValidity = UserID::Validity::Unknown; for (const auto &uid : key.userIDs()) { if (QString::fromStdString(uid.addrSpec()).toLower() == address.toLower()) { return uid.validity(); } overallValidity = std::max(overallValidity, static_cast(uid.validity())); } return overallValidity; } static int minimumValidity(const std::vector &keys, const QString &address) { const int minValidity = std::accumulate(keys.cbegin(), // keys.cend(), UserID::Ultimate + 1, [address](int validity, const Key &key) { return std::min(validity, keyValidity(key, address)); }); return minValidity <= UserID::Ultimate ? static_cast(minValidity) : UserID::Unknown; } bool allKeysHaveProtocol(const std::vector &keys, Protocol protocol) { return std::all_of(keys.cbegin(), keys.cend(), [protocol](const Key &key) { return key.protocol() == protocol; }); } bool anyKeyHasProtocol(const std::vector &keys, Protocol protocol) { return std::any_of(std::begin(keys), std::end(keys), [protocol](const Key &key) { return key.protocol() == protocol; }); } } // namespace class KeyResolverCore::Private { public: Private(KeyResolverCore *qq, bool enc, bool sig, Protocol fmt) : q(qq) , mFormat(fmt) , mEncrypt(enc) , mSign(sig) , mCache(KeyCache::instance()) , mPreferredProtocol(UnknownProtocol) , mMinimumValidity(UserID::Marginal) { } ~Private() = default; bool isAcceptableSigningKey(const Key &key); bool isAcceptableEncryptionKey(const Key &key, const QString &address = QString()); void setSender(const QString &address); void addRecipients(const QStringList &addresses); void setOverrideKeys(const QMap> &overrides); void resolveOverrides(); std::vector resolveRecipientWithGroup(const QString &address, Protocol protocol); void resolveEncryptionGroups(); std::vector resolveSenderWithGroup(const QString &address, Protocol protocol); void resolveSigningGroups(); void resolveSign(Protocol proto); void setSigningKeys(const QStringList &fingerprints); std::vector resolveRecipient(const QString &address, Protocol protocol); void resolveEnc(Protocol proto); void mergeEncryptionKeys(); Result resolve(); KeyResolverCore *const q; QString mSender; QStringList mRecipients; QMap> mSigKeys; QMap>> mEncKeys; QMap> mOverrides; Protocol mFormat; QStringList mFatalErrors; bool mEncrypt; bool mSign; // The cache is needed as a member variable to avoid rebuilding // it between calls if we are the only user. std::shared_ptr mCache; bool mAllowMixed = true; Protocol mPreferredProtocol; int mMinimumValidity; }; bool KeyResolverCore::Private::isAcceptableSigningKey(const Key &key) { if (!ValidSigningKey(key)) { return false; } if (Kleo::gnupgIsDeVsCompliant()) { if (!Formatting::isKeyDeVs(key)) { qCDebug(LIBKLEO_LOG) << "Rejected sig key" << key.primaryFingerprint() << "because it is not de-vs compliant."; return false; } } return true; } bool KeyResolverCore::Private::isAcceptableEncryptionKey(const Key &key, const QString &address) { if (!ValidEncryptionKey(key)) { return false; } if (Kleo::gnupgIsDeVsCompliant()) { if (!Formatting::isKeyDeVs(key)) { qCDebug(LIBKLEO_LOG) << "Rejected enc key" << key.primaryFingerprint() << "because it is not de-vs compliant."; return false; } } if (address.isEmpty()) { return true; } for (const auto &uid : key.userIDs()) { if (uid.addrSpec() == address.toStdString()) { if (uid.validity() >= mMinimumValidity) { return true; } } } return false; } void KeyResolverCore::Private::setSender(const QString &address) { const auto normalized = UserID::addrSpecFromString(address.toUtf8().constData()); if (normalized.empty()) { // should not happen bug in the caller, non localized // error for bug reporting. mFatalErrors << QStringLiteral("The sender address '%1' could not be extracted").arg(address); return; } const auto normStr = QString::fromUtf8(normalized.c_str()); mSender = normStr; addRecipients({address}); } void KeyResolverCore::Private::addRecipients(const QStringList &addresses) { if (!mEncrypt) { return; } // Internally we work with normalized addresses. Normalization // matches the gnupg one. for (const auto &addr : addresses) { // PGP Uids are defined to be UTF-8 (RFC 4880 §5.11) const auto normalized = UserID::addrSpecFromString(addr.toUtf8().constData()); if (normalized.empty()) { // should not happen bug in the caller, non localized // error for bug reporting. mFatalErrors << QStringLiteral("The mail address for '%1' could not be extracted").arg(addr); continue; } const QString normStr = QString::fromUtf8(normalized.c_str()); mRecipients << normStr; // Initially add empty lists of keys for both protocols mEncKeys[normStr] = {{CMS, {}}, {OpenPGP, {}}}; } } void KeyResolverCore::Private::setOverrideKeys(const QMap> &overrides) { for (auto protocolIt = overrides.cbegin(); protocolIt != overrides.cend(); ++protocolIt) { const Protocol &protocol = protocolIt.key(); const auto &addressFingerprintMap = protocolIt.value(); for (auto addressIt = addressFingerprintMap.cbegin(); addressIt != addressFingerprintMap.cend(); ++addressIt) { const QString &address = addressIt.key(); const QStringList &fingerprints = addressIt.value(); const QString normalizedAddress = QString::fromUtf8(UserID::addrSpecFromString(address.toUtf8().constData()).c_str()); mOverrides[normalizedAddress][protocol] = fingerprints; } } } namespace { std::vector resolveOverride(const QString &address, Protocol protocol, const QStringList &fingerprints) { std::vector keys; for (const auto &fprOrId : fingerprints) { const Key key = KeyCache::instance()->findByKeyIDOrFingerprint(fprOrId.toUtf8().constData()); if (key.isNull()) { // FIXME: Report to caller qCDebug(LIBKLEO_LOG) << "Failed to find override key for:" << address << "fpr:" << fprOrId; continue; } if (protocol != UnknownProtocol && key.protocol() != protocol) { qCDebug(LIBKLEO_LOG) << "Ignoring key" << Formatting::summaryLine(key) << "given as" << Formatting::displayName(protocol) << "override for" << address; continue; } qCDebug(LIBKLEO_LOG) << "Using key" << Formatting::summaryLine(key) << "as" << Formatting::displayName(protocol) << "override for" << address; keys.push_back(key); } return keys; } } void KeyResolverCore::Private::resolveOverrides() { if (!mEncrypt) { // No encryption we are done. return; } for (auto addressIt = mOverrides.cbegin(); addressIt != mOverrides.cend(); ++addressIt) { const QString &address = addressIt.key(); const auto &protocolFingerprintsMap = addressIt.value(); if (!mRecipients.contains(address)) { qCDebug(LIBKLEO_LOG) << "Overrides provided for an address that is " "neither sender nor recipient. Address:" << address; continue; } const QStringList commonOverride = protocolFingerprintsMap.value(UnknownProtocol); if (!commonOverride.empty()) { mEncKeys[address][UnknownProtocol] = resolveOverride(address, UnknownProtocol, commonOverride); if (protocolFingerprintsMap.contains(OpenPGP)) { qCDebug(LIBKLEO_LOG) << "Ignoring OpenPGP-specific override for" << address << "in favor of common override"; } if (protocolFingerprintsMap.contains(CMS)) { qCDebug(LIBKLEO_LOG) << "Ignoring S/MIME-specific override for" << address << "in favor of common override"; } } else { if (mFormat != CMS) { mEncKeys[address][OpenPGP] = resolveOverride(address, OpenPGP, protocolFingerprintsMap.value(OpenPGP)); } if (mFormat != OpenPGP) { mEncKeys[address][CMS] = resolveOverride(address, CMS, protocolFingerprintsMap.value(CMS)); } } } } std::vector KeyResolverCore::Private::resolveSenderWithGroup(const QString &address, Protocol protocol) { // prefer single-protocol groups over mixed-protocol groups auto group = mCache->findGroup(address, protocol, KeyCache::KeyUsage::Sign); if (group.isNull()) { group = mCache->findGroup(address, UnknownProtocol, KeyCache::KeyUsage::Sign); } if (group.isNull()) { return {}; } // take the first key matching the protocol const auto &keys = group.keys(); const auto it = std::find_if(std::begin(keys), std::end(keys), [protocol](const auto &key) { return key.protocol() == protocol; }); if (it == std::end(keys)) { qCDebug(LIBKLEO_LOG) << "group" << group.name() << "has no" << Formatting::displayName(protocol) << "signing key"; return {}; } const auto key = *it; if (!isAcceptableSigningKey(key)) { qCDebug(LIBKLEO_LOG) << "group" << group.name() << "has unacceptable signing key" << key; return {}; } return {key}; } void KeyResolverCore::Private::resolveSigningGroups() { auto &protocolKeysMap = mSigKeys; if (!protocolKeysMap[UnknownProtocol].empty()) { // already resolved by common override return; } if (mFormat == OpenPGP) { if (!protocolKeysMap[OpenPGP].empty()) { // already resolved by override return; } protocolKeysMap[OpenPGP] = resolveSenderWithGroup(mSender, OpenPGP); } else if (mFormat == CMS) { if (!protocolKeysMap[CMS].empty()) { // already resolved by override return; } protocolKeysMap[CMS] = resolveSenderWithGroup(mSender, CMS); } else { protocolKeysMap[OpenPGP] = resolveSenderWithGroup(mSender, OpenPGP); protocolKeysMap[CMS] = resolveSenderWithGroup(mSender, CMS); } } void KeyResolverCore::Private::resolveSign(Protocol proto) { if (!mSigKeys[proto].empty()) { // Explicitly set return; } const auto key = mCache->findBestByMailBox(mSender.toUtf8().constData(), proto, KeyCache::KeyUsage::Sign); if (key.isNull()) { qCDebug(LIBKLEO_LOG) << "Failed to find" << Formatting::displayName(proto) << "signing key for" << mSender; return; } if (!isAcceptableSigningKey(key)) { qCDebug(LIBKLEO_LOG) << "Unacceptable signing key" << key.primaryFingerprint() << "for" << mSender; return; } mSigKeys.insert(proto, {key}); } void KeyResolverCore::Private::setSigningKeys(const QStringList &fingerprints) { if (mSign) { for (const auto &fpr : fingerprints) { const auto key = mCache->findByKeyIDOrFingerprint(fpr.toUtf8().constData()); if (key.isNull()) { qCDebug(LIBKLEO_LOG) << "Failed to find signing key with fingerprint" << fpr; continue; } mSigKeys[key.protocol()].push_back(key); } } } std::vector KeyResolverCore::Private::resolveRecipientWithGroup(const QString &address, Protocol protocol) { const auto group = mCache->findGroup(address, protocol, KeyCache::KeyUsage::Encrypt); if (group.isNull()) { return {}; } // If we have one unacceptable group key we reject the // whole group to avoid the situation where one key is // skipped or the operation fails. // // We are in Autoresolve land here. In the GUI we // will also show unacceptable group keys so that the // user can see which key is not acceptable. const auto &keys = group.keys(); const bool allKeysAreAcceptable = std::all_of(std::begin(keys), std::end(keys), [this](const auto &key) { return isAcceptableEncryptionKey(key); }); if (!allKeysAreAcceptable) { qCDebug(LIBKLEO_LOG) << "group" << group.name() << "has at least one unacceptable key"; return {}; } for (const auto &k : keys) { qCDebug(LIBKLEO_LOG) << "Resolved encrypt to" << address << "with key" << k.primaryFingerprint(); } std::vector result; std::copy(std::begin(keys), std::end(keys), std::back_inserter(result)); return result; } void KeyResolverCore::Private::resolveEncryptionGroups() { for (auto it = mEncKeys.begin(); it != mEncKeys.end(); ++it) { const QString &address = it.key(); auto &protocolKeysMap = it.value(); if (!protocolKeysMap[UnknownProtocol].empty()) { // already resolved by common override continue; } if (mFormat == OpenPGP) { if (!protocolKeysMap[OpenPGP].empty()) { // already resolved by override continue; } protocolKeysMap[OpenPGP] = resolveRecipientWithGroup(address, OpenPGP); } else if (mFormat == CMS) { if (!protocolKeysMap[CMS].empty()) { // already resolved by override continue; } protocolKeysMap[CMS] = resolveRecipientWithGroup(address, CMS); } else { // prefer single-protocol groups over mixed-protocol groups const auto openPGPGroupKeys = resolveRecipientWithGroup(address, OpenPGP); const auto smimeGroupKeys = resolveRecipientWithGroup(address, CMS); if (!openPGPGroupKeys.empty() && !smimeGroupKeys.empty()) { protocolKeysMap[OpenPGP] = openPGPGroupKeys; protocolKeysMap[CMS] = smimeGroupKeys; } else if (openPGPGroupKeys.empty() && smimeGroupKeys.empty()) { // no single-protocol groups found; // if mixed protocols are allowed, then look for any group with encryption keys if (mAllowMixed) { protocolKeysMap[UnknownProtocol] = resolveRecipientWithGroup(address, UnknownProtocol); } } else { // there is a single-protocol group only for one protocol; use this group for all protocols protocolKeysMap[UnknownProtocol] = !openPGPGroupKeys.empty() ? openPGPGroupKeys : smimeGroupKeys; } } } } std::vector KeyResolverCore::Private::resolveRecipient(const QString &address, Protocol protocol) { const auto key = mCache->findBestByMailBox(address.toUtf8().constData(), protocol, KeyCache::KeyUsage::Encrypt); if (key.isNull()) { qCDebug(LIBKLEO_LOG) << "Failed to find any" << Formatting::displayName(protocol) << "key for:" << address; return {}; } if (!isAcceptableEncryptionKey(key, address)) { qCDebug(LIBKLEO_LOG) << "key for:" << address << key.primaryFingerprint() << "has not enough validity"; return {}; } qCDebug(LIBKLEO_LOG) << "Resolved encrypt to" << address << "with key" << key.primaryFingerprint(); return {key}; } // Try to find matching keys in the provided protocol for the unresolved addresses void KeyResolverCore::Private::resolveEnc(Protocol proto) { for (auto it = mEncKeys.begin(); it != mEncKeys.end(); ++it) { const QString &address = it.key(); auto &protocolKeysMap = it.value(); if (!protocolKeysMap[proto].empty()) { // already resolved for current protocol (by override or group) continue; } const std::vector &commonOverrideOrGroup = protocolKeysMap[UnknownProtocol]; if (!commonOverrideOrGroup.empty()) { // there is a common override or group; use it for current protocol if possible if (allKeysHaveProtocol(commonOverrideOrGroup, proto)) { protocolKeysMap[proto] = commonOverrideOrGroup; continue; } else { qCDebug(LIBKLEO_LOG) << "Common override/group for" << address << "is unusable for" << Formatting::displayName(proto); continue; } } protocolKeysMap[proto] = resolveRecipient(address, proto); } } auto getBestEncryptionKeys(const QMap>> &encryptionKeys, Protocol preferredProtocol) { QMap> result; for (auto it = encryptionKeys.begin(); it != encryptionKeys.end(); ++it) { const QString &address = it.key(); auto &protocolKeysMap = it.value(); const std::vector &overrideKeys = protocolKeysMap[UnknownProtocol]; if (!overrideKeys.empty()) { result.insert(address, overrideKeys); continue; } const std::vector &keysOpenPGP = protocolKeysMap[OpenPGP]; const std::vector &keysCMS = protocolKeysMap[CMS]; if (keysOpenPGP.empty() && keysCMS.empty()) { result.insert(address, {}); } else if (!keysOpenPGP.empty() && keysCMS.empty()) { result.insert(address, keysOpenPGP); } else if (keysOpenPGP.empty() && !keysCMS.empty()) { result.insert(address, keysCMS); } else { // check whether OpenPGP keys or S/MIME keys have higher validity const int validityPGP = minimumValidity(keysOpenPGP, address); const int validityCMS = minimumValidity(keysCMS, address); if ((validityCMS > validityPGP) || (validityCMS == validityPGP && preferredProtocol == CMS)) { result.insert(address, keysCMS); } else { result.insert(address, keysOpenPGP); } } } return result; } namespace { bool hasUnresolvedSender(const QMap> &signingKeys, Protocol protocol) { return signingKeys.value(protocol).empty(); } bool hasUnresolvedRecipients(const QMap>> &encryptionKeys, Protocol protocol) { return std::any_of(std::cbegin(encryptionKeys), std::cend(encryptionKeys), [protocol](const auto &protocolKeysMap) { return protocolKeysMap.value(protocol).empty(); }); } bool anyCommonOverrideHasKeyOfType(const QMap>> &encryptionKeys, Protocol protocol) { return std::any_of(std::cbegin(encryptionKeys), std::cend(encryptionKeys), [protocol](const auto &protocolKeysMap) { return anyKeyHasProtocol(protocolKeysMap.value(UnknownProtocol), protocol); }); } auto keysForProtocol(const QMap>> &encryptionKeys, Protocol protocol) { QMap> keys; for (auto it = std::begin(encryptionKeys), end = std::end(encryptionKeys); it != end; ++it) { const QString &address = it.key(); const auto &protocolKeysMap = it.value(); keys.insert(address, protocolKeysMap.value(protocol)); } return keys; } template auto concatenate(std::vector v1, const std::vector &v2) { v1.reserve(v1.size() + v2.size()); v1.insert(std::end(v1), std::begin(v2), std::end(v2)); return v1; } } KeyResolverCore::Result KeyResolverCore::Private::resolve() { qCDebug(LIBKLEO_LOG) << "Starting "; if (!mSign && !mEncrypt) { // nothing to do return {AllResolved, {}, {}}; } // First resolve through overrides resolveOverrides(); // check protocols needed for overrides const bool commonOverridesNeedOpenPGP = anyCommonOverrideHasKeyOfType(mEncKeys, OpenPGP); const bool commonOverridesNeedCMS = anyCommonOverrideHasKeyOfType(mEncKeys, CMS); if ((mFormat == OpenPGP && commonOverridesNeedCMS) // || (mFormat == CMS && commonOverridesNeedOpenPGP) // || (!mAllowMixed && commonOverridesNeedOpenPGP && commonOverridesNeedCMS)) { // invalid protocol requirements -> clear intermediate result and abort resolution mEncKeys.clear(); return {Error, {}, {}}; } // Next look for matching groups of keys if (mSign) { resolveSigningGroups(); } if (mEncrypt) { resolveEncryptionGroups(); } // Then look for signing / encryption keys if (mFormat == OpenPGP || mFormat == UnknownProtocol) { resolveSign(OpenPGP); resolveEnc(OpenPGP); } const bool pgpOnly = ((!mEncrypt || !hasUnresolvedRecipients(mEncKeys, OpenPGP)) // && (!mSign || !hasUnresolvedSender(mSigKeys, OpenPGP))); if (mFormat == OpenPGP) { return { SolutionFlags((pgpOnly ? AllResolved : SomeUnresolved) | OpenPGPOnly), {OpenPGP, mSigKeys.value(OpenPGP), keysForProtocol(mEncKeys, OpenPGP)}, {}, }; } if (mFormat == CMS || mFormat == UnknownProtocol) { resolveSign(CMS); resolveEnc(CMS); } const bool cmsOnly = ((!mEncrypt || !hasUnresolvedRecipients(mEncKeys, CMS)) // && (!mSign || !hasUnresolvedSender(mSigKeys, CMS))); if (mFormat == CMS) { return { SolutionFlags((cmsOnly ? AllResolved : SomeUnresolved) | CMSOnly), {CMS, mSigKeys.value(CMS), keysForProtocol(mEncKeys, CMS)}, {}, }; } // check if single-protocol solution has been found if (cmsOnly && (!pgpOnly || mPreferredProtocol == CMS)) { if (!mAllowMixed) { return { SolutionFlags(AllResolved | CMSOnly), {CMS, mSigKeys.value(CMS), keysForProtocol(mEncKeys, CMS)}, {OpenPGP, mSigKeys.value(OpenPGP), keysForProtocol(mEncKeys, OpenPGP)}, }; } else { return { SolutionFlags(AllResolved | CMSOnly), {CMS, mSigKeys.value(CMS), keysForProtocol(mEncKeys, CMS)}, {}, }; } } if (pgpOnly) { if (!mAllowMixed) { return { SolutionFlags(AllResolved | OpenPGPOnly), {OpenPGP, mSigKeys.value(OpenPGP), keysForProtocol(mEncKeys, OpenPGP)}, {CMS, mSigKeys.value(CMS), keysForProtocol(mEncKeys, CMS)}, }; } else { return { SolutionFlags(AllResolved | OpenPGPOnly), {OpenPGP, mSigKeys.value(OpenPGP), keysForProtocol(mEncKeys, OpenPGP)}, {}, }; } } if (!mAllowMixed) { // return incomplete single-protocol solution if (mPreferredProtocol == CMS) { return { SolutionFlags(SomeUnresolved | CMSOnly), {CMS, mSigKeys.value(CMS), keysForProtocol(mEncKeys, CMS)}, {OpenPGP, mSigKeys.value(OpenPGP), keysForProtocol(mEncKeys, OpenPGP)}, }; } else { return { SolutionFlags(SomeUnresolved | OpenPGPOnly), {OpenPGP, mSigKeys.value(OpenPGP), keysForProtocol(mEncKeys, OpenPGP)}, {CMS, mSigKeys.value(CMS), keysForProtocol(mEncKeys, CMS)}, }; } } const auto bestEncryptionKeys = getBestEncryptionKeys(mEncKeys, mPreferredProtocol); // we are in mixed mode, i.e. we need an OpenPGP signing key and an S/MIME signing key const bool senderIsResolved = (!mSign || (!hasUnresolvedSender(mSigKeys, OpenPGP) && !hasUnresolvedSender(mSigKeys, CMS))); const bool allRecipientsAreResolved = std::all_of(std::begin(bestEncryptionKeys), std::end(bestEncryptionKeys), [](const auto &keys) { return !keys.empty(); }); if (senderIsResolved && allRecipientsAreResolved) { return { SolutionFlags(AllResolved | MixedProtocols), {UnknownProtocol, concatenate(mSigKeys.value(OpenPGP), mSigKeys.value(CMS)), bestEncryptionKeys}, {}, }; } const bool allKeysAreOpenPGP = std::all_of(std::begin(bestEncryptionKeys), std::end(bestEncryptionKeys), [](const auto &keys) { return allKeysHaveProtocol(keys, OpenPGP); }); if (allKeysAreOpenPGP) { return { SolutionFlags(SomeUnresolved | OpenPGPOnly), {OpenPGP, mSigKeys.value(OpenPGP), bestEncryptionKeys}, {}, }; } const bool allKeysAreCMS = std::all_of(std::begin(bestEncryptionKeys), std::end(bestEncryptionKeys), [](const auto &keys) { return allKeysHaveProtocol(keys, CMS); }); if (allKeysAreCMS) { return { SolutionFlags(SomeUnresolved | CMSOnly), {CMS, mSigKeys.value(CMS), bestEncryptionKeys}, {}, }; } return { SolutionFlags(SomeUnresolved | MixedProtocols), {UnknownProtocol, concatenate(mSigKeys.value(OpenPGP), mSigKeys.value(CMS)), bestEncryptionKeys}, {}, }; } KeyResolverCore::KeyResolverCore(bool encrypt, bool sign, Protocol fmt) : d(new Private(this, encrypt, sign, fmt)) { } KeyResolverCore::~KeyResolverCore() = default; void KeyResolverCore::setSender(const QString &address) { d->setSender(address); } QString KeyResolverCore::normalizedSender() const { return d->mSender; } void KeyResolverCore::setRecipients(const QStringList &addresses) { d->addRecipients(addresses); } void KeyResolverCore::setSigningKeys(const QStringList &fingerprints) { d->setSigningKeys(fingerprints); } void KeyResolverCore::setOverrideKeys(const QMap> &overrides) { d->setOverrideKeys(overrides); } void KeyResolverCore::setAllowMixedProtocols(bool allowMixed) { d->mAllowMixed = allowMixed; } void KeyResolverCore::setPreferredProtocol(Protocol proto) { d->mPreferredProtocol = proto; } void KeyResolverCore::setMinimumValidity(int validity) { d->mMinimumValidity = validity; } KeyResolverCore::Result KeyResolverCore::resolve() { return d->resolve(); } diff --git a/src/kleo/keyserverconfig.cpp b/src/kleo/keyserverconfig.cpp index 1d3c4d0c2..9b66d9127 100644 --- a/src/kleo/keyserverconfig.cpp +++ b/src/kleo/keyserverconfig.cpp @@ -1,219 +1,221 @@ /* kleo/keyserverconfig.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "keyserverconfig.h" #include "utils/algorithm.h" #include #include using namespace Kleo; class KeyserverConfig::Private { public: explicit Private(); QString host; int port = -1; // -1 == use default port KeyserverAuthentication authentication = KeyserverAuthentication::Anonymous; QString user; QString password; KeyserverConnection connection = KeyserverConnection::Default; QString baseDn; QStringList additionalFlags; }; KeyserverConfig::Private::Private() { } KeyserverConfig::KeyserverConfig() : d{std::make_unique()} { } KeyserverConfig::~KeyserverConfig() = default; KeyserverConfig::KeyserverConfig(const KeyserverConfig &other) : d{std::make_unique(*other.d)} { } KeyserverConfig &KeyserverConfig::operator=(const KeyserverConfig &other) { *d = *other.d; return *this; } KeyserverConfig::KeyserverConfig(KeyserverConfig &&other) = default; KeyserverConfig &KeyserverConfig::operator=(KeyserverConfig &&other) = default; KeyserverConfig KeyserverConfig::fromUrl(const QUrl &url) { KeyserverConfig config; config.d->host = url.host(); config.d->port = url.port(); config.d->user = url.userName(); config.d->password = url.password(); if (!config.d->user.isEmpty()) { config.d->authentication = KeyserverAuthentication::Password; } if (url.hasFragment()) { const auto flags = transformInPlace(url.fragment().split(QLatin1Char{','}, Qt::SkipEmptyParts), [](const auto &flag) { return flag.trimmed().toLower(); }); for (const auto &flag : flags) { if (flag == QLatin1String{"starttls"}) { config.d->connection = KeyserverConnection::UseSTARTTLS; } else if (flag == QLatin1String{"ldaptls"}) { config.d->connection = KeyserverConnection::TunnelThroughTLS; } else if (flag == QLatin1String{"plain"}) { config.d->connection = KeyserverConnection::Plain; } else if (flag == QLatin1String{"ntds"}) { config.d->authentication = KeyserverAuthentication::ActiveDirectory; } else { config.d->additionalFlags.push_back(flag); } } } if (url.hasQuery()) { config.d->baseDn = url.query(); } return config; } QUrl KeyserverConfig::toUrl() const { QUrl url; url.setScheme(QStringLiteral("ldap")); // set host to empty string if it's a null string; this ensures that the URL has an authority and always gets a "//" after the scheme url.setHost(d->host.isNull() ? QStringLiteral("") : d->host); if (d->port != -1) { url.setPort(d->port); } if (!d->user.isEmpty()) { url.setUserName(d->user); } if (!d->password.isEmpty()) { url.setPassword(d->password); } if (!d->baseDn.isEmpty()) { url.setQuery(d->baseDn); } QStringList flags; switch (d->connection) { case KeyserverConnection::UseSTARTTLS: flags.push_back(QStringLiteral("starttls")); break; case KeyserverConnection::TunnelThroughTLS: flags.push_back(QStringLiteral("ldaptls")); break; case KeyserverConnection::Plain: flags.push_back(QStringLiteral("plain")); break; case KeyserverConnection::Default:; // omit connection flag to use default } if (d->authentication == KeyserverAuthentication::ActiveDirectory) { flags.push_back(QStringLiteral("ntds")); } std::copy(std::cbegin(d->additionalFlags), std::cend(d->additionalFlags), std::back_inserter(flags)); if (!flags.isEmpty()) { url.setFragment(flags.join(QLatin1Char{','})); } return url; } QString KeyserverConfig::host() const { return d->host; } void KeyserverConfig::setHost(const QString &host) { d->host = host; } int KeyserverConfig::port() const { return d->port; } void KeyserverConfig::setPort(int port) { d->port = port; } KeyserverAuthentication KeyserverConfig::authentication() const { return d->authentication; } void KeyserverConfig::setAuthentication(KeyserverAuthentication authentication) { d->authentication = authentication; } QString KeyserverConfig::user() const { return d->user; } void KeyserverConfig::setUser(const QString &user) { d->user = user; } QString KeyserverConfig::password() const { return d->password; } void KeyserverConfig::setPassword(const QString &password) { d->password = password; } KeyserverConnection KeyserverConfig::connection() const { return d->connection; } void KeyserverConfig::setConnection(KeyserverConnection connection) { d->connection = connection; } QString KeyserverConfig::ldapBaseDn() const { return d->baseDn; } void KeyserverConfig::setLdapBaseDn(const QString &baseDn) { d->baseDn = baseDn; } QStringList KeyserverConfig::additionalFlags() const { return d->additionalFlags; } void KeyserverConfig::setAdditionalFlags(const QStringList &flags) { d->additionalFlags = flags; } diff --git a/src/kleo/kleoexception.cpp b/src/kleo/kleoexception.cpp index 939469021..571f8b09d 100644 --- a/src/kleo/kleoexception.cpp +++ b/src/kleo/kleoexception.cpp @@ -1,14 +1,16 @@ /* -*- mode: c++; c-basic-offset:4 -*- exception.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "kleoexception.h" Kleo::Exception::~Exception() throw() { } diff --git a/src/kleo/oidmap.cpp b/src/kleo/oidmap.cpp index a27b7264f..00810d27f 100644 --- a/src/kleo/oidmap.cpp +++ b/src/kleo/oidmap.cpp @@ -1,62 +1,64 @@ /* oidmap.cpp This file is part of libkleo, the KDE keymanagement library SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2022 Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "oidmap.h" #include #include namespace { struct NameAndOID { const char *name; const char *oid; }; static const std::vector oidmap = { // clang-format off // keep them ordered by oid: {"SP", "ST" }, // hack to show the Sphinx-required/desired SP for // StateOrProvince, otherwise known as ST or even S {"NameDistinguisher", "0.2.262.1.10.7.20" }, {"EMAIL", "1.2.840.113549.1.9.1"}, {"SN", "2.5.4.4" }, {"SerialNumber", "2.5.4.5" }, {"T", "2.5.4.12" }, {"D", "2.5.4.13" }, {"BC", "2.5.4.15" }, {"ADDR", "2.5.4.16" }, {"PC", "2.5.4.17" }, {"GN", "2.5.4.42" }, {"Pseudo", "2.5.4.65" }, // clang-format on }; } const char *Kleo::oidForAttributeName(const QString &attr) { QByteArray attrUtf8 = attr.toUtf8(); for (const auto &m : oidmap) { if (qstricmp(attrUtf8.constData(), m.name) == 0) { return m.oid; } } return nullptr; } const char *Kleo::attributeNameForOID(const char *oid) { for (const auto &m : oidmap) { if (qstricmp(oid, m.oid) == 0) { return m.name; } } return nullptr; } diff --git a/src/models/keycache.cpp b/src/models/keycache.cpp index 7172379df..dd4623156 100644 --- a/src/models/keycache.cpp +++ b/src/models/keycache.cpp @@ -1,1739 +1,1741 @@ /* -*- mode: c++; c-basic-offset:4 -*- models/keycache.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007, 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2018 Intevation GmbH SPDX-FileCopyrightText: 2020, 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "keycache.h" #include "keycache_p.h" #include "kleo/dn.h" #include "kleo/enum.h" #include "kleo/keygroup.h" #include "kleo/keygroupconfig.h" #include "kleo/predicates.h" #include "kleo/stl_util.h" #include "utils/algorithm.h" #include "utils/compat.h" #include "utils/filesystemwatcher.h" #include "utils/qtstlhelpers.h" #include #include #include #include #include #include #include #include #include #include #include #include #include //#include #include #include #include #include #include #include #include #include "kleo/debug.h" #include using namespace std::chrono_literals; using namespace Kleo; using namespace GpgME; using namespace KMime::Types; static const unsigned int hours2ms = 1000 * 60 * 60; // // // KeyCache // // namespace { make_comparator_str(ByEMail, .first.c_str()); } class Kleo::KeyCacheAutoRefreshSuspension { KeyCacheAutoRefreshSuspension() { qCDebug(LIBKLEO_LOG) << __func__; auto cache = KeyCache::mutableInstance(); cache->enableFileSystemWatcher(false); m_refreshInterval = cache->refreshInterval(); cache->setRefreshInterval(0); cache->cancelKeyListing(); m_cache = cache; } public: ~KeyCacheAutoRefreshSuspension() { qCDebug(LIBKLEO_LOG) << __func__; if (auto cache = m_cache.lock()) { cache->enableFileSystemWatcher(true); cache->setRefreshInterval(m_refreshInterval); } } static std::shared_ptr instance() { static std::weak_ptr self; if (auto s = self.lock()) { return s; } else { s = std::shared_ptr{new KeyCacheAutoRefreshSuspension{}}; self = s; return s; } } private: std::weak_ptr m_cache; int m_refreshInterval = 0; }; class KeyCache::Private { friend class ::Kleo::KeyCache; KeyCache *const q; public: explicit Private(KeyCache *qq) : q(qq) , m_refreshInterval(1) , m_initalized(false) , m_pgpOnly(true) , m_remarks_enabled(false) { connect(&m_autoKeyListingTimer, &QTimer::timeout, q, [this]() { q->startKeyListing(); }); updateAutoKeyListingTimer(); } ~Private() { if (m_refreshJob) { m_refreshJob->cancel(); } } template class Op> class Comp> std::vector::const_iterator find(const std::vector &keys, const char *key) const { ensureCachePopulated(); const auto it = std::lower_bound(keys.begin(), keys.end(), key, Comp()); if (it == keys.end() || Comp()(*it, key)) { return it; } else { return keys.end(); } } template class Op> class Comp> std::vector::const_iterator find(const std::vector &keys, const char *key) const { ensureCachePopulated(); const auto it = std::lower_bound(keys.begin(), keys.end(), key, Comp()); if (it == keys.end() || Comp()(*it, key)) { return it; } else { return keys.end(); } } std::vector::const_iterator find_fpr(const char *fpr) const { return find<_detail::ByFingerprint>(by.fpr, fpr); } std::pair>::const_iterator, std::vector>::const_iterator> find_email(const char *email) const { ensureCachePopulated(); return std::equal_range(by.email.begin(), by.email.end(), email, ByEMail()); } std::vector find_mailbox(const QString &email, bool sign) const; std::vector::const_iterator find_keygrip(const char *keygrip) const { return find<_detail::ByKeyGrip>(by.keygrip, keygrip); } std::vector::const_iterator find_subkeyid(const char *subkeyid) const { return find<_detail::ByKeyID>(by.subkeyid, subkeyid); } std::vector::const_iterator find_keyid(const char *keyid) const { return find<_detail::ByKeyID>(by.keyid, keyid); } std::vector::const_iterator find_shortkeyid(const char *shortkeyid) const { return find<_detail::ByShortKeyID>(by.shortkeyid, shortkeyid); } std::pair::const_iterator, std::vector::const_iterator> find_subjects(const char *chain_id) const { ensureCachePopulated(); return std::equal_range(by.chainid.begin(), by.chainid.end(), chain_id, _detail::ByChainID()); } void refreshJobDone(const KeyListResult &result); void setRefreshInterval(int interval) { m_refreshInterval = interval; updateAutoKeyListingTimer(); } int refreshInterval() const { return m_refreshInterval; } void updateAutoKeyListingTimer() { setAutoKeyListingInterval(hours2ms * m_refreshInterval); } void setAutoKeyListingInterval(int ms) { m_autoKeyListingTimer.stop(); m_autoKeyListingTimer.setInterval(ms); if (ms != 0) { m_autoKeyListingTimer.start(); } } void ensureCachePopulated() const; void readGroupsFromGpgConf() { // According to Werner Koch groups are more of a hack to solve // a valid usecase (e.g. several keys defined for an internal mailing list) // that won't make it in the proper keylist interface. And using gpgconf // was the suggested way to support groups. auto conf = QGpgME::cryptoConfig(); if (!conf) { return; } auto entry = getCryptoConfigEntry(conf, "gpg", "group"); if (!entry) { return; } // collect the key fingerprints for all groups read from the configuration QMap fingerprints; const auto stringValueList = entry->stringValueList(); for (const QString &value : stringValueList) { const QStringList split = value.split(QLatin1Char('=')); if (split.size() != 2) { qCDebug(LIBKLEO_LOG) << "Ignoring invalid group config:" << value; continue; } const QString groupName = split[0]; const QString fingerprint = split[1]; fingerprints[groupName].push_back(fingerprint); } // add all groups read from the configuration to the list of groups for (auto it = fingerprints.cbegin(); it != fingerprints.cend(); ++it) { const QString groupName = it.key(); const std::vector groupKeys = q->findByFingerprint(toStdStrings(it.value())); KeyGroup g(groupName, groupName, groupKeys, KeyGroup::GnuPGConfig); m_groups.push_back(g); } } void readGroupsFromGroupsConfig() { Q_ASSERT(m_groupConfig); if (!m_groupConfig) { qCWarning(LIBKLEO_LOG) << __func__ << "group config not set"; return; } m_groups = m_groupConfig->readGroups(); } KeyGroup writeGroupToGroupsConfig(const KeyGroup &group) { Q_ASSERT(m_groupConfig); if (!m_groupConfig) { qCWarning(LIBKLEO_LOG) << __func__ << "group config not set"; return {}; } Q_ASSERT(!group.isNull()); Q_ASSERT(group.source() == KeyGroup::ApplicationConfig); if (group.isNull() || group.source() != KeyGroup::ApplicationConfig) { qCDebug(LIBKLEO_LOG) << __func__ << "group cannot be written to application configuration:" << group; return group; } return m_groupConfig->writeGroup(group); } bool removeGroupFromGroupsConfig(const KeyGroup &group) { Q_ASSERT(m_groupConfig); if (!m_groupConfig) { qCWarning(LIBKLEO_LOG) << __func__ << "group config not set"; return false; } Q_ASSERT(!group.isNull()); Q_ASSERT(group.source() == KeyGroup::ApplicationConfig); if (group.isNull() || group.source() != KeyGroup::ApplicationConfig) { qCDebug(LIBKLEO_LOG) << __func__ << "group cannot be removed from application configuration:" << group; return false; } return m_groupConfig->removeGroup(group); } void updateGroupCache() { // Update Group Keys // this is a quick thing as it only involves reading the config // so no need for a job. m_groups.clear(); if (m_groupsEnabled) { readGroupsFromGpgConf(); readGroupsFromGroupsConfig(); } } bool insert(const KeyGroup &group) { Q_ASSERT(!group.isNull()); Q_ASSERT(group.source() == KeyGroup::ApplicationConfig); if (group.isNull() || group.source() != KeyGroup::ApplicationConfig) { qCDebug(LIBKLEO_LOG) << "KeyCache::Private::insert - Invalid group:" << group; return false; } const auto it = std::find_if(m_groups.cbegin(), m_groups.cend(), [group](const auto &g) { return g.source() == group.source() && g.id() == group.id(); }); if (it != m_groups.cend()) { qCDebug(LIBKLEO_LOG) << "KeyCache::Private::insert - Group already present in list of groups:" << group; return false; } const KeyGroup savedGroup = writeGroupToGroupsConfig(group); if (savedGroup.isNull()) { qCDebug(LIBKLEO_LOG) << "KeyCache::Private::insert - Writing group" << group.id() << "to config file failed"; return false; } m_groups.push_back(savedGroup); Q_EMIT q->groupAdded(savedGroup); return true; } bool update(const KeyGroup &group) { Q_ASSERT(!group.isNull()); Q_ASSERT(group.source() == KeyGroup::ApplicationConfig); if (group.isNull() || group.source() != KeyGroup::ApplicationConfig) { qCDebug(LIBKLEO_LOG) << "KeyCache::Private::update - Invalid group:" << group; return false; } const auto it = std::find_if(m_groups.cbegin(), m_groups.cend(), [group](const auto &g) { return g.source() == group.source() && g.id() == group.id(); }); if (it == m_groups.cend()) { qCDebug(LIBKLEO_LOG) << "KeyCache::Private::update - Group not found in list of groups:" << group; return false; } const auto groupIndex = std::distance(m_groups.cbegin(), it); const KeyGroup savedGroup = writeGroupToGroupsConfig(group); if (savedGroup.isNull()) { qCDebug(LIBKLEO_LOG) << "KeyCache::Private::update - Writing group" << group.id() << "to config file failed"; return false; } m_groups[groupIndex] = savedGroup; Q_EMIT q->groupUpdated(savedGroup); return true; } bool remove(const KeyGroup &group) { Q_ASSERT(!group.isNull()); Q_ASSERT(group.source() == KeyGroup::ApplicationConfig); if (group.isNull() || group.source() != KeyGroup::ApplicationConfig) { qCDebug(LIBKLEO_LOG) << "KeyCache::Private::remove - Invalid group:" << group; return false; } const auto it = std::find_if(m_groups.cbegin(), m_groups.cend(), [group](const auto &g) { return g.source() == group.source() && g.id() == group.id(); }); if (it == m_groups.cend()) { qCDebug(LIBKLEO_LOG) << "KeyCache::Private::remove - Group not found in list of groups:" << group; return false; } const bool success = removeGroupFromGroupsConfig(group); if (!success) { qCDebug(LIBKLEO_LOG) << "KeyCache::Private::remove - Removing group" << group.id() << "from config file failed"; return false; } m_groups.erase(it); Q_EMIT q->groupRemoved(group); return true; } private: QPointer m_refreshJob; std::vector> m_fsWatchers; QTimer m_autoKeyListingTimer; int m_refreshInterval; struct By { std::vector fpr, keyid, shortkeyid, chainid; std::vector> email; std::vector subkeyid, keygrip; } by; bool m_initalized; bool m_pgpOnly; bool m_remarks_enabled; bool m_groupsEnabled = false; std::shared_ptr m_groupConfig; std::vector m_groups; }; std::shared_ptr KeyCache::instance() { return mutableInstance(); } std::shared_ptr KeyCache::mutableInstance() { static std::weak_ptr self; try { return std::shared_ptr(self); } catch (const std::bad_weak_ptr &) { const std::shared_ptr s(new KeyCache); self = s; return s; } } KeyCache::KeyCache() : QObject() , d(new Private(this)) { } KeyCache::~KeyCache() { } void KeyCache::setGroupsEnabled(bool enabled) { d->m_groupsEnabled = enabled; if (d->m_initalized) { d->updateGroupCache(); } } void KeyCache::setGroupConfig(const std::shared_ptr &groupConfig) { d->m_groupConfig = groupConfig; } void KeyCache::enableFileSystemWatcher(bool enable) { for (const auto &i : std::as_const(d->m_fsWatchers)) { i->setEnabled(enable); } } void KeyCache::setRefreshInterval(int hours) { d->setRefreshInterval(hours); } int KeyCache::refreshInterval() const { return d->refreshInterval(); } std::shared_ptr KeyCache::suspendAutoRefresh() { return KeyCacheAutoRefreshSuspension::instance(); } void KeyCache::reload(GpgME::Protocol /*proto*/) { if (d->m_refreshJob) { return; } d->updateAutoKeyListingTimer(); enableFileSystemWatcher(false); d->m_refreshJob = new RefreshKeysJob(this); connect(d->m_refreshJob.data(), &RefreshKeysJob::done, this, [this](const GpgME::KeyListResult &r) { d->refreshJobDone(r); }); connect(d->m_refreshJob.data(), &RefreshKeysJob::canceled, this, [this]() { d->m_refreshJob.clear(); }); d->m_refreshJob->start(); } void KeyCache::cancelKeyListing() { if (!d->m_refreshJob) { return; } d->m_refreshJob->cancel(); } void KeyCache::addFileSystemWatcher(const std::shared_ptr &watcher) { if (!watcher) { return; } d->m_fsWatchers.push_back(watcher); connect(watcher.get(), &FileSystemWatcher::directoryChanged, this, [this]() { startKeyListing(); }); connect(watcher.get(), &FileSystemWatcher::fileChanged, this, [this]() { startKeyListing(); }); watcher->setEnabled(d->m_refreshJob.isNull()); } void KeyCache::enableRemarks(bool value) { if (!d->m_remarks_enabled && value) { d->m_remarks_enabled = value; if (d->m_initalized && !d->m_refreshJob) { qCDebug(LIBKLEO_LOG) << "Reloading keycache with remarks enabled"; reload(); } else { connect(d->m_refreshJob.data(), &RefreshKeysJob::done, this, [this](const GpgME::KeyListResult &) { qCDebug(LIBKLEO_LOG) << "Reloading keycache with remarks enabled"; QTimer::singleShot(1s, this, [this]() { reload(); }); }); } } else { d->m_remarks_enabled = value; } } bool KeyCache::remarksEnabled() const { return d->m_remarks_enabled; } void KeyCache::Private::refreshJobDone(const KeyListResult &result) { m_refreshJob.clear(); q->enableFileSystemWatcher(true); m_initalized = true; updateGroupCache(); Q_EMIT q->keyListingDone(result); } const Key &KeyCache::findByFingerprint(const char *fpr) const { const std::vector::const_iterator it = d->find_fpr(fpr); if (it == d->by.fpr.end()) { static const Key null; return null; } else { return *it; } } const Key &KeyCache::findByFingerprint(const std::string &fpr) const { return findByFingerprint(fpr.c_str()); } std::vector KeyCache::findByFingerprint(const std::vector &fprs) const { std::vector keys; keys.reserve(fprs.size()); for (const auto &fpr : fprs) { const Key key = findByFingerprint(fpr.c_str()); if (key.isNull()) { qCDebug(LIBKLEO_LOG) << __func__ << "Ignoring unknown key with fingerprint:" << fpr.c_str(); continue; } keys.push_back(key); } return keys; } std::vector KeyCache::findByEMailAddress(const char *email) const { const auto pair = d->find_email(email); std::vector result; result.reserve(std::distance(pair.first, pair.second)); std::transform(pair.first, pair.second, std::back_inserter(result), [](const std::pair &pair) { return pair.second; }); return result; } std::vector KeyCache::findByEMailAddress(const std::string &email) const { return findByEMailAddress(email.c_str()); } const Key &KeyCache::findByShortKeyID(const char *id) const { const std::vector::const_iterator it = d->find_shortkeyid(id); if (it != d->by.shortkeyid.end()) { return *it; } static const Key null; return null; } const Key &KeyCache::findByShortKeyID(const std::string &id) const { return findByShortKeyID(id.c_str()); } const Key &KeyCache::findByKeyIDOrFingerprint(const char *id) const { { // try by.fpr first: const std::vector::const_iterator it = d->find_fpr(id); if (it != d->by.fpr.end()) { return *it; } } { // try by.keyid next: const std::vector::const_iterator it = d->find_keyid(id); if (it != d->by.keyid.end()) { return *it; } } static const Key null; return null; } const Key &KeyCache::findByKeyIDOrFingerprint(const std::string &id) const { return findByKeyIDOrFingerprint(id.c_str()); } std::vector KeyCache::findByKeyIDOrFingerprint(const std::vector &ids) const { std::vector keyids; std::remove_copy_if(ids.begin(), ids.end(), std::back_inserter(keyids), [](const std::string &str) { return !str.c_str() || !*str.c_str(); }); // this is just case-insensitive string search: std::sort(keyids.begin(), keyids.end(), _detail::ByFingerprint()); std::vector result; result.reserve(keyids.size()); // dups shouldn't happen d->ensureCachePopulated(); kdtools::set_intersection(d->by.fpr.begin(), d->by.fpr.end(), keyids.begin(), keyids.end(), std::back_inserter(result), _detail::ByFingerprint()); if (result.size() < keyids.size()) { // note that By{Fingerprint,KeyID,ShortKeyID} define the same // order for _strings_ kdtools::set_intersection(d->by.keyid.begin(), d->by.keyid.end(), keyids.begin(), keyids.end(), std::back_inserter(result), _detail::ByKeyID()); } // duplicates shouldn't happen, but make sure nonetheless: std::sort(result.begin(), result.end(), _detail::ByFingerprint()); result.erase(std::unique(result.begin(), result.end(), _detail::ByFingerprint()), result.end()); // we skip looking into short key ids here, as it's highly // unlikely they're used for this purpose. We might need to revise // this decision, but only after testing. return result; } const Subkey &KeyCache::findSubkeyByKeyGrip(const char *grip, Protocol protocol) const { static const Subkey null; d->ensureCachePopulated(); const auto range = std::equal_range(d->by.keygrip.begin(), d->by.keygrip.end(), grip, _detail::ByKeyGrip()); if (range.first == range.second) { return null; } else if (protocol == UnknownProtocol) { return *range.first; } else { for (auto it = range.first; it != range.second; ++it) { if (it->parent().protocol() == protocol) { return *it; } } } return null; } const Subkey &KeyCache::findSubkeyByKeyGrip(const std::string &grip, Protocol protocol) const { return findSubkeyByKeyGrip(grip.c_str(), protocol); } std::vector KeyCache::findSubkeysByKeyID(const std::vector &ids) const { std::vector sorted; sorted.reserve(ids.size()); std::remove_copy_if(ids.begin(), ids.end(), std::back_inserter(sorted), [](const std::string &str) { return !str.c_str() || !*str.c_str(); }); std::sort(sorted.begin(), sorted.end(), _detail::ByKeyID()); std::vector result; d->ensureCachePopulated(); kdtools::set_intersection(d->by.subkeyid.begin(), d->by.subkeyid.end(), sorted.begin(), sorted.end(), std::back_inserter(result), _detail::ByKeyID()); return result; } std::vector KeyCache::findRecipients(const DecryptionResult &res) const { std::vector keyids; const auto recipients = res.recipients(); for (const DecryptionResult::Recipient &r : recipients) { if (const char *kid = r.keyID()) { keyids.push_back(kid); } } const std::vector subkeys = findSubkeysByKeyID(keyids); std::vector result; result.reserve(subkeys.size()); std::transform(subkeys.begin(), subkeys.end(), std::back_inserter(result), std::mem_fn(&Subkey::parent)); std::sort(result.begin(), result.end(), _detail::ByFingerprint()); result.erase(std::unique(result.begin(), result.end(), _detail::ByFingerprint()), result.end()); return result; } std::vector KeyCache::findSigners(const VerificationResult &res) const { std::vector fprs; const auto signatures = res.signatures(); for (const Signature &s : signatures) { if (const char *fpr = s.fingerprint()) { fprs.push_back(fpr); } } return findByKeyIDOrFingerprint(fprs); } std::vector KeyCache::findSigningKeysByMailbox(const QString &mb) const { return d->find_mailbox(mb, true); } std::vector KeyCache::findEncryptionKeysByMailbox(const QString &mb) const { return d->find_mailbox(mb, false); } namespace { #define DO(op, meth, meth2) \ if (op key.meth()) { \ } else { \ qDebug("rejecting for signing: %s: %s", #meth2, key.primaryFingerprint()); \ return false; \ } #define ACCEPT(meth) DO(!!, meth, !meth) #define REJECT(meth) DO(!, meth, meth) struct ready_for_signing { bool operator()(const Key &key) const { #if 1 ACCEPT(hasSecret); ACCEPT(canReallySign); REJECT(isRevoked); REJECT(isExpired); REJECT(isDisabled); REJECT(isInvalid); return true; #else return key.hasSecret() && key.canReallySign() && !key.isRevoked() && !key.isExpired() && !key.isDisabled() && !key.isInvalid(); #endif #undef DO } }; #define DO(op, meth, meth2) \ if (op key.meth()) { \ } else { \ qDebug("rejecting for encrypting: %s: %s", #meth2, key.primaryFingerprint()); \ return false; \ } struct ready_for_encryption { bool operator()(const Key &key) const { #if 1 ACCEPT(canEncrypt); REJECT(isRevoked); REJECT(isExpired); REJECT(isDisabled); REJECT(isInvalid); return true; #else return key.canEncrypt() && !key.isRevoked() && !key.isExpired() && !key.isDisabled() && !key.isInvalid(); #endif } #undef DO #undef ACCEPT #undef REJECT }; } std::vector KeyCache::Private::find_mailbox(const QString &email, bool sign) const { if (email.isEmpty()) { return std::vector(); } const auto pair = find_email(email.toUtf8().constData()); std::vector result; result.reserve(std::distance(pair.first, pair.second)); if (sign) { kdtools::copy_2nd_if(pair.first, pair.second, std::back_inserter(result), ready_for_signing()); } else { kdtools::copy_2nd_if(pair.first, pair.second, std::back_inserter(result), ready_for_encryption()); } return result; } std::vector KeyCache::findSubjects(const GpgME::Key &key, Options options) const { return findSubjects(std::vector(1, key), options); } std::vector KeyCache::findSubjects(const std::vector &keys, Options options) const { return findSubjects(keys.begin(), keys.end(), options); } std::vector KeyCache::findSubjects(std::vector::const_iterator first, std::vector::const_iterator last, Options options) const { if (first == last) { return std::vector(); } std::vector result; while (first != last) { const auto pair = d->find_subjects(first->primaryFingerprint()); result.insert(result.end(), pair.first, pair.second); ++first; } std::sort(result.begin(), result.end(), _detail::ByFingerprint()); result.erase(std::unique(result.begin(), result.end(), _detail::ByFingerprint()), result.end()); if (options & RecursiveSearch) { const std::vector furtherSubjects = findSubjects(result, options); std::vector combined; combined.reserve(result.size() + furtherSubjects.size()); std::merge(result.begin(), result.end(), furtherSubjects.begin(), furtherSubjects.end(), std::back_inserter(combined), _detail::ByFingerprint()); combined.erase(std::unique(combined.begin(), combined.end(), _detail::ByFingerprint()), combined.end()); result.swap(combined); } return result; } std::vector KeyCache::findIssuers(const Key &key, Options options) const { std::vector result; if (key.isNull()) { return result; } if (options & IncludeSubject) { result.push_back(key); } if (key.isRoot()) { return result; } const Key &issuer = findByFingerprint(key.chainID()); if (issuer.isNull()) { return result; } result.push_back(issuer); if (!(options & RecursiveSearch)) { return result; } while (true) { const Key &issuer = findByFingerprint(result.back().chainID()); if (issuer.isNull()) { break; } const bool chainAlreadyContainsIssuer = Kleo::contains_if(result, [issuer](const auto &key) { return _detail::ByFingerprint()(issuer, key); }); // we also add the issuer if the chain already contains it, so that // the user can spot the recursion result.push_back(issuer); if (issuer.isRoot() || chainAlreadyContainsIssuer) { break; } } return result; } static std::string email(const UserID &uid) { // Prefer the gnupg normalized one const std::string addr = uid.addrSpec(); if (!addr.empty()) { return addr; } const std::string email = uid.email(); if (email.empty()) { return DN(uid.id())[QStringLiteral("EMAIL")].trimmed().toUtf8().constData(); } if (email[0] == '<' && email[email.size() - 1] == '>') { return email.substr(1, email.size() - 2); } else { return email; } } static std::vector emails(const Key &key) { std::vector emails; const auto userIDs = key.userIDs(); for (const UserID &uid : userIDs) { const std::string e = email(uid); if (!e.empty()) { emails.push_back(e); } } std::sort(emails.begin(), emails.end(), ByEMail()); emails.erase(std::unique(emails.begin(), emails.end(), ByEMail()), emails.end()); return emails; } void KeyCache::remove(const Key &key) { if (key.isNull()) { return; } const char *fpr = key.primaryFingerprint(); if (!fpr) { return; } Q_EMIT aboutToRemove(key); { const auto range = std::equal_range(d->by.fpr.begin(), d->by.fpr.end(), fpr, _detail::ByFingerprint()); d->by.fpr.erase(range.first, range.second); } if (const char *keyid = key.keyID()) { const auto range = std::equal_range(d->by.keyid.begin(), d->by.keyid.end(), keyid, _detail::ByKeyID()); const auto it = std::remove_if(range.first, range.second, [fpr](const GpgME::Key &key) { return _detail::ByFingerprint()(fpr, key); }); d->by.keyid.erase(it, range.second); } if (const char *shortkeyid = key.shortKeyID()) { const auto range = std::equal_range(d->by.shortkeyid.begin(), d->by.shortkeyid.end(), shortkeyid, _detail::ByShortKeyID()); const auto it = std::remove_if(range.first, range.second, [fpr](const GpgME::Key &key) { return _detail::ByFingerprint()(fpr, key); }); d->by.shortkeyid.erase(it, range.second); } if (const char *chainid = key.chainID()) { const auto range = std::equal_range(d->by.chainid.begin(), d->by.chainid.end(), chainid, _detail::ByChainID()); const auto range2 = std::equal_range(range.first, range.second, fpr, _detail::ByFingerprint()); d->by.chainid.erase(range2.first, range2.second); } const auto emailsKey{emails(key)}; for (const std::string &email : emailsKey) { const auto range = std::equal_range(d->by.email.begin(), d->by.email.end(), email, ByEMail()); const auto it = std::remove_if(range.first, range.second, [fpr](const std::pair &pair) { return qstricmp(fpr, pair.second.primaryFingerprint()) == 0; }); d->by.email.erase(it, range.second); } const auto keySubKeys{key.subkeys()}; for (const Subkey &subkey : keySubKeys) { if (const char *keyid = subkey.keyID()) { const auto range = std::equal_range(d->by.subkeyid.begin(), d->by.subkeyid.end(), keyid, _detail::ByKeyID()); const auto it = std::remove_if(range.first, range.second, [fpr](const Subkey &subkey) { return !qstricmp(fpr, subkey.parent().primaryFingerprint()); }); d->by.subkeyid.erase(it, range.second); } if (const char *keygrip = subkey.keyGrip()) { const auto range = std::equal_range(d->by.keygrip.begin(), d->by.keygrip.end(), keygrip, _detail::ByKeyGrip()); const auto it = std::remove_if(range.first, range.second, [fpr](const Subkey &subkey) { return !qstricmp(fpr, subkey.parent().primaryFingerprint()); }); d->by.keygrip.erase(it, range.second); } } } void KeyCache::remove(const std::vector &keys) { for (const Key &key : keys) { remove(key); } } const std::vector &KeyCache::keys() const { d->ensureCachePopulated(); return d->by.fpr; } std::vector KeyCache::secretKeys() const { std::vector keys = this->keys(); keys.erase(std::remove_if(keys.begin(), keys.end(), [](const Key &key) { return !key.hasSecret(); }), keys.end()); return keys; } KeyGroup KeyCache::group(const QString &id) const { KeyGroup result{}; const auto it = std::find_if(std::cbegin(d->m_groups), std::cend(d->m_groups), [id](const auto &g) { return g.id() == id; }); if (it != std::cend(d->m_groups)) { result = *it; } return result; } std::vector KeyCache::groups() const { d->ensureCachePopulated(); return d->m_groups; } std::vector KeyCache::configurableGroups() const { std::vector groups; groups.reserve(d->m_groups.size()); std::copy_if(d->m_groups.cbegin(), d->m_groups.cend(), std::back_inserter(groups), [](const KeyGroup &group) { return group.source() == KeyGroup::ApplicationConfig; }); return groups; } namespace { bool compareById(const KeyGroup &lhs, const KeyGroup &rhs) { return lhs.id() < rhs.id(); } std::vector sortedById(std::vector groups) { std::sort(groups.begin(), groups.end(), &compareById); return groups; } } void KeyCache::saveConfigurableGroups(const std::vector &groups) { const std::vector oldGroups = sortedById(configurableGroups()); const std::vector newGroups = sortedById(groups); { std::vector removedGroups; std::set_difference(oldGroups.begin(), oldGroups.end(), newGroups.begin(), newGroups.end(), std::back_inserter(removedGroups), &compareById); for (const auto &group : std::as_const(removedGroups)) { qCDebug(LIBKLEO_LOG) << "Removing group" << group; d->remove(group); } } { std::vector updatedGroups; std::set_intersection(newGroups.begin(), newGroups.end(), oldGroups.begin(), oldGroups.end(), std::back_inserter(updatedGroups), &compareById); for (const auto &group : std::as_const(updatedGroups)) { qCDebug(LIBKLEO_LOG) << "Updating group" << group; d->update(group); } } { std::vector addedGroups; std::set_difference(newGroups.begin(), newGroups.end(), oldGroups.begin(), oldGroups.end(), std::back_inserter(addedGroups), &compareById); for (const auto &group : std::as_const(addedGroups)) { qCDebug(LIBKLEO_LOG) << "Adding group" << group; d->insert(group); } } Q_EMIT keysMayHaveChanged(); } bool KeyCache::insert(const KeyGroup &group) { if (!d->insert(group)) { return false; } Q_EMIT keysMayHaveChanged(); return true; } bool KeyCache::update(const KeyGroup &group) { if (!d->update(group)) { return false; } Q_EMIT keysMayHaveChanged(); return true; } bool KeyCache::remove(const KeyGroup &group) { if (!d->remove(group)) { return false; } Q_EMIT keysMayHaveChanged(); return true; } void KeyCache::refresh(const std::vector &keys) { // make this better... clear(); insert(keys); } void KeyCache::insert(const Key &key) { insert(std::vector(1, key)); } namespace { template class Op> class T1, template class Op> class T2> struct lexicographically { using result_type = bool; template bool operator()(const U &lhs, const V &rhs) const { return T1()(lhs, rhs) // || (T1()(lhs, rhs) && T2()(lhs, rhs)); } }; } void KeyCache::insert(const std::vector &keys) { // 1. remove those with empty fingerprints: std::vector sorted; sorted.reserve(keys.size()); std::remove_copy_if(keys.begin(), keys.end(), std::back_inserter(sorted), [](const Key &key) { auto fp = key.primaryFingerprint(); return !fp || !*fp; }); Q_FOREACH (const Key &key, sorted) { remove(key); // this is sub-optimal, but makes implementation from here on much easier } // 2. sort by fingerprint: std::sort(sorted.begin(), sorted.end(), _detail::ByFingerprint()); // 2a. insert into fpr index: std::vector by_fpr; by_fpr.reserve(sorted.size() + d->by.fpr.size()); std::merge(sorted.begin(), sorted.end(), d->by.fpr.begin(), d->by.fpr.end(), std::back_inserter(by_fpr), _detail::ByFingerprint()); // 3. build email index: std::vector> pairs; pairs.reserve(sorted.size()); for (const Key &key : std::as_const(sorted)) { const std::vector emails = ::emails(key); for (const std::string &e : emails) { pairs.push_back(std::make_pair(e, key)); } } std::sort(pairs.begin(), pairs.end(), ByEMail()); // 3a. insert into email index: std::vector> by_email; by_email.reserve(pairs.size() + d->by.email.size()); std::merge(pairs.begin(), pairs.end(), d->by.email.begin(), d->by.email.end(), std::back_inserter(by_email), ByEMail()); // 3.5: stable-sort by chain-id (effectively lexicographically) std::stable_sort(sorted.begin(), sorted.end(), _detail::ByChainID()); // 3.5a: insert into chain-id index: std::vector nonroot; nonroot.reserve(sorted.size()); std::vector by_chainid; by_chainid.reserve(sorted.size() + d->by.chainid.size()); std::copy_if(sorted.cbegin(), sorted.cend(), std::back_inserter(nonroot), [](const Key &key) { return !key.isRoot(); }); std::merge(nonroot.cbegin(), nonroot.cend(), d->by.chainid.cbegin(), d->by.chainid.cend(), std::back_inserter(by_chainid), lexicographically<_detail::ByChainID, _detail::ByFingerprint>()); // 4. sort by key id: std::sort(sorted.begin(), sorted.end(), _detail::ByKeyID()); // 4a. insert into keyid index: std::vector by_keyid; by_keyid.reserve(sorted.size() + d->by.keyid.size()); std::merge(sorted.begin(), sorted.end(), d->by.keyid.begin(), d->by.keyid.end(), std::back_inserter(by_keyid), _detail::ByKeyID()); // 5. sort by short key id: std::sort(sorted.begin(), sorted.end(), _detail::ByShortKeyID()); // 5a. insert into short keyid index: std::vector by_shortkeyid; by_shortkeyid.reserve(sorted.size() + d->by.shortkeyid.size()); std::merge(sorted.begin(), sorted.end(), d->by.shortkeyid.begin(), d->by.shortkeyid.end(), std::back_inserter(by_shortkeyid), _detail::ByShortKeyID()); // 6. build subkey ID index: std::vector subkeys; subkeys.reserve(sorted.size()); for (const Key &key : std::as_const(sorted)) { const auto keySubkeys{key.subkeys()}; for (const Subkey &subkey : keySubkeys) { subkeys.push_back(subkey); } } // 6a sort by key id: std::sort(subkeys.begin(), subkeys.end(), _detail::ByKeyID()); // 6b. insert into subkey ID index: std::vector by_subkeyid; by_subkeyid.reserve(subkeys.size() + d->by.subkeyid.size()); std::merge(subkeys.begin(), subkeys.end(), d->by.subkeyid.begin(), d->by.subkeyid.end(), std::back_inserter(by_subkeyid), _detail::ByKeyID()); // 6c. sort by key grip std::sort(subkeys.begin(), subkeys.end(), _detail::ByKeyGrip()); // 6d. insert into subkey keygrip index: std::vector by_keygrip; by_keygrip.reserve(subkeys.size() + d->by.keygrip.size()); std::merge(subkeys.begin(), subkeys.end(), d->by.keygrip.begin(), d->by.keygrip.end(), std::back_inserter(by_keygrip), _detail::ByKeyGrip()); // now commit (well, we already removed keys...) by_fpr.swap(d->by.fpr); by_keyid.swap(d->by.keyid); by_shortkeyid.swap(d->by.shortkeyid); by_email.swap(d->by.email); by_subkeyid.swap(d->by.subkeyid); by_keygrip.swap(d->by.keygrip); by_chainid.swap(d->by.chainid); for (const Key &key : std::as_const(sorted)) { d->m_pgpOnly &= key.protocol() == GpgME::OpenPGP; Q_EMIT added(key); } Q_EMIT keysMayHaveChanged(); } void KeyCache::clear() { d->by = Private::By(); } // // // RefreshKeysJob // // class KeyCache::RefreshKeysJob::Private { RefreshKeysJob *const q; public: Private(KeyCache *cache, RefreshKeysJob *qq); void doStart(); Error startKeyListing(GpgME::Protocol protocol); void listAllKeysJobDone(const KeyListResult &res, const std::vector &nextKeys) { std::vector keys; keys.reserve(m_keys.size() + nextKeys.size()); if (m_keys.empty()) { keys = nextKeys; } else { std::merge(m_keys.begin(), m_keys.end(), nextKeys.begin(), nextKeys.end(), std::back_inserter(keys), _detail::ByFingerprint()); } m_keys.swap(keys); jobDone(res); } void emitDone(const KeyListResult &result); void updateKeyCache(); QPointer m_cache; QVector m_jobsPending; std::vector m_keys; KeyListResult m_mergedResult; bool m_canceled; private: void jobDone(const KeyListResult &res); }; KeyCache::RefreshKeysJob::Private::Private(KeyCache *cache, RefreshKeysJob *qq) : q(qq) , m_cache(cache) , m_canceled(false) { Q_ASSERT(m_cache); } void KeyCache::RefreshKeysJob::Private::jobDone(const KeyListResult &result) { if (m_canceled) { q->deleteLater(); return; } QObject *const sender = q->sender(); if (sender) { sender->disconnect(q); } Q_ASSERT(m_jobsPending.size() > 0); m_jobsPending.removeOne(qobject_cast(sender)); m_mergedResult.mergeWith(result); if (m_jobsPending.size() > 0) { return; } updateKeyCache(); emitDone(m_mergedResult); } void KeyCache::RefreshKeysJob::Private::emitDone(const KeyListResult &res) { q->deleteLater(); Q_EMIT q->done(res); } KeyCache::RefreshKeysJob::RefreshKeysJob(KeyCache *cache, QObject *parent) : QObject(parent) , d(new Private(cache, this)) { } KeyCache::RefreshKeysJob::~RefreshKeysJob() { delete d; } void KeyCache::RefreshKeysJob::start() { QTimer::singleShot(0, this, [this]() { d->doStart(); }); } void KeyCache::RefreshKeysJob::cancel() { d->m_canceled = true; std::for_each(d->m_jobsPending.begin(), d->m_jobsPending.end(), std::mem_fn(&QGpgME::ListAllKeysJob::slotCancel)); Q_EMIT canceled(); } void KeyCache::RefreshKeysJob::Private::doStart() { if (m_canceled) { q->deleteLater(); return; } Q_ASSERT(m_jobsPending.size() == 0); m_mergedResult.mergeWith(KeyListResult(startKeyListing(GpgME::OpenPGP))); m_mergedResult.mergeWith(KeyListResult(startKeyListing(GpgME::CMS))); if (m_jobsPending.size() != 0) { return; } const bool hasError = m_mergedResult.error() || m_mergedResult.error().isCanceled(); emitDone(hasError ? m_mergedResult : KeyListResult(Error(GPG_ERR_UNSUPPORTED_OPERATION))); } void KeyCache::RefreshKeysJob::Private::updateKeyCache() { if (!m_cache || m_canceled) { q->deleteLater(); return; } std::vector cachedKeys = m_cache->initialized() ? m_cache->keys() : std::vector(); std::sort(cachedKeys.begin(), cachedKeys.end(), _detail::ByFingerprint()); std::vector keysToRemove; std::set_difference(cachedKeys.begin(), cachedKeys.end(), m_keys.begin(), m_keys.end(), std::back_inserter(keysToRemove), _detail::ByFingerprint()); m_cache->remove(keysToRemove); m_cache->refresh(m_keys); } Error KeyCache::RefreshKeysJob::Private::startKeyListing(GpgME::Protocol proto) { const auto *const protocol = (proto == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); if (!protocol) { return Error(); } QGpgME::ListAllKeysJob *const job = protocol->listAllKeysJob(/*includeSigs*/ false, /*validate*/ true); if (!job) { return Error(); } #if 0 aheinecke: 2017.01.12: For unknown reasons the new style connect fails at runtime over library borders into QGpgME from the GpgME repo when cross compiled for Windows and default arguments are used in the Signal. This was tested with gcc 4.9 (Mingw 3.0.2) and we could not find an explanation for this. So until this is fixed or we understand the problem we need to use the old style connect for QGpgME signals. The new style connect of the canceled signal right below works fine. connect(job, &QGpgME::ListAllKeysJob::result, q, [this](const GpgME::KeyListResult &res, const std::vector &keys) { listAllKeysJobDone(res, keys); }); #endif connect(job, SIGNAL(result(GpgME::KeyListResult, std::vector)), q, SLOT(listAllKeysJobDone(GpgME::KeyListResult, std::vector))); connect(q, &RefreshKeysJob::canceled, job, &QGpgME::Job::slotCancel); // Only do this for initialized keycaches to avoid huge waits for // signature notations during initial keylisting. if (proto == GpgME::OpenPGP && m_cache->remarksEnabled() && m_cache->initialized()) { auto ctx = QGpgME::Job::context(job); if (ctx) { ctx->addKeyListMode(KeyListMode::Signatures | KeyListMode::SignatureNotations); } } const Error error = job->start(true); if (!error && !error.isCanceled()) { m_jobsPending.push_back(job); } return error; } bool KeyCache::initialized() const { return d->m_initalized; } void KeyCache::Private::ensureCachePopulated() const { if (!m_initalized) { q->startKeyListing(); QEventLoop loop; loop.connect(q, &KeyCache::keyListingDone, &loop, &QEventLoop::quit); qCDebug(LIBKLEO_LOG) << "Waiting for keycache."; loop.exec(); qCDebug(LIBKLEO_LOG) << "Keycache available."; } } bool KeyCache::pgpOnly() const { return d->m_pgpOnly; } static bool keyIsOk(const Key &k) { return !k.isExpired() && !k.isRevoked() && !k.isInvalid() && !k.isDisabled(); } static bool uidIsOk(const UserID &uid) { return keyIsOk(uid.parent()) && !uid.isRevoked() && !uid.isInvalid(); } static bool subkeyIsOk(const Subkey &s) { return !s.isRevoked() && !s.isInvalid() && !s.isDisabled(); } namespace { time_t creationTimeOfNewestSuitableSubKey(const Key &key, KeyCache::KeyUsage usage) { time_t creationTime = 0; for (const Subkey &s : key.subkeys()) { if (!subkeyIsOk(s)) { continue; } if (usage == KeyCache::KeyUsage::Sign && !s.canSign()) { continue; } if (usage == KeyCache::KeyUsage::Encrypt && !s.canEncrypt()) { continue; } if (s.creationTime() > creationTime) { creationTime = s.creationTime(); } } return creationTime; } struct BestMatch { Key key; UserID uid; time_t creationTime = 0; }; } GpgME::Key KeyCache::findBestByMailBox(const char *addr, GpgME::Protocol proto, KeyUsage usage) const { d->ensureCachePopulated(); if (!addr) { return {}; } // support lookup of email addresses enclosed in angle brackets QByteArray address(addr); if (address[0] == '<' && address[address.size() - 1] == '>') { address = address.mid(1, address.size() - 2); } address = address.toLower(); BestMatch best; for (const Key &k : findByEMailAddress(address.constData())) { if (proto != Protocol::UnknownProtocol && k.protocol() != proto) { continue; } if (usage == KeyUsage::Encrypt && !k.canEncrypt()) { continue; } if (usage == KeyUsage::Sign && (!k.canSign() || !k.hasSecret())) { continue; } const time_t creationTime = creationTimeOfNewestSuitableSubKey(k, usage); if (creationTime == 0) { // key does not have a suitable (and usable) subkey continue; } for (const UserID &u : k.userIDs()) { if (QByteArray::fromStdString(u.addrSpec()).toLower() != address) { // user ID does not match the given email address continue; } if (best.uid.isNull()) { // we have found our first candidate best = {k, u, creationTime}; } else if (!uidIsOk(best.uid) && uidIsOk(u)) { // validity of the new key is better best = {k, u, creationTime}; } else if (!k.isExpired() && best.uid.validity() < u.validity()) { // validity of the new key is better best = {k, u, creationTime}; } else if (best.key.isExpired() && !k.isExpired()) { // validity of the new key is better best = {k, u, creationTime}; } else if (best.uid.validity() == u.validity() && uidIsOk(u) && best.creationTime < creationTime) { // both keys/user IDs have same validity, but the new key is newer best = {k, u, creationTime}; } } } return best.key; } namespace { template bool allKeysAllowUsage(const T &keys, KeyCache::KeyUsage usage) { switch (usage) { case KeyCache::KeyUsage::AnyUsage: return true; case KeyCache::KeyUsage::Sign: return std::all_of(std::begin(keys), std::end(keys), std::mem_fn(&Key::canSign)); case KeyCache::KeyUsage::Encrypt: return std::all_of(std::begin(keys), std::end(keys), std::mem_fn(&Key::canEncrypt)); case KeyCache::KeyUsage::Certify: return std::all_of(std::begin(keys), std::end(keys), std::mem_fn(&Key::canCertify)); case KeyCache::KeyUsage::Authenticate: return std::all_of(std::begin(keys), std::end(keys), std::mem_fn(&Key::canAuthenticate)); } qCDebug(LIBKLEO_LOG) << __func__ << "called with invalid usage" << int(usage); return false; } template bool allKeysHaveProtocol(const T &keys, Protocol protocol) { return std::all_of(std::begin(keys), std::end(keys), [protocol](const auto &key) { return key.protocol() == protocol; }); } } KeyGroup KeyCache::findGroup(const QString &name, Protocol protocol, KeyUsage usage) const { d->ensureCachePopulated(); Q_ASSERT(usage == KeyUsage::Sign || usage == KeyUsage::Encrypt); for (const auto &group : std::as_const(d->m_groups)) { if (group.name() == name) { const KeyGroup::Keys &keys = group.keys(); if (allKeysAllowUsage(keys, usage) && (protocol == UnknownProtocol || allKeysHaveProtocol(keys, protocol))) { return group; } } } return {}; } std::vector KeyCache::getGroupKeys(const QString &groupName) const { std::vector result; for (const KeyGroup &g : std::as_const(d->m_groups)) { if (g.name() == groupName) { const KeyGroup::Keys &keys = g.keys(); std::copy(keys.cbegin(), keys.cend(), std::back_inserter(result)); } } _detail::sort_by_fpr(result); _detail::remove_duplicates_by_fpr(result); return result; } void KeyCache::setKeys(const std::vector &keys) { // disable regular key listing and cancel running key listing setRefreshInterval(0); cancelKeyListing(); clear(); insert(keys); d->m_initalized = true; Q_EMIT keyListingDone(KeyListResult()); } void KeyCache::setGroups(const std::vector &groups) { Q_ASSERT(d->m_initalized && "Call setKeys() before setting groups"); d->m_groups = groups; Q_EMIT keysMayHaveChanged(); } #include "moc_keycache.cpp" #include "moc_keycache_p.cpp" diff --git a/src/models/keylistmodel.cpp b/src/models/keylistmodel.cpp index acfc94d2b..9f07e428c 100644 --- a/src/models/keylistmodel.cpp +++ b/src/models/keylistmodel.cpp @@ -1,1596 +1,1598 @@ /* -*- mode: c++; c-basic-offset:4 -*- models/keylistmodel.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "keylistmodel.h" #include "keycache.h" #include "kleo/keyfilter.h" #include "kleo/keyfiltermanager.h" #include "kleo/predicates.h" #include "utils/algorithm.h" #include "utils/formatting.h" #ifdef KLEO_MODEL_TEST #include #endif #include #include #include #include #include #include #include #include #include #ifndef Q_MOC_RUN // QTBUG-22829 #include #include #endif #include #include #include #include using namespace GpgME; using namespace Kleo; using namespace Kleo::KeyList; Q_DECLARE_METATYPE(GpgME::Key) Q_DECLARE_METATYPE(KeyGroup) class AbstractKeyListModel::Private { AbstractKeyListModel *const q; public: explicit Private(AbstractKeyListModel *qq); void updateFromKeyCache(); QString getEMail(const Key &key) const; public: int m_toolTipOptions = Formatting::Validity; mutable QHash prettyEMailCache; mutable QHash remarksCache; bool m_useKeyCache = false; bool m_modelResetInProgress = false; KeyList::Options m_keyListOptions = AllKeys; std::vector m_remarkKeys; }; AbstractKeyListModel::Private::Private(Kleo::AbstractKeyListModel *qq) : q(qq) { } void AbstractKeyListModel::Private::updateFromKeyCache() { if (m_useKeyCache) { q->setKeys(m_keyListOptions == SecretKeysOnly ? KeyCache::instance()->secretKeys() : KeyCache::instance()->keys()); if (m_keyListOptions == IncludeGroups) { q->setGroups(KeyCache::instance()->groups()); } } } QString AbstractKeyListModel::Private::getEMail(const Key &key) const { QString email; if (const auto fpr = key.primaryFingerprint()) { const auto it = prettyEMailCache.constFind(fpr); if (it != prettyEMailCache.constEnd()) { email = *it; } else { email = Formatting::prettyEMail(key); prettyEMailCache[fpr] = email; } } return email; } AbstractKeyListModel::AbstractKeyListModel(QObject *p) : QAbstractItemModel(p) , KeyListModelInterface() , d(new Private(this)) { connect(this, &QAbstractItemModel::modelAboutToBeReset, this, [this]() { d->m_modelResetInProgress = true; }); connect(this, &QAbstractItemModel::modelReset, this, [this]() { d->m_modelResetInProgress = false; }); } AbstractKeyListModel::~AbstractKeyListModel() { } void AbstractKeyListModel::setToolTipOptions(int opts) { d->m_toolTipOptions = opts; } int AbstractKeyListModel::toolTipOptions() const { return d->m_toolTipOptions; } void AbstractKeyListModel::setRemarkKeys(const std::vector &keys) { d->m_remarkKeys = keys; } std::vector AbstractKeyListModel::remarkKeys() const { return d->m_remarkKeys; } Key AbstractKeyListModel::key(const QModelIndex &idx) const { Key key = Key::null; if (idx.isValid()) { key = doMapToKey(idx); } return key; } std::vector AbstractKeyListModel::keys(const QList &indexes) const { std::vector result; result.reserve(indexes.size()); std::transform(indexes.begin(), // indexes.end(), std::back_inserter(result), [this](const QModelIndex &idx) { return this->key(idx); }); result.erase(std::remove_if(result.begin(), result.end(), std::mem_fn(&GpgME::Key::isNull)), result.end()); _detail::remove_duplicates_by_fpr(result); return result; } KeyGroup AbstractKeyListModel::group(const QModelIndex &idx) const { if (idx.isValid()) { return doMapToGroup(idx); } else { return KeyGroup(); } } QModelIndex AbstractKeyListModel::index(const Key &key) const { return index(key, 0); } QModelIndex AbstractKeyListModel::index(const Key &key, int col) const { if (key.isNull() || col < 0 || col >= NumColumns) { return {}; } else { return doMapFromKey(key, col); } } QList AbstractKeyListModel::indexes(const std::vector &keys) const { QList result; result.reserve(keys.size()); std::transform(keys.begin(), // keys.end(), std::back_inserter(result), [this](const Key &key) { return this->index(key); }); return result; } QModelIndex AbstractKeyListModel::index(const KeyGroup &group) const { return index(group, 0); } QModelIndex AbstractKeyListModel::index(const KeyGroup &group, int col) const { if (group.isNull() || col < 0 || col >= NumColumns) { return {}; } else { return doMapFromGroup(group, col); } } void AbstractKeyListModel::setKeys(const std::vector &keys) { beginResetModel(); clear(Keys); addKeys(keys); endResetModel(); } QModelIndex AbstractKeyListModel::addKey(const Key &key) { const std::vector vec(1, key); const QList l = doAddKeys(vec); return l.empty() ? QModelIndex() : l.front(); } void AbstractKeyListModel::removeKey(const Key &key) { if (key.isNull()) { return; } doRemoveKey(key); d->prettyEMailCache.remove(key.primaryFingerprint()); d->remarksCache.remove(key.primaryFingerprint()); } QList AbstractKeyListModel::addKeys(const std::vector &keys) { std::vector sorted; sorted.reserve(keys.size()); std::remove_copy_if(keys.begin(), keys.end(), std::back_inserter(sorted), std::mem_fn(&Key::isNull)); std::sort(sorted.begin(), sorted.end(), _detail::ByFingerprint()); return doAddKeys(sorted); } void AbstractKeyListModel::setGroups(const std::vector &groups) { beginResetModel(); clear(Groups); doSetGroups(groups); endResetModel(); } QModelIndex AbstractKeyListModel::addGroup(const KeyGroup &group) { if (group.isNull()) { return QModelIndex(); } return doAddGroup(group); } bool AbstractKeyListModel::removeGroup(const KeyGroup &group) { if (group.isNull()) { return false; } return doRemoveGroup(group); } void AbstractKeyListModel::clear(ItemTypes types) { const bool inReset = modelResetInProgress(); if (!inReset) { beginResetModel(); } doClear(types); if (types & Keys) { d->prettyEMailCache.clear(); d->remarksCache.clear(); } if (!inReset) { endResetModel(); } } int AbstractKeyListModel::columnCount(const QModelIndex &) const { return NumColumns; } QVariant AbstractKeyListModel::headerData(int section, Qt::Orientation o, int role) const { if (o == Qt::Horizontal) { if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::ToolTipRole) { switch (section) { case PrettyName: return i18n("Name"); case PrettyEMail: return i18n("E-Mail"); case Validity: return i18n("User-IDs"); case ValidFrom: return i18n("Valid From"); case ValidUntil: return i18n("Valid Until"); case TechnicalDetails: return i18n("Protocol"); case ShortKeyID: return i18n("Key-ID"); case KeyID: return i18n("Key-ID"); case Fingerprint: return i18n("Fingerprint"); case Issuer: return i18n("Issuer"); case SerialNumber: return i18n("Serial Number"); case Origin: return i18n("Origin"); case LastUpdate: return i18n("Last Update"); case OwnerTrust: return i18n("Certification Trust"); case Remarks: return i18n("Tags"); case NumColumns:; } } } return QVariant(); } static QVariant returnIfValid(const QColor &t) { if (t.isValid()) { return t; } else { return QVariant(); } } static QVariant returnIfValid(const QIcon &t) { if (!t.isNull()) { return t; } else { return QVariant(); } } QVariant AbstractKeyListModel::data(const QModelIndex &index, int role) const { const Key key = this->key(index); if (!key.isNull()) { return data(key, index.column(), role); } const KeyGroup group = this->group(index); if (!group.isNull()) { return data(group, index.column(), role); } return QVariant(); } QVariant AbstractKeyListModel::data(const Key &key, int column, int role) const { if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::AccessibleTextRole) { switch (column) { case PrettyName: { const auto name = Formatting::prettyName(key); if (role == Qt::AccessibleTextRole) { return name.isEmpty() ? i18nc("text for screen readers for an empty name", "no name") : name; } return name; } case PrettyEMail: { const auto email = d->getEMail(key); if (role == Qt::AccessibleTextRole) { return email.isEmpty() ? i18nc("text for screen readers for an empty email address", "no email") : email; } return email; } case Validity: return Formatting::complianceStringShort(key); case ValidFrom: if (role == Qt::EditRole) { return Formatting::creationDate(key); } else if (role == Qt::AccessibleTextRole) { return Formatting::accessibleCreationDate(key); } else { return Formatting::creationDateString(key); } case ValidUntil: if (role == Qt::EditRole) { return Formatting::expirationDate(key); } else if (role == Qt::AccessibleTextRole) { return Formatting::accessibleExpirationDate(key); } else { return Formatting::expirationDateString(key); } case TechnicalDetails: return Formatting::type(key); case ShortKeyID: if (role == Qt::AccessibleTextRole) { return Formatting::accessibleHexID(key.shortKeyID()); } else { return Formatting::prettyID(key.shortKeyID()); } case KeyID: if (role == Qt::AccessibleTextRole) { return Formatting::accessibleHexID(key.keyID()); } else { return Formatting::prettyID(key.keyID()); } case Summary: return Formatting::summaryLine(key); case Fingerprint: if (role == Qt::AccessibleTextRole) { return Formatting::accessibleHexID(key.primaryFingerprint()); } else { return Formatting::prettyID(key.primaryFingerprint()); } case Issuer: return QString::fromUtf8(key.issuerName()); case Origin: return Formatting::origin(key.origin()); case LastUpdate: if (role == Qt::AccessibleTextRole) { return Formatting::accessibleDate(key.lastUpdate()); } else { return Formatting::dateString(key.lastUpdate()); } case SerialNumber: return QString::fromUtf8(key.issuerSerial()); case OwnerTrust: return Formatting::ownerTrustShort(key.ownerTrust()); case Remarks: { const char *const fpr = key.primaryFingerprint(); if (fpr && key.protocol() == GpgME::OpenPGP && key.numUserIDs() && d->m_remarkKeys.size()) { if (!(key.keyListMode() & GpgME::SignatureNotations)) { return i18n("Loading..."); } const QHash::const_iterator it = d->remarksCache.constFind(fpr); if (it != d->remarksCache.constEnd()) { return *it; } else { GpgME::Error err; const auto remarks = key.userID(0).remarks(d->m_remarkKeys, err); if (remarks.size() == 1) { const auto remark = QString::fromStdString(remarks[0]); return d->remarksCache[fpr] = remark; } else { QStringList remarkList; remarkList.reserve(remarks.size()); for (const auto &rem : remarks) { remarkList << QString::fromStdString(rem); } const auto remark = remarkList.join(QStringLiteral("; ")); return d->remarksCache[fpr] = remark; } } } else { return QVariant(); } } return QVariant(); case NumColumns: break; } } else if (role == Qt::ToolTipRole) { return Formatting::toolTip(key, toolTipOptions()); } else if (role == Qt::FontRole) { return KeyFilterManager::instance()->font(key, (column == ShortKeyID || column == KeyID || column == Fingerprint) ? QFont(QStringLiteral("monospace")) : QFont()); } else if (role == Qt::DecorationRole) { return column == Icon ? returnIfValid(KeyFilterManager::instance()->icon(key)) : QVariant(); } else if (role == Qt::BackgroundRole) { return returnIfValid(KeyFilterManager::instance()->bgColor(key)); } else if (role == Qt::ForegroundRole) { return returnIfValid(KeyFilterManager::instance()->fgColor(key)); } else if (role == FingerprintRole) { return QString::fromLatin1(key.primaryFingerprint()); } else if (role == KeyRole) { return QVariant::fromValue(key); } return QVariant(); } QVariant AbstractKeyListModel::data(const KeyGroup &group, int column, int role) const { if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::AccessibleTextRole) { switch (column) { case PrettyName: return group.name(); case Validity: return Formatting::complianceStringShort(group); case TechnicalDetails: return Formatting::type(group); case Summary: return Formatting::summaryLine(group); // used for filtering case PrettyEMail: case ValidFrom: case ValidUntil: case ShortKeyID: case KeyID: case Fingerprint: case Issuer: case Origin: case LastUpdate: case SerialNumber: case OwnerTrust: case Remarks: if (role == Qt::AccessibleTextRole) { return i18nc("text for screen readers", "not applicable"); } break; case NumColumns: break; } } else if (role == Qt::ToolTipRole) { return Formatting::toolTip(group, toolTipOptions()); } else if (role == Qt::FontRole) { return QFont(); } else if (role == Qt::DecorationRole) { return column == Icon ? QIcon::fromTheme(QStringLiteral("group")) : QVariant(); } else if (role == Qt::BackgroundRole) { } else if (role == Qt::ForegroundRole) { } else if (role == GroupRole) { return QVariant::fromValue(group); } return QVariant(); } bool AbstractKeyListModel::setData(const QModelIndex &index, const QVariant &value, int role) { Q_UNUSED(role) Q_ASSERT(value.canConvert()); if (value.canConvert()) { const KeyGroup group = value.value(); return doSetGroupData(index, group); } return false; } bool AbstractKeyListModel::modelResetInProgress() { return d->m_modelResetInProgress; } namespace { template class TableModelMixin : public Base { public: explicit TableModelMixin(QObject *p = nullptr) : Base(p) { } ~TableModelMixin() override { } using Base::index; QModelIndex index(int row, int column, const QModelIndex &pidx = QModelIndex()) const override { return this->hasIndex(row, column, pidx) ? this->createIndex(row, column, nullptr) : QModelIndex(); } private: QModelIndex parent(const QModelIndex &) const override { return QModelIndex(); } bool hasChildren(const QModelIndex &pidx) const override { return (pidx.model() == this || !pidx.isValid()) && this->rowCount(pidx) > 0 && this->columnCount(pidx) > 0; } }; class FlatKeyListModel #ifndef Q_MOC_RUN : public TableModelMixin #else : public AbstractKeyListModel #endif { Q_OBJECT public: explicit FlatKeyListModel(QObject *parent = nullptr); ~FlatKeyListModel() override; int rowCount(const QModelIndex &pidx) const override { return pidx.isValid() ? 0 : mKeysByFingerprint.size() + mGroups.size(); } private: Key doMapToKey(const QModelIndex &index) const override; QModelIndex doMapFromKey(const Key &key, int col) const override; QList doAddKeys(const std::vector &keys) override; void doRemoveKey(const Key &key) override; KeyGroup doMapToGroup(const QModelIndex &index) const override; QModelIndex doMapFromGroup(const KeyGroup &group, int column) const override; void doSetGroups(const std::vector &groups) override; QModelIndex doAddGroup(const KeyGroup &group) override; bool doSetGroupData(const QModelIndex &index, const KeyGroup &group) override; bool doRemoveGroup(const KeyGroup &group) override; void doClear(ItemTypes types) override { if (types & Keys) { mKeysByFingerprint.clear(); } if (types & Groups) { mGroups.clear(); } } int firstGroupRow() const { return mKeysByFingerprint.size(); } int lastGroupRow() const { return mKeysByFingerprint.size() + mGroups.size() - 1; } int groupIndex(const QModelIndex &index) const { if (!index.isValid() || index.row() < firstGroupRow() || index.row() > lastGroupRow() || index.column() >= NumColumns) { return -1; } return index.row() - firstGroupRow(); } private: std::vector mKeysByFingerprint; std::vector mGroups; }; class HierarchicalKeyListModel : public AbstractKeyListModel { Q_OBJECT public: explicit HierarchicalKeyListModel(QObject *parent = nullptr); ~HierarchicalKeyListModel() override; int rowCount(const QModelIndex &pidx) const override; using AbstractKeyListModel::index; QModelIndex index(int row, int col, const QModelIndex &pidx) const override; QModelIndex parent(const QModelIndex &idx) const override; bool hasChildren(const QModelIndex &pidx) const override { return rowCount(pidx) > 0; } private: Key doMapToKey(const QModelIndex &index) const override; QModelIndex doMapFromKey(const Key &key, int col) const override; QList doAddKeys(const std::vector &keys) override; void doRemoveKey(const Key &key) override; KeyGroup doMapToGroup(const QModelIndex &index) const override; QModelIndex doMapFromGroup(const KeyGroup &group, int column) const override; void doSetGroups(const std::vector &groups) override; QModelIndex doAddGroup(const KeyGroup &group) override; bool doSetGroupData(const QModelIndex &index, const KeyGroup &group) override; bool doRemoveGroup(const KeyGroup &group) override; void doClear(ItemTypes types) override; int firstGroupRow() const { return mTopLevels.size(); } int lastGroupRow() const { return mTopLevels.size() + mGroups.size() - 1; } int groupIndex(const QModelIndex &index) const { if (!index.isValid() || index.row() < firstGroupRow() || index.row() > lastGroupRow() || index.column() >= NumColumns) { return -1; } return index.row() - firstGroupRow(); } private: void addTopLevelKey(const Key &key); void addKeyWithParent(const char *issuer_fpr, const Key &key); void addKeyWithoutParent(const char *issuer_fpr, const Key &key); private: typedef std::map> Map; std::vector mKeysByFingerprint; // all keys Map mKeysByExistingParent, mKeysByNonExistingParent; // parent->child map std::vector mTopLevels; // all roots + parent-less std::vector mGroups; }; class Issuers { Issuers() { } public: static Issuers *instance() { static auto self = std::unique_ptr{new Issuers{}}; return self.get(); } const char *cleanChainID(const Key &key) const { const char *chainID = ""; if (!key.isRoot()) { const char *const chid = key.chainID(); if (chid && mKeysWithMaskedIssuer.find(key) == std::end(mKeysWithMaskedIssuer)) { chainID = chid; } } return chainID; } void maskIssuerOfKey(const Key &key) { mKeysWithMaskedIssuer.insert(key); } void clear() { mKeysWithMaskedIssuer.clear(); } private: std::set> mKeysWithMaskedIssuer; }; static const char *cleanChainID(const Key &key) { return Issuers::instance()->cleanChainID(key); } } FlatKeyListModel::FlatKeyListModel(QObject *p) : TableModelMixin(p) { } FlatKeyListModel::~FlatKeyListModel() { } Key FlatKeyListModel::doMapToKey(const QModelIndex &idx) const { Q_ASSERT(idx.isValid()); if (static_cast(idx.row()) < mKeysByFingerprint.size() && idx.column() < NumColumns) { return mKeysByFingerprint[idx.row()]; } else { return Key::null; } } QModelIndex FlatKeyListModel::doMapFromKey(const Key &key, int col) const { Q_ASSERT(!key.isNull()); const std::vector::const_iterator it = std::lower_bound(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), key, _detail::ByFingerprint()); if (it == mKeysByFingerprint.end() || !_detail::ByFingerprint()(*it, key)) { return {}; } else { return createIndex(it - mKeysByFingerprint.begin(), col); } } QList FlatKeyListModel::doAddKeys(const std::vector &keys) { Q_ASSERT(std::is_sorted(keys.begin(), keys.end(), _detail::ByFingerprint())); if (keys.empty()) { return QList(); } for (auto it = keys.begin(), end = keys.end(); it != end; ++it) { // find an insertion point: const std::vector::iterator pos = std::upper_bound(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), *it, _detail::ByFingerprint()); const unsigned int idx = std::distance(mKeysByFingerprint.begin(), pos); if (idx > 0 && qstrcmp(mKeysByFingerprint[idx - 1].primaryFingerprint(), it->primaryFingerprint()) == 0) { // key existed before - replace with new one: mKeysByFingerprint[idx - 1] = *it; if (!modelResetInProgress()) { Q_EMIT dataChanged(createIndex(idx - 1, 0), createIndex(idx - 1, NumColumns - 1)); } } else { // new key - insert: if (!modelResetInProgress()) { beginInsertRows(QModelIndex(), idx, idx); } mKeysByFingerprint.insert(pos, *it); if (!modelResetInProgress()) { endInsertRows(); } } } return indexes(keys); } void FlatKeyListModel::doRemoveKey(const Key &key) { const std::vector::iterator it = Kleo::binary_find(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), key, _detail::ByFingerprint()); if (it == mKeysByFingerprint.end()) { return; } const unsigned int row = std::distance(mKeysByFingerprint.begin(), it); if (!modelResetInProgress()) { beginRemoveRows(QModelIndex(), row, row); } mKeysByFingerprint.erase(it); if (!modelResetInProgress()) { endRemoveRows(); } } KeyGroup FlatKeyListModel::doMapToGroup(const QModelIndex &idx) const { Q_ASSERT(idx.isValid()); if (static_cast(idx.row()) >= mKeysByFingerprint.size() && static_cast(idx.row()) < mKeysByFingerprint.size() + mGroups.size() && idx.column() < NumColumns) { return mGroups[idx.row() - mKeysByFingerprint.size()]; } else { return KeyGroup(); } } QModelIndex FlatKeyListModel::doMapFromGroup(const KeyGroup &group, int column) const { Q_ASSERT(!group.isNull()); const auto it = std::find_if(mGroups.cbegin(), mGroups.cend(), [group](const KeyGroup &g) { return g.source() == group.source() && g.id() == group.id(); }); if (it == mGroups.cend()) { return QModelIndex(); } else { return createIndex(it - mGroups.cbegin() + mKeysByFingerprint.size(), column); } } void FlatKeyListModel::doSetGroups(const std::vector &groups) { Q_ASSERT(mGroups.empty()); // ensure that groups have been cleared const int first = mKeysByFingerprint.size(); const int last = first + groups.size() - 1; if (!modelResetInProgress()) { beginInsertRows(QModelIndex(), first, last); } mGroups = groups; if (!modelResetInProgress()) { endInsertRows(); } } QModelIndex FlatKeyListModel::doAddGroup(const KeyGroup &group) { const int newRow = lastGroupRow() + 1; if (!modelResetInProgress()) { beginInsertRows(QModelIndex(), newRow, newRow); } mGroups.push_back(group); if (!modelResetInProgress()) { endInsertRows(); } return createIndex(newRow, 0); } bool FlatKeyListModel::doSetGroupData(const QModelIndex &index, const KeyGroup &group) { if (group.isNull()) { return false; } const int groupIndex = this->groupIndex(index); if (groupIndex == -1) { return false; } mGroups[groupIndex] = group; if (!modelResetInProgress()) { Q_EMIT dataChanged(createIndex(index.row(), 0), createIndex(index.row(), NumColumns - 1)); } return true; } bool FlatKeyListModel::doRemoveGroup(const KeyGroup &group) { const QModelIndex modelIndex = doMapFromGroup(group, 0); if (!modelIndex.isValid()) { return false; } const int groupIndex = this->groupIndex(modelIndex); Q_ASSERT(groupIndex != -1); if (groupIndex == -1) { return false; } if (!modelResetInProgress()) { beginRemoveRows(QModelIndex(), modelIndex.row(), modelIndex.row()); } mGroups.erase(mGroups.begin() + groupIndex); if (!modelResetInProgress()) { endRemoveRows(); } return true; } HierarchicalKeyListModel::HierarchicalKeyListModel(QObject *p) : AbstractKeyListModel(p) , mKeysByFingerprint() , mKeysByExistingParent() , mKeysByNonExistingParent() , mTopLevels() { } HierarchicalKeyListModel::~HierarchicalKeyListModel() { } int HierarchicalKeyListModel::rowCount(const QModelIndex &pidx) const { // toplevel item: if (!pidx.isValid()) { return mTopLevels.size() + mGroups.size(); } if (pidx.column() != 0) { return 0; } // non-toplevel item - find the number of subjects for this issuer: const Key issuer = this->key(pidx); const char *const fpr = issuer.primaryFingerprint(); if (!fpr || !*fpr) { return 0; } const Map::const_iterator it = mKeysByExistingParent.find(fpr); if (it == mKeysByExistingParent.end()) { return 0; } return it->second.size(); } QModelIndex HierarchicalKeyListModel::index(int row, int col, const QModelIndex &pidx) const { if (row < 0 || col < 0 || col >= NumColumns) { return {}; } // toplevel item: if (!pidx.isValid()) { if (static_cast(row) < mTopLevels.size()) { return index(mTopLevels[row], col); } else if (static_cast(row) < mTopLevels.size() + mGroups.size()) { return index(mGroups[row - mTopLevels.size()], col); } else { return QModelIndex(); } } // non-toplevel item - find the row'th subject of this key: const Key issuer = this->key(pidx); const char *const fpr = issuer.primaryFingerprint(); if (!fpr || !*fpr) { return QModelIndex(); } const Map::const_iterator it = mKeysByExistingParent.find(fpr); if (it == mKeysByExistingParent.end() || static_cast(row) >= it->second.size()) { return QModelIndex(); } return index(it->second[row], col); } QModelIndex HierarchicalKeyListModel::parent(const QModelIndex &idx) const { const Key key = this->key(idx); if (key.isNull() || key.isRoot()) { return {}; } const std::vector::const_iterator it = Kleo::binary_find(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), cleanChainID(key), _detail::ByFingerprint()); return it != mKeysByFingerprint.end() ? index(*it) : QModelIndex(); } Key HierarchicalKeyListModel::doMapToKey(const QModelIndex &idx) const { Key key = Key::null; if (idx.isValid()) { const char *const issuer_fpr = static_cast(idx.internalPointer()); if (!issuer_fpr || !*issuer_fpr) { // top-level: if (static_cast(idx.row()) < mTopLevels.size()) { key = mTopLevels[idx.row()]; } } else { // non-toplevel: const Map::const_iterator it = mKeysByExistingParent.find(issuer_fpr); if (it != mKeysByExistingParent.end() && static_cast(idx.row()) < it->second.size()) { key = it->second[idx.row()]; } } } return key; } QModelIndex HierarchicalKeyListModel::doMapFromKey(const Key &key, int col) const { if (key.isNull()) { return {}; } const char *issuer_fpr = cleanChainID(key); // we need to look in the toplevels list,... const std::vector *v = &mTopLevels; if (issuer_fpr && *issuer_fpr) { const std::map>::const_iterator it = mKeysByExistingParent.find(issuer_fpr); // ...unless we find an existing parent: if (it != mKeysByExistingParent.end()) { v = &it->second; } else { issuer_fpr = nullptr; // force internalPointer to zero for toplevels } } const std::vector::const_iterator it = std::lower_bound(v->begin(), v->end(), key, _detail::ByFingerprint()); if (it == v->end() || !_detail::ByFingerprint()(*it, key)) { return QModelIndex(); } const unsigned int row = std::distance(v->begin(), it); return createIndex(row, col, const_cast(issuer_fpr)); } void HierarchicalKeyListModel::addKeyWithParent(const char *issuer_fpr, const Key &key) { Q_ASSERT(issuer_fpr); Q_ASSERT(*issuer_fpr); Q_ASSERT(!key.isNull()); std::vector &subjects = mKeysByExistingParent[issuer_fpr]; // find insertion point: const std::vector::iterator it = std::lower_bound(subjects.begin(), subjects.end(), key, _detail::ByFingerprint()); const int row = std::distance(subjects.begin(), it); if (it != subjects.end() && qstricmp(it->primaryFingerprint(), key.primaryFingerprint()) == 0) { // exists -> replace *it = key; if (!modelResetInProgress()) { Q_EMIT dataChanged(createIndex(row, 0, const_cast(issuer_fpr)), createIndex(row, NumColumns - 1, const_cast(issuer_fpr))); } } else { // doesn't exist -> insert const std::vector::const_iterator pos = Kleo::binary_find(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), issuer_fpr, _detail::ByFingerprint()); Q_ASSERT(pos != mKeysByFingerprint.end()); if (!modelResetInProgress()) { beginInsertRows(index(*pos), row, row); } subjects.insert(it, key); if (!modelResetInProgress()) { endInsertRows(); } } } void HierarchicalKeyListModel::addKeyWithoutParent(const char *issuer_fpr, const Key &key) { Q_ASSERT(issuer_fpr); Q_ASSERT(*issuer_fpr); Q_ASSERT(!key.isNull()); std::vector &subjects = mKeysByNonExistingParent[issuer_fpr]; // find insertion point: const std::vector::iterator it = std::lower_bound(subjects.begin(), subjects.end(), key, _detail::ByFingerprint()); if (it != subjects.end() && qstricmp(it->primaryFingerprint(), key.primaryFingerprint()) == 0) { // exists -> replace *it = key; } else { // doesn't exist -> insert subjects.insert(it, key); } addTopLevelKey(key); } void HierarchicalKeyListModel::addTopLevelKey(const Key &key) { // find insertion point: const std::vector::iterator it = std::lower_bound(mTopLevels.begin(), mTopLevels.end(), key, _detail::ByFingerprint()); const int row = std::distance(mTopLevels.begin(), it); if (it != mTopLevels.end() && qstricmp(it->primaryFingerprint(), key.primaryFingerprint()) == 0) { // exists -> replace *it = key; if (!modelResetInProgress()) { Q_EMIT dataChanged(createIndex(row, 0), createIndex(row, NumColumns - 1)); } } else { // doesn't exist -> insert if (!modelResetInProgress()) { beginInsertRows(QModelIndex(), row, row); } mTopLevels.insert(it, key); if (!modelResetInProgress()) { endInsertRows(); } } } namespace { // based on https://www.boost.org/doc/libs/1_77_0/libs/graph/doc/file_dependency_example.html#sec:cycles struct cycle_detector : public boost::dfs_visitor<> { cycle_detector(bool &has_cycle) : _has_cycle{has_cycle} { } template void back_edge(Edge, Graph &) { _has_cycle = true; } private: bool &_has_cycle; }; static bool graph_has_cycle(const boost::adjacency_list<> &graph) { bool cycle_found = false; cycle_detector vis{cycle_found}; boost::depth_first_search(graph, visitor(vis)); return cycle_found; } static void find_keys_causing_cycles_and_mask_their_issuers(const std::vector &keys) { boost::adjacency_list<> graph{keys.size()}; for (unsigned int i = 0, end = keys.size(); i != end; ++i) { const auto &key = keys[i]; const char *const issuer_fpr = cleanChainID(key); if (!issuer_fpr || !*issuer_fpr) { continue; } const std::vector::const_iterator it = Kleo::binary_find(keys.begin(), keys.end(), issuer_fpr, _detail::ByFingerprint()); if (it == keys.end()) { continue; } const auto j = std::distance(keys.begin(), it); const auto edge = boost::add_edge(i, j, graph).first; if (graph_has_cycle(graph)) { Issuers::instance()->maskIssuerOfKey(key); boost::remove_edge(edge, graph); } } } static auto build_key_graph(const std::vector &keys) { boost::adjacency_list<> graph(keys.size()); // add edges from children to parents: for (unsigned int i = 0, end = keys.size(); i != end; ++i) { const char *const issuer_fpr = cleanChainID(keys[i]); if (!issuer_fpr || !*issuer_fpr) { continue; } const std::vector::const_iterator it = Kleo::binary_find(keys.begin(), keys.end(), issuer_fpr, _detail::ByFingerprint()); if (it == keys.end()) { continue; } const auto j = std::distance(keys.begin(), it); add_edge(i, j, graph); } return graph; } // sorts 'keys' such that parent always come before their children: static std::vector topological_sort(const std::vector &keys) { const auto graph = build_key_graph(keys); std::vector order; order.reserve(keys.size()); topological_sort(graph, std::back_inserter(order)); Q_ASSERT(order.size() == keys.size()); std::vector result; result.reserve(keys.size()); for (int i : std::as_const(order)) { result.push_back(keys[i]); } return result; } } QList HierarchicalKeyListModel::doAddKeys(const std::vector &keys) { Q_ASSERT(std::is_sorted(keys.begin(), keys.end(), _detail::ByFingerprint())); if (keys.empty()) { return QList(); } const std::vector oldKeys = mKeysByFingerprint; std::vector merged; merged.reserve(keys.size() + mKeysByFingerprint.size()); std::set_union(keys.begin(), keys.end(), mKeysByFingerprint.begin(), mKeysByFingerprint.end(), std::back_inserter(merged), _detail::ByFingerprint()); mKeysByFingerprint = merged; if (graph_has_cycle(build_key_graph(mKeysByFingerprint))) { find_keys_causing_cycles_and_mask_their_issuers(mKeysByFingerprint); } std::set> changedParents; const auto topologicalSortedList = topological_sort(keys); for (const Key &key : topologicalSortedList) { // check to see whether this key is a parent for a previously parent-less group: const char *const fpr = key.primaryFingerprint(); if (!fpr || !*fpr) { continue; } const bool keyAlreadyExisted = std::binary_search(oldKeys.begin(), oldKeys.end(), key, _detail::ByFingerprint()); const Map::iterator it = mKeysByNonExistingParent.find(fpr); const std::vector children = it != mKeysByNonExistingParent.end() ? it->second : std::vector(); if (it != mKeysByNonExistingParent.end()) { mKeysByNonExistingParent.erase(it); } // Step 1: For new keys, remove children from toplevel: if (!keyAlreadyExisted) { auto last = mTopLevels.begin(); auto lastFP = mKeysByFingerprint.begin(); for (const Key &k : children) { last = Kleo::binary_find(last, mTopLevels.end(), k, _detail::ByFingerprint()); Q_ASSERT(last != mTopLevels.end()); const int row = std::distance(mTopLevels.begin(), last); lastFP = Kleo::binary_find(lastFP, mKeysByFingerprint.end(), k, _detail::ByFingerprint()); Q_ASSERT(lastFP != mKeysByFingerprint.end()); Q_EMIT rowAboutToBeMoved(QModelIndex(), row); if (!modelResetInProgress()) { beginRemoveRows(QModelIndex(), row, row); } last = mTopLevels.erase(last); lastFP = mKeysByFingerprint.erase(lastFP); if (!modelResetInProgress()) { endRemoveRows(); } } } // Step 2: add/update key const char *const issuer_fpr = cleanChainID(key); if (!issuer_fpr || !*issuer_fpr) { // root or something... addTopLevelKey(key); } else if (std::binary_search(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), issuer_fpr, _detail::ByFingerprint())) { // parent exists... addKeyWithParent(issuer_fpr, key); } else { // parent doesn't exist yet... addKeyWithoutParent(issuer_fpr, key); } const QModelIndex key_idx = index(key); QModelIndex key_parent = key_idx.parent(); while (key_parent.isValid()) { changedParents.insert(doMapToKey(key_parent)); key_parent = key_parent.parent(); } // Step 3: Add children to new parent ( == key ) if (!keyAlreadyExisted && !children.empty()) { addKeys(children); const QModelIndex new_parent = index(key); // Q_EMIT the rowMoved() signals in reversed direction, so the // implementation can use a stack for mapping. for (int i = children.size() - 1; i >= 0; --i) { Q_EMIT rowMoved(new_parent, i); } } } // Q_EMIT dataChanged for all parents with new children. This triggers KeyListSortFilterProxyModel to // show a parent node if it just got children matching the proxy's filter if (!modelResetInProgress()) { for (const Key &i : std::as_const(changedParents)) { const QModelIndex idx = index(i); if (idx.isValid()) { Q_EMIT dataChanged(idx.sibling(idx.row(), 0), idx.sibling(idx.row(), NumColumns - 1)); } } } return indexes(keys); } void HierarchicalKeyListModel::doRemoveKey(const Key &key) { const QModelIndex idx = index(key); if (!idx.isValid()) { return; } const char *const fpr = key.primaryFingerprint(); if (mKeysByExistingParent.find(fpr) != mKeysByExistingParent.end()) { // handle non-leave nodes: std::vector keys = mKeysByFingerprint; const std::vector::iterator it = Kleo::binary_find(keys.begin(), keys.end(), key, _detail::ByFingerprint()); if (it == keys.end()) { return; } keys.erase(it); // FIXME for simplicity, we just clear the model and re-add all keys minus the removed one. This is suboptimal, // but acceptable given that deletion of non-leave nodes is rather rare. clear(Keys); addKeys(keys); return; } // handle leave nodes: const std::vector::iterator it = Kleo::binary_find(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), key, _detail::ByFingerprint()); Q_ASSERT(it != mKeysByFingerprint.end()); Q_ASSERT(mKeysByNonExistingParent.find(fpr) == mKeysByNonExistingParent.end()); Q_ASSERT(mKeysByExistingParent.find(fpr) == mKeysByExistingParent.end()); if (!modelResetInProgress()) { beginRemoveRows(parent(idx), idx.row(), idx.row()); } mKeysByFingerprint.erase(it); const char *const issuer_fpr = cleanChainID(key); const std::vector::iterator tlIt = Kleo::binary_find(mTopLevels.begin(), mTopLevels.end(), key, _detail::ByFingerprint()); if (tlIt != mTopLevels.end()) { mTopLevels.erase(tlIt); } if (issuer_fpr && *issuer_fpr) { const Map::iterator nexIt = mKeysByNonExistingParent.find(issuer_fpr); if (nexIt != mKeysByNonExistingParent.end()) { const std::vector::iterator eit = Kleo::binary_find(nexIt->second.begin(), nexIt->second.end(), key, _detail::ByFingerprint()); if (eit != nexIt->second.end()) { nexIt->second.erase(eit); } if (nexIt->second.empty()) { mKeysByNonExistingParent.erase(nexIt); } } const Map::iterator exIt = mKeysByExistingParent.find(issuer_fpr); if (exIt != mKeysByExistingParent.end()) { const std::vector::iterator eit = Kleo::binary_find(exIt->second.begin(), exIt->second.end(), key, _detail::ByFingerprint()); if (eit != exIt->second.end()) { exIt->second.erase(eit); } if (exIt->second.empty()) { mKeysByExistingParent.erase(exIt); } } } if (!modelResetInProgress()) { endRemoveRows(); } } KeyGroup HierarchicalKeyListModel::doMapToGroup(const QModelIndex &idx) const { Q_ASSERT(idx.isValid()); if (idx.parent().isValid()) { // groups are always top-level return KeyGroup(); } if (static_cast(idx.row()) >= mTopLevels.size() && static_cast(idx.row()) < mTopLevels.size() + mGroups.size() && idx.column() < NumColumns) { return mGroups[idx.row() - mTopLevels.size()]; } else { return KeyGroup(); } } QModelIndex HierarchicalKeyListModel::doMapFromGroup(const KeyGroup &group, int column) const { Q_ASSERT(!group.isNull()); const auto it = std::find_if(mGroups.cbegin(), mGroups.cend(), [group](const KeyGroup &g) { return g.source() == group.source() && g.id() == group.id(); }); if (it == mGroups.cend()) { return QModelIndex(); } else { return createIndex(it - mGroups.cbegin() + mTopLevels.size(), column); } } void HierarchicalKeyListModel::doSetGroups(const std::vector &groups) { Q_ASSERT(mGroups.empty()); // ensure that groups have been cleared const int first = mTopLevels.size(); const int last = first + groups.size() - 1; if (!modelResetInProgress()) { beginInsertRows(QModelIndex(), first, last); } mGroups = groups; if (!modelResetInProgress()) { endInsertRows(); } } QModelIndex HierarchicalKeyListModel::doAddGroup(const KeyGroup &group) { const int newRow = lastGroupRow() + 1; if (!modelResetInProgress()) { beginInsertRows(QModelIndex(), newRow, newRow); } mGroups.push_back(group); if (!modelResetInProgress()) { endInsertRows(); } return createIndex(newRow, 0); } bool HierarchicalKeyListModel::doSetGroupData(const QModelIndex &index, const KeyGroup &group) { if (group.isNull()) { return false; } const int groupIndex = this->groupIndex(index); if (groupIndex == -1) { return false; } mGroups[groupIndex] = group; if (!modelResetInProgress()) { Q_EMIT dataChanged(createIndex(index.row(), 0), createIndex(index.row(), NumColumns - 1)); } return true; } bool HierarchicalKeyListModel::doRemoveGroup(const KeyGroup &group) { const QModelIndex modelIndex = doMapFromGroup(group, 0); if (!modelIndex.isValid()) { return false; } const int groupIndex = this->groupIndex(modelIndex); Q_ASSERT(groupIndex != -1); if (groupIndex == -1) { return false; } if (!modelResetInProgress()) { beginRemoveRows(QModelIndex(), modelIndex.row(), modelIndex.row()); } mGroups.erase(mGroups.begin() + groupIndex); if (!modelResetInProgress()) { endRemoveRows(); } return true; } void HierarchicalKeyListModel::doClear(ItemTypes types) { if (types & Keys) { mTopLevels.clear(); mKeysByFingerprint.clear(); mKeysByExistingParent.clear(); mKeysByNonExistingParent.clear(); Issuers::instance()->clear(); } if (types & Groups) { mGroups.clear(); } } void AbstractKeyListModel::useKeyCache(bool value, KeyList::Options options) { d->m_keyListOptions = options; d->m_useKeyCache = value; if (!d->m_useKeyCache) { clear(All); } else { d->updateFromKeyCache(); } connect(KeyCache::instance().get(), &KeyCache::keysMayHaveChanged, this, [this] { d->updateFromKeyCache(); }); } // static AbstractKeyListModel *AbstractKeyListModel::createFlatKeyListModel(QObject *p) { AbstractKeyListModel *const m = new FlatKeyListModel(p); #ifdef KLEO_MODEL_TEST new QAbstractItemModelTester(m, p); #endif return m; } // static AbstractKeyListModel *AbstractKeyListModel::createHierarchicalKeyListModel(QObject *p) { AbstractKeyListModel *const m = new HierarchicalKeyListModel(p); #ifdef KLEO_MODEL_TEST new QAbstractItemModelTester(m, p); #endif return m; } #include "keylistmodel.moc" /*! \fn AbstractKeyListModel::rowAboutToBeMoved( const QModelIndex & old_parent, int old_row ) Emitted before the removal of a row from that model. It will later be added to the model again, in response to which rowMoved() will be emitted. If multiple rows are moved in one go, multiple rowAboutToBeMoved() signals are emitted before the corresponding number of rowMoved() signals is emitted - in reverse order. This works around the absence of move semantics in QAbstractItemModel. Clients can maintain a stack to perform the QModelIndex-mapping themselves, or, e.g., to preserve the selection status of the row: \code std::vector mMovingRowWasSelected; // transient, used when rows are moved // ... void slotRowAboutToBeMoved( const QModelIndex & p, int row ) { mMovingRowWasSelected.push_back( selectionModel()->isSelected( model()->index( row, 0, p ) ) ); } void slotRowMoved( const QModelIndex & p, int row ) { const bool wasSelected = mMovingRowWasSelected.back(); mMovingRowWasSelected.pop_back(); if ( wasSelected ) selectionModel()->select( model()->index( row, 0, p ), Select|Rows ); } \endcode A similar mechanism could be used to preserve the current item during moves. */ /*! \fn AbstractKeyListModel::rowMoved( const QModelIndex & new_parent, int new_parent ) See rowAboutToBeMoved() */ diff --git a/src/models/keylistmodelinterface.cpp b/src/models/keylistmodelinterface.cpp index af44a9eb9..c005a87b5 100644 --- a/src/models/keylistmodelinterface.cpp +++ b/src/models/keylistmodelinterface.cpp @@ -1,13 +1,16 @@ /* -*- mode: c++; c-basic-offset:4 -*- models/keylistmodelinterface.cpp This file is part of libkleo, the KDE keymanagement library SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-License-Identifier: GPL-2.0-or-later */ + +#include + #include "keylistmodelinterface.h" Kleo::KeyListModelInterface::~KeyListModelInterface() { } diff --git a/src/models/keylistsortfilterproxymodel.cpp b/src/models/keylistsortfilterproxymodel.cpp index 11240f9a4..e73dc0aad 100644 --- a/src/models/keylistsortfilterproxymodel.cpp +++ b/src/models/keylistsortfilterproxymodel.cpp @@ -1,251 +1,253 @@ /* -*- mode: c++; c-basic-offset:4 -*- models/keylistsortfilterproxymodel.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "keylistsortfilterproxymodel.h" #include "keylist.h" #include "keylistmodel.h" #include "kleo/keyfilter.h" #include "kleo/keygroup.h" #include "kleo/stl_util.h" #include #include using namespace Kleo; using namespace GpgME; AbstractKeyListSortFilterProxyModel::AbstractKeyListSortFilterProxyModel(QObject *p) : QSortFilterProxyModel(p) , KeyListModelInterface() { init(); } AbstractKeyListSortFilterProxyModel::AbstractKeyListSortFilterProxyModel(const AbstractKeyListSortFilterProxyModel &other) : QSortFilterProxyModel() , KeyListModelInterface() { Q_UNUSED(other) init(); } void AbstractKeyListSortFilterProxyModel::init() { setDynamicSortFilter(true); setSortRole(Qt::EditRole); // EditRole can be expected to be in a less formatted way, better for sorting setFilterRole(Qt::DisplayRole); setFilterCaseSensitivity(Qt::CaseInsensitive); } AbstractKeyListSortFilterProxyModel::~AbstractKeyListSortFilterProxyModel() { } Key AbstractKeyListSortFilterProxyModel::key(const QModelIndex &idx) const { const KeyListModelInterface *const klmi = dynamic_cast(sourceModel()); if (!klmi) { static Key null; return null; } return klmi->key(mapToSource(idx)); } std::vector AbstractKeyListSortFilterProxyModel::keys(const QList &indexes) const { const KeyListModelInterface *const klmi = dynamic_cast(sourceModel()); if (!klmi) { return std::vector(); } QList mapped; mapped.reserve(indexes.size()); std::transform(indexes.begin(), // indexes.end(), std::back_inserter(mapped), [this](const QModelIndex &idx) { return mapToSource(idx); }); return klmi->keys(mapped); } KeyGroup AbstractKeyListSortFilterProxyModel::group(const QModelIndex &idx) const { if (const KeyListModelInterface *const klmi = dynamic_cast(sourceModel())) { return klmi->group(mapToSource(idx)); } return KeyGroup(); } QModelIndex AbstractKeyListSortFilterProxyModel::index(const Key &key) const { if (const KeyListModelInterface *const klmi = dynamic_cast(sourceModel())) { return mapFromSource(klmi->index(key)); } return {}; } QList AbstractKeyListSortFilterProxyModel::indexes(const std::vector &keys) const { if (const KeyListModelInterface *const klmi = dynamic_cast(sourceModel())) { const QList source = klmi->indexes(keys); QList mapped; mapped.reserve(source.size()); std::transform(source.begin(), // source.end(), std::back_inserter(mapped), [this](const QModelIndex &idx) { return mapFromSource(idx); }); return mapped; } return QList(); } QModelIndex AbstractKeyListSortFilterProxyModel::index(const Kleo::KeyGroup &group) const { if (const KeyListModelInterface *const klmi = dynamic_cast(sourceModel())) { return mapFromSource(klmi->index(group)); } return {}; } class KeyListSortFilterProxyModel::Private { friend class ::Kleo::KeyListSortFilterProxyModel; public: explicit Private() : keyFilter() { } ~Private() { } private: std::shared_ptr keyFilter; }; KeyListSortFilterProxyModel::KeyListSortFilterProxyModel(QObject *p) : AbstractKeyListSortFilterProxyModel(p) , d(new Private) { } KeyListSortFilterProxyModel::KeyListSortFilterProxyModel(const KeyListSortFilterProxyModel &other) : AbstractKeyListSortFilterProxyModel(other) , d(new Private(*other.d)) { } KeyListSortFilterProxyModel::~KeyListSortFilterProxyModel() { } KeyListSortFilterProxyModel *KeyListSortFilterProxyModel::clone() const { return new KeyListSortFilterProxyModel(*this); } std::shared_ptr KeyListSortFilterProxyModel::keyFilter() const { return d->keyFilter; } void KeyListSortFilterProxyModel::setKeyFilter(const std::shared_ptr &kf) { if (kf == d->keyFilter) { return; } d->keyFilter = kf; invalidate(); } bool KeyListSortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { // // 0. Keep parents of matching children: // const QModelIndex index = sourceModel()->index(source_row, 0, source_parent); for (int i = 0, end = sourceModel()->rowCount(index); i != end; ++i) { if (filterAcceptsRow(i, index)) { return true; } } // // 1. Check filterRegExp // const int role = filterRole(); const int col = filterKeyColumn(); const QRegularExpression rx = filterRegularExpression(); const QModelIndex nameIndex = sourceModel()->index(source_row, KeyList::PrettyName, source_parent); const KeyListModelInterface *const klm = dynamic_cast(sourceModel()); Q_ASSERT(klm); const Key key = klm->key(nameIndex); const KeyGroup group = klm->group(nameIndex); Q_ASSERT(!key.isNull() || !group.isNull()); if (col) { const QModelIndex colIdx = sourceModel()->index(source_row, col, source_parent); const QString content = colIdx.data(role).toString(); if (!content.contains(rx)) { return false; } } else if (!key.isNull()) { // By default match against the full uid data (name / email / comment / dn) bool match = false; for (const auto &uid : key.userIDs()) { const auto id = QString::fromUtf8(uid.id()); if (id.contains(rx)) { match = true; break; } // Also match against remarks (search tags) const auto alm = dynamic_cast(sourceModel()); if (alm) { const auto remarks = alm->data(alm->index(key, KeyList::Remarks)); if (!remarks.isNull() && remarks.toString().contains(rx)) { match = true; break; } } // Also match against fingerprints for (const auto &subkey : key.subkeys()) { const auto fpr = QString::fromLatin1(subkey.fingerprint()); if (fpr.contains(rx)) { match = true; break; } } } if (!match) { return false; } } else if (!group.isNull()) { if (!group.name().contains(rx)) { return false; } } else { return false; } // // 2. For keys check that key filters match (if any are defined) // if (d->keyFilter && !key.isNull()) { // avoid artifacts when no filters are defined return d->keyFilter->matches(key, KeyFilter::Filtering); } // 3. match by default: return true; } diff --git a/src/models/keyrearrangecolumnsproxymodel.cpp b/src/models/keyrearrangecolumnsproxymodel.cpp index d46c36702..3c36afb59 100644 --- a/src/models/keyrearrangecolumnsproxymodel.cpp +++ b/src/models/keyrearrangecolumnsproxymodel.cpp @@ -1,83 +1,85 @@ /* models/keyrearangecolumnsproxymodel.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "keyrearrangecolumnsproxymodel.h" #include "kleo/keygroup.h" #include using namespace Kleo; using namespace GpgME; KeyRearrangeColumnsProxyModel::KeyRearrangeColumnsProxyModel(QObject *parent) : KRearrangeColumnsProxyModel(parent) , KeyListModelInterface() { } KeyListModelInterface *KeyRearrangeColumnsProxyModel::klm() const { auto *ret = dynamic_cast(sourceModel()); Q_ASSERT(ret); return ret; } Key KeyRearrangeColumnsProxyModel::key(const QModelIndex &idx) const { return klm()->key(mapToSource(idx)); } std::vector KeyRearrangeColumnsProxyModel::keys(const QList &idxs) const { QList srcIdxs; srcIdxs.reserve(idxs.count()); for (const QModelIndex &idx : idxs) { srcIdxs << mapToSource(idx); } return klm()->keys(srcIdxs); } KeyGroup KeyRearrangeColumnsProxyModel::group(const QModelIndex &idx) const { return klm()->group(mapToSource(idx)); } QModelIndex KeyRearrangeColumnsProxyModel::index(const GpgME::Key &key) const { return mapFromSource(klm()->index(key)); } QList KeyRearrangeColumnsProxyModel::indexes(const std::vector &keys) const { QList myIdxs; const QList srcIdxs = klm()->indexes(keys); myIdxs.reserve(srcIdxs.count()); for (const QModelIndex &idx : srcIdxs) { myIdxs << mapFromSource(idx); } return myIdxs; } QModelIndex KeyRearrangeColumnsProxyModel::index(const KeyGroup &group) const { return mapFromSource(klm()->index(group)); } void KeyRearrangeColumnsProxyModel::sort(int column, Qt::SortOrder order) { const auto fakeIdx = createIndex(0, column); if (!fakeIdx.isValid()) { // Empty model? KRearrangeColumnsProxyModel::sort(column, order); return; } const auto remappedIdx = mapToSource(fakeIdx); KRearrangeColumnsProxyModel::sort(remappedIdx.column(), order); } diff --git a/src/models/subkeylistmodel.cpp b/src/models/subkeylistmodel.cpp index c541d4036..a9485fb73 100644 --- a/src/models/subkeylistmodel.cpp +++ b/src/models/subkeylistmodel.cpp @@ -1,212 +1,214 @@ /* -*- mode: c++; c-basic-offset:4 -*- models/subkeylistmodel.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "subkeylistmodel.h" #include "utils/formatting.h" #include #include #include #include #include #include using namespace GpgME; using namespace Kleo; class SubkeyListModel::Private { friend class ::Kleo::SubkeyListModel; SubkeyListModel *const q; public: explicit Private(SubkeyListModel *qq) : q(qq) , key() { } private: Key key; }; SubkeyListModel::SubkeyListModel(QObject *p) : QAbstractTableModel(p) , d(new Private(this)) { } SubkeyListModel::~SubkeyListModel() { } Key SubkeyListModel::key() const { return d->key; } // slot void SubkeyListModel::setKey(const Key &key) { const Key oldKey = d->key; if (qstricmp(key.primaryFingerprint(), oldKey.primaryFingerprint()) != 0) { // different key -> reset beginResetModel(); d->key = key; endResetModel(); return; } d->key = key; // ### diff them, and signal more fine-grained than this: if (key.numSubkeys() > 0 && oldKey.numSubkeys() == key.numSubkeys()) { Q_EMIT dataChanged(index(0, 0), index(key.numSubkeys() - 1, NumColumns - 1)); } else { Q_EMIT layoutAboutToBeChanged(); Q_EMIT layoutChanged(); } } Subkey SubkeyListModel::subkey(const QModelIndex &idx) const { if (idx.isValid()) { return d->key.subkey(idx.row()); } else { return Subkey(); } } std::vector SubkeyListModel::subkeys(const QList &indexes) const { std::vector result; result.reserve(indexes.size()); std::transform(indexes.begin(), // indexes.end(), std::back_inserter(result), [this](const QModelIndex &idx) { return subkey(idx); }); return result; } QModelIndex SubkeyListModel::index(const Subkey &subkey, int col) const { // O(N), but not sorted, so no better way... for (unsigned int row = 0, end = d->key.numSubkeys(); row != end; ++row) { if (qstricmp(subkey.keyID(), d->key.subkey(row).keyID()) == 0) { return index(row, col); } } return {}; } QList SubkeyListModel::indexes(const std::vector &subkeys) const { QList result; result.reserve(subkeys.size()); // O(N*M), but who cares...? std::transform(subkeys.begin(), // subkeys.end(), std::back_inserter(result), [this](const Subkey &key) { return index(key); }); return result; } void SubkeyListModel::clear() { beginResetModel(); d->key = Key::null; endResetModel(); } int SubkeyListModel::columnCount(const QModelIndex &) const { return NumColumns; } int SubkeyListModel::rowCount(const QModelIndex &pidx) const { return pidx.isValid() ? 0 : d->key.numSubkeys(); } QVariant SubkeyListModel::headerData(int section, Qt::Orientation o, int role) const { if (o == Qt::Horizontal) { if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::ToolTipRole) { switch (section) { case ID: return i18n("ID"); case Type: return i18n("Type"); case ValidFrom: return i18n("Valid From"); case ValidUntil: return i18n("Valid Until"); case Status: return i18n("Status"); case Strength: return i18n("Strength"); case Usage: return i18n("Usage"); case NumColumns:; } } } return QVariant(); } QVariant SubkeyListModel::data(const QModelIndex &idx, int role) const { if (role != Qt::DisplayRole && role != Qt::EditRole && role != Qt::ToolTipRole) { return QVariant(); } const Subkey subkey = this->subkey(idx); if (subkey.isNull()) { return QVariant(); } switch (idx.column()) { case ID: return QString::fromLatin1(subkey.keyID()); case Type: return Formatting::type(subkey); case ValidFrom: if (role == Qt::EditRole) { return Formatting::creationDate(subkey); } else { return Formatting::creationDateString(subkey); } case ValidUntil: if (role == Qt::EditRole) { return Formatting::expirationDate(subkey); } else { return Formatting::expirationDateString(subkey); } case Status: return Formatting::validityShort(subkey); case Usage: return Formatting::usageString(subkey); case Strength: const QString algName = QString::fromStdString(subkey.algoName()); // For ECC keys the algo name is something like bp512 and directly // indicated the "strength" return algName.isEmpty() ? QVariant(subkey.length()) : algName; } return QVariant(); } diff --git a/src/ui/auditlogviewer.cpp b/src/ui/auditlogviewer.cpp index 1c653b143..67dba5398 100644 --- a/src/ui/auditlogviewer.cpp +++ b/src/ui/auditlogviewer.cpp @@ -1,153 +1,155 @@ /* SPDX-FileCopyrightText: 2015-2021 Laurent Montel SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: LGPL-2.0-or-later */ +#include + #include "auditlogviewer.h" #include #include #ifdef HAVE_PIMTEXTEDIT #include "kpimtextedit/richtexteditor.h" #else #include #endif #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo::Private; AuditLogViewer::AuditLogViewer(const QString &log, QWidget *parent) : QDialog(parent) , m_log(/* sic */) , #ifdef HAVE_PIMTEXTEDIT m_textEdit(new KPIMTextEdit::RichTextEditorWidget(this)) #else m_textEdit(new QTextEdit(this)) #endif { setWindowTitle(i18nc("@title:window", "View GnuPG Audit Log")); QDialogButtonBox *buttonBox = new QDialogButtonBox{}; auto copyClipBtn = buttonBox->addButton(i18n("&Copy to Clipboard"), QDialogButtonBox::ActionRole); copyClipBtn->setObjectName(QStringLiteral("copyClipBtn")); copyClipBtn->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); connect(copyClipBtn, &QPushButton::clicked, this, &AuditLogViewer::slotCopyClip); auto saveAsBtn = buttonBox->addButton(i18n("&Save to Disk..."), QDialogButtonBox::ActionRole); saveAsBtn->setObjectName(QStringLiteral("saveAsBtn")); saveAsBtn->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as"))); connect(saveAsBtn, &QPushButton::clicked, this, &AuditLogViewer::slotSaveAs); auto closeBtn = buttonBox->addButton(QString{}, QDialogButtonBox::AcceptRole); closeBtn->setObjectName(QStringLiteral("Close")); KGuiItem::assign(closeBtn, KStandardGuiItem::close()); m_textEdit->setObjectName(QStringLiteral("m_textEdit")); m_textEdit->setReadOnly(true); auto mainLayout = new QVBoxLayout(this); mainLayout->addWidget(m_textEdit); mainLayout->addWidget(buttonBox); #if 0 qDebug() << "buttonBox->style()->styleHint(QStyle::SH_DialogButtonLayout, ...):" << buttonBox->style()->styleHint(QStyle::SH_DialogButtonLayout, nullptr, buttonBox); qDebug() << __func__ << "buttonBox->focusProxy():" << buttonBox->focusProxy(); qDebug() << __func__ << "copyClipBtn->nextInFocusChain():" << copyClipBtn->nextInFocusChain(); qDebug() << __func__ << "saveAsBtn->nextInFocusChain():" << saveAsBtn->nextInFocusChain(); qDebug() << __func__ << "closeBtn->nextInFocusChain():" << closeBtn->nextInFocusChain(); #endif connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); setAuditLog(log); readConfig(); } AuditLogViewer::~AuditLogViewer() { writeConfig(); } void AuditLogViewer::setAuditLog(const QString &log) { if (log == m_log) { return; } m_log = log; m_textEdit->setHtml(QLatin1String("") + log + QLatin1String("")); } void AuditLogViewer::slotSaveAs() { const QString fileName = QFileDialog::getSaveFileName(this, i18n("Choose File to Save GnuPG Audit Log to")); if (fileName.isEmpty()) { return; } QSaveFile file(fileName); if (file.open(QIODevice::WriteOnly)) { QTextStream s(&file); s << ""; if (!windowTitle().isEmpty()) { s << "\n" << windowTitle().toHtmlEscaped() << "\n"; } s << "\n" << m_log << "\n\n"; s.flush(); file.commit(); } if (const int err = file.error()) { KMessageBox::error(this, i18n("Could not save to file \"%1\": %2", file.fileName(), QString::fromLocal8Bit(strerror(err))), i18n("File Save Error")); } } void AuditLogViewer::slotCopyClip() { #ifdef HAVE_PIMTEXTEDIT m_textEdit->editor()->selectAll(); m_textEdit->editor()->copy(); m_textEdit->editor()->textCursor().clearSelection(); #else m_textEdit->selectAll(); m_textEdit->copy(); m_textEdit->textCursor().clearSelection(); #endif } void AuditLogViewer::readConfig() { KConfigGroup group(KSharedConfig::openConfig(), "AuditLogViewer"); const QSize size = group.readEntry("Size", QSize()); if (size.isValid()) { resize(size); } else { resize(600, 400); } } void AuditLogViewer::writeConfig() { KConfigGroup group(KSharedConfig::openConfig(), "AuditLogViewer"); group.writeEntry("Size", size()); group.sync(); } diff --git a/src/ui/cryptoconfigmodule.cpp b/src/ui/cryptoconfigmodule.cpp index 02714053c..dd60dbc34 100644 --- a/src/ui/cryptoconfigmodule.cpp +++ b/src/ui/cryptoconfigmodule.cpp @@ -1,1039 +1,1041 @@ /* cryptoconfigmodule.cpp This file is part of kgpgcertmanager SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "cryptoconfigmodule.h" #include "cryptoconfigentryreaderport_p.h" #include "cryptoconfigmodule_p.h" #include "directoryserviceswidget.h" #include "filenamerequester.h" #include "kleo/keyserverconfig.h" #include "utils/formatting.h" #include "utils/gnupg.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; namespace { class ScrollArea : public QScrollArea { public: explicit ScrollArea(QWidget *p) : QScrollArea(p) { } QSize sizeHint() const override { const QSize wsz = widget() ? widget()->sizeHint() : QSize(); return {wsz.width() + style()->pixelMetric(QStyle::PM_ScrollBarExtent), QScrollArea::sizeHint().height()}; } }; } inline QIcon loadIcon(const QString &s) { QString ss = s; const static QRegularExpression reg(QRegularExpression(QLatin1String("[^a-zA-Z0-9_]"))); return QIcon::fromTheme(ss.replace(reg, QStringLiteral("-"))); } static unsigned int num_components_with_options(const QGpgME::CryptoConfig *config) { if (!config) { return 0; } const QStringList components = config->componentList(); unsigned int result = 0; for (QStringList::const_iterator it = components.begin(); it != components.end(); ++it) { if (const QGpgME::CryptoConfigComponent *const comp = config->component(*it)) { if (!comp->groupList().empty()) { ++result; } } } return result; } static KPageView::FaceType determineJanusFace(const QGpgME::CryptoConfig *config, Kleo::CryptoConfigModule::Layout layout, bool &ok) { ok = true; if (num_components_with_options(config) < 2) { ok = false; return KPageView::Plain; } switch (layout) { case CryptoConfigModule::LinearizedLayout: return KPageView::Plain; case CryptoConfigModule::TabbedLayout: return KPageView::Tabbed; case CryptoConfigModule::IconListLayout: return KPageView::List; } Q_ASSERT(!"we should never get here"); return KPageView::List; } Kleo::CryptoConfigModule::CryptoConfigModule(QGpgME::CryptoConfig *config, QWidget *parent) : KPageWidget(parent) , mConfig(config) { init(IconListLayout); } Kleo::CryptoConfigModule::CryptoConfigModule(QGpgME::CryptoConfig *config, Layout layout, QWidget *parent) : KPageWidget(parent) , mConfig(config) { init(layout); } void Kleo::CryptoConfigModule::init(Layout layout) { if (QLayout *l = this->layout()) { l->setContentsMargins(0, 0, 0, 0); } QGpgME::CryptoConfig *const config = mConfig; bool configOK = false; const KPageView::FaceType type = determineJanusFace(config, layout, configOK); setFaceType(type); QVBoxLayout *vlay = nullptr; QWidget *vbox = nullptr; if (type == Plain) { QWidget *w = new QWidget(this); auto l = new QVBoxLayout(w); l->setContentsMargins(0, 0, 0, 0); auto s = new QScrollArea(w); s->setFrameStyle(QFrame::NoFrame); s->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); s->setWidgetResizable(true); l->addWidget(s); vbox = new QWidget(s->viewport()); vlay = new QVBoxLayout(vbox); vlay->setContentsMargins(0, 0, 0, 0); s->setWidget(vbox); addPage(w, configOK ? QString() : i18n("GpgConf Error")); } const QStringList components = sortComponentList(config->componentList()); for (QStringList::const_iterator it = components.begin(); it != components.end(); ++it) { // qCDebug(KLEO_UI_LOG) <<"Component" << (*it).toLocal8Bit() <<":"; QGpgME::CryptoConfigComponent *comp = config->component(*it); Q_ASSERT(comp); if (comp->groupList().empty()) { continue; } std::unique_ptr compGUI(new CryptoConfigComponentGUI(this, comp)); compGUI->setObjectName(*it); // KJanusWidget doesn't seem to have iterators, so we store a copy... mComponentGUIs.append(compGUI.get()); if (type == Plain) { QGroupBox *gb = new QGroupBox(comp->description(), vbox); (new QVBoxLayout(gb))->addWidget(compGUI.release()); vlay->addWidget(gb); } else { vbox = new QWidget(this); vlay = new QVBoxLayout(vbox); vlay->setContentsMargins(0, 0, 0, 0); KPageWidgetItem *pageItem = new KPageWidgetItem(vbox, comp->description()); if (type != Tabbed) { pageItem->setIcon(loadIcon(comp->iconName())); } addPage(pageItem); QScrollArea *scrollArea = type == Tabbed ? new QScrollArea(vbox) : new ScrollArea(vbox); scrollArea->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); scrollArea->setWidgetResizable(true); vlay->addWidget(scrollArea); const QSize compGUISize = compGUI->sizeHint(); scrollArea->setWidget(compGUI.release()); // Set a nice startup size const int deskHeight = screen()->size().height(); int dialogHeight; if (deskHeight > 1000) { // very big desktop ? dialogHeight = 800; } else if (deskHeight > 650) { // big desktop ? dialogHeight = 500; } else { // small (800x600, 640x480) desktop dialogHeight = 400; } Q_ASSERT(scrollArea->widget()); if (type != Tabbed) { scrollArea->setMinimumHeight(qMin(compGUISize.height(), dialogHeight)); } } } if (mComponentGUIs.empty()) { const QString msg = i18n( "The gpgconf tool used to provide the information " "for this dialog does not seem to be installed " "properly. It did not return any components. " "Try running \"%1\" on the command line for more " "information.", components.empty() ? QLatin1String("gpgconf --list-components") : QLatin1String("gpgconf --list-options gpg")); QLabel *label = new QLabel(msg, vbox); label->setWordWrap(true); label->setMinimumHeight(fontMetrics().lineSpacing() * 5); vlay->addWidget(label); } } namespace { template QStringList sortConfigEntries(const Iterator orderBegin, const Iterator orderEnd, const QStringList &entries) { // components sorting algorithm: // 1. components with predefined order (provided via orderBegin / orderEnd) // 2. other components sorted alphabetically QStringList result; QStringList others; for (auto it = orderBegin; it != orderEnd; ++it) { if (entries.contains(*it)) { result.append(*it); } } for (const auto &item : entries) { if (!result.contains(item)) { others.append(item); } } others.sort(); result.append(others); return result; } } // namespace QStringList Kleo::CryptoConfigModule::sortComponentList(const QStringList &components) { static const std::array order = { QStringLiteral("gpg"), QStringLiteral("gpgsm"), QStringLiteral("gpg-agent"), QStringLiteral("dirmngr"), QStringLiteral("pinentry"), QStringLiteral("scdaemon"), }; return sortConfigEntries(order.begin(), order.end(), components); } QStringList Kleo::CryptoConfigModule::sortGroupList(const QString &moduleName, const QStringList &groups) { if (moduleName == QStringLiteral("gpg")) { static const std::array order = { QStringLiteral("Keyserver"), QStringLiteral("Configuration"), QStringLiteral("Monitor"), QStringLiteral("Debug"), }; return sortConfigEntries(order.begin(), order.end(), groups); } else if (moduleName == QStringLiteral("gpgsm")) { static const std::array order = { QStringLiteral("Security"), QStringLiteral("Configuration"), QStringLiteral("Monitor"), QStringLiteral("Debug"), }; return sortConfigEntries(order.begin(), order.end(), groups); } else if (moduleName == QStringLiteral("gpg-agent")) { static const std::array order = { QStringLiteral("Security"), QStringLiteral("Passphrase policy"), QStringLiteral("Configuration"), QStringLiteral("Monitor"), QStringLiteral("Debug"), }; return sortConfigEntries(order.begin(), order.end(), groups); } else if (moduleName == QStringLiteral("dirmngr")) { static const std::array order = { QStringLiteral("Keyserver"), QStringLiteral("HTTP"), QStringLiteral("LDAP"), QStringLiteral("OCSP"), QStringLiteral("Tor"), QStringLiteral("Enforcement"), QStringLiteral("Configuration"), QStringLiteral("Format"), QStringLiteral("Monitor"), QStringLiteral("Debug"), }; return sortConfigEntries(order.begin(), order.end(), groups); } else if (moduleName == QStringLiteral("scdaemon")) { static const std::array order = { QStringLiteral("Monitor"), QStringLiteral("Configuration"), QStringLiteral("Security"), QStringLiteral("Debug"), }; return sortConfigEntries(order.begin(), order.end(), groups); } else { qCDebug(KLEO_UI_LOG) << "Configuration groups order is not defined for " << moduleName; QStringList result(groups); result.sort(); return result; } } bool Kleo::CryptoConfigModule::hasError() const { return mComponentGUIs.empty(); } void Kleo::CryptoConfigModule::save() { bool changed = false; QList::Iterator it = mComponentGUIs.begin(); for (; it != mComponentGUIs.end(); ++it) { if ((*it)->save()) { changed = true; } } if (changed) { mConfig->sync(true /*runtime*/); } } void Kleo::CryptoConfigModule::reset() { QList::Iterator it = mComponentGUIs.begin(); for (; it != mComponentGUIs.end(); ++it) { (*it)->load(); } } void Kleo::CryptoConfigModule::defaults() { QList::Iterator it = mComponentGUIs.begin(); for (; it != mComponentGUIs.end(); ++it) { (*it)->defaults(); } } void Kleo::CryptoConfigModule::cancel() { mConfig->clear(); } //// namespace { bool offerEntryForConfiguration(QGpgME::CryptoConfigEntry *entry) { static const QRegularExpression entryPathGroupSegmentRegexp{QStringLiteral("/.*/")}; static std::set entriesToExclude; if (entriesToExclude.empty()) { entriesToExclude.insert(QStringLiteral("gpg/keyserver")); if (engineIsVersion(2, 3, 5, GpgME::GpgConfEngine) || (engineIsVersion(2, 2, 34, GpgME::GpgConfEngine) && !engineIsVersion(2, 3, 0, GpgME::GpgConfEngine))) { // exclude for 2.2.{34,...} and 2.3.5+ entriesToExclude.insert(QStringLiteral("gpgsm/keyserver")); } } const bool de_vs = Kleo::gnupgUsesDeVsCompliance(); // Skip "dangerous" expert options if we are running in CO_DE_VS. // Otherwise, skip any options beyond "invisible" (== expert + 1) level. const auto maxEntryLevel = de_vs ? QGpgME::CryptoConfigEntry::Level_Advanced // : QGpgME::CryptoConfigEntry::Level_Expert + 1; // we ignore the group when looking up entries to exclude because entries // are uniquely identified by their name and their component const auto entryId = entry->path().replace(entryPathGroupSegmentRegexp, QLatin1String{"/"}).toLower(); return (entry->level() <= maxEntryLevel) && (entriesToExclude.find(entryId) == entriesToExclude.end()); } auto getGroupEntriesToOfferForConfiguration(QGpgME::CryptoConfigGroup *group) { std::vector result; const auto entryNames = group->entryList(); for (const auto &entryName : entryNames) { auto *const entry = group->entry(entryName); Q_ASSERT(entry); if (offerEntryForConfiguration(entry)) { result.push_back(entry); } else { qCDebug(KLEO_UI_LOG) << "entry" << entry->path() << "too advanced or excluded explicitly, skipping"; } } return result; } } Kleo::CryptoConfigComponentGUI::CryptoConfigComponentGUI(CryptoConfigModule *module, QGpgME::CryptoConfigComponent *component, QWidget *parent) : QWidget(parent) , mComponent(component) { auto glay = new QGridLayout(this); const QStringList groups = module->sortGroupList(mComponent->name(), mComponent->groupList()); if (groups.size() > 1) { glay->setColumnMinimumWidth(0, 30); for (QStringList::const_iterator it = groups.begin(), end = groups.end(); it != end; ++it) { QGpgME::CryptoConfigGroup *group = mComponent->group(*it); Q_ASSERT(group); if (!group) { continue; } auto groupEntries = getGroupEntriesToOfferForConfiguration(group); if (groupEntries.size() == 0) { // skip groups without entries to be offered in the UI continue; } const QString title = group->description(); auto hbox = new QHBoxLayout; hbox->addWidget(new QLabel{title.isEmpty() ? *it : title, this}); hbox->addWidget(new KSeparator{Qt::Horizontal, this}, 1); const int row = glay->rowCount(); glay->addLayout(hbox, row, 0, 1, 3); mGroupGUIs.append(new CryptoConfigGroupGUI(module, group, groupEntries, glay, this)); } } else if (!groups.empty()) { auto *const group = mComponent->group(groups.front()); auto groupEntries = getGroupEntriesToOfferForConfiguration(group); if (groupEntries.size() > 0) { mGroupGUIs.append(new CryptoConfigGroupGUI(module, group, groupEntries, glay, this)); } } glay->setRowStretch(glay->rowCount(), 1); } bool Kleo::CryptoConfigComponentGUI::save() { bool changed = false; QList::Iterator it = mGroupGUIs.begin(); for (; it != mGroupGUIs.end(); ++it) { if ((*it)->save()) { changed = true; } } return changed; } void Kleo::CryptoConfigComponentGUI::load() { QList::Iterator it = mGroupGUIs.begin(); for (; it != mGroupGUIs.end(); ++it) { (*it)->load(); } } void Kleo::CryptoConfigComponentGUI::defaults() { QList::Iterator it = mGroupGUIs.begin(); for (; it != mGroupGUIs.end(); ++it) { (*it)->defaults(); } } //// Kleo::CryptoConfigGroupGUI::CryptoConfigGroupGUI(CryptoConfigModule *module, QGpgME::CryptoConfigGroup *group, const std::vector &entries, QGridLayout *glay, QWidget *widget) : QObject(module) { const int startRow = glay->rowCount(); for (auto entry : entries) { CryptoConfigEntryGUI *entryGUI = CryptoConfigEntryGUIFactory::createEntryGUI(module, entry, entry->name(), glay, widget); if (entryGUI) { mEntryGUIs.append(entryGUI); entryGUI->load(); } } const int endRow = glay->rowCount() - 1; if (endRow < startRow) { return; } const QString iconName = group->iconName(); if (iconName.isEmpty()) { return; } QLabel *l = new QLabel(widget); l->setPixmap(loadIcon(iconName).pixmap(32, 32)); glay->addWidget(l, startRow, 0, endRow - startRow + 1, 1, Qt::AlignTop); } bool Kleo::CryptoConfigGroupGUI::save() { bool changed = false; QList::Iterator it = mEntryGUIs.begin(); for (; it != mEntryGUIs.end(); ++it) { if ((*it)->isChanged()) { (*it)->save(); changed = true; } } return changed; } void Kleo::CryptoConfigGroupGUI::load() { QList::Iterator it = mEntryGUIs.begin(); for (; it != mEntryGUIs.end(); ++it) { (*it)->load(); } } void Kleo::CryptoConfigGroupGUI::defaults() { QList::Iterator it = mEntryGUIs.begin(); for (; it != mEntryGUIs.end(); ++it) { (*it)->resetToDefault(); } } //// using constructor = CryptoConfigEntryGUI *(*)(CryptoConfigModule *, QGpgME::CryptoConfigEntry *, const QString &, QGridLayout *, QWidget *); namespace { template CryptoConfigEntryGUI *_create(CryptoConfigModule *m, QGpgME::CryptoConfigEntry *e, const QString &n, QGridLayout *l, QWidget *p) { return new T_Widget(m, e, n, l, p); } } static const struct WidgetsByEntryName { const char *entryGlob; constructor create; } widgetsByEntryName[] = { {"*/*/debug-level", &_create}, {"scdaemon/*/reader-port", &_create}, }; static const unsigned int numWidgetsByEntryName = sizeof widgetsByEntryName / sizeof *widgetsByEntryName; static const constructor listWidgets[QGpgME::CryptoConfigEntry::NumArgType] = { // None: A list of options with no arguments (e.g. -v -v -v) is shown as a spinbox &_create, nullptr, // String // Int/UInt: Let people type list of numbers (1,2,3....). Untested. &_create, &_create, nullptr, // Path nullptr, // Formerly URL &_create, nullptr, // DirPath }; static const constructor scalarWidgets[QGpgME::CryptoConfigEntry::NumArgType] = { // clang-format off &_create, // None &_create, // String &_create, // Int &_create, // UInt &_create, // Path nullptr, // Formerly URL nullptr, // LDAPURL &_create, // DirPath // clang-format on }; CryptoConfigEntryGUI *Kleo::CryptoConfigEntryGUIFactory::createEntryGUI(CryptoConfigModule *module, QGpgME::CryptoConfigEntry *entry, const QString &entryName, QGridLayout *glay, QWidget *widget) { Q_ASSERT(entry); // try to lookup by path: const QString path = entry->path(); for (unsigned int i = 0; i < numWidgetsByEntryName; ++i) { if (QRegExp(QLatin1String(widgetsByEntryName[i].entryGlob), Qt::CaseSensitive, QRegExp::Wildcard).exactMatch(path)) { return widgetsByEntryName[i].create(module, entry, entryName, glay, widget); } } // none found, so look up by type: const unsigned int argType = entry->argType(); Q_ASSERT(argType < QGpgME::CryptoConfigEntry::NumArgType); if (entry->isList()) { if (const constructor create = listWidgets[argType]) { return create(module, entry, entryName, glay, widget); } else { qCWarning(KLEO_UI_LOG) << "No widget implemented for list of type" << entry->argType(); } } else if (const constructor create = scalarWidgets[argType]) { return create(module, entry, entryName, glay, widget); } else { qCWarning(KLEO_UI_LOG) << "No widget implemented for type" << entry->argType(); } return nullptr; } //// Kleo::CryptoConfigEntryGUI::CryptoConfigEntryGUI(CryptoConfigModule *module, QGpgME::CryptoConfigEntry *entry, const QString &entryName) : QObject(module) , mEntry(entry) , mName(entryName) , mChanged(false) { connect(this, &CryptoConfigEntryGUI::changed, module, &CryptoConfigModule::changed); } QString Kleo::CryptoConfigEntryGUI::description() const { QString descr = mEntry->description(); if (descr.isEmpty()) { // happens for expert options // String does not need to be translated because the options itself // are also not translated return QStringLiteral("\"%1\"").arg(mName); } if (i18nc("Translate this to 'yes' or 'no' (use the English words!) " "depending on whether your language uses " "Sentence style capitalization in GUI labels (yes) or not (no). " "Context: We get some backend strings in that have the wrong " "capitalization (in English, at least) so we need to force the " "first character to upper-case. It is this behaviour you can " "control for your language with this translation.", "yes") == QLatin1String("yes")) { descr[0] = descr[0].toUpper(); } return descr; } void Kleo::CryptoConfigEntryGUI::resetToDefault() { mEntry->resetToDefault(); load(); } //// Kleo::CryptoConfigEntryLineEdit::CryptoConfigEntryLineEdit(CryptoConfigModule *module, QGpgME::CryptoConfigEntry *entry, const QString &entryName, QGridLayout *glay, QWidget *widget) : CryptoConfigEntryGUI(module, entry, entryName) { const int row = glay->rowCount(); mLineEdit = new KLineEdit(widget); QLabel *label = new QLabel(description(), widget); label->setBuddy(mLineEdit); glay->addWidget(label, row, 1); glay->addWidget(mLineEdit, row, 2); if (entry->isReadOnly()) { label->setEnabled(false); mLineEdit->setEnabled(false); } else { connect(mLineEdit, &KLineEdit::textChanged, this, &CryptoConfigEntryLineEdit::slotChanged); } } void Kleo::CryptoConfigEntryLineEdit::doSave() { mEntry->setStringValue(mLineEdit->text()); } void Kleo::CryptoConfigEntryLineEdit::doLoad() { mLineEdit->setText(mEntry->stringValue()); } //// /* Note: Do not use "guru" as debug level but use the value 10. The former also enables the creation of hash dump files and thus leaves traces of plaintext on the disk. */ static const struct { const KLazyLocalizedString label; const char *name; } debugLevels[] = { {kli18n("0 - None"), "none"}, {kli18n("1 - Basic"), "basic"}, {kli18n("2 - Verbose"), "advanced"}, {kli18n("3 - More Verbose"), "expert"}, {kli18n("4 - All"), "10"}, }; static const unsigned int numDebugLevels = sizeof debugLevels / sizeof *debugLevels; Kleo::CryptoConfigEntryDebugLevel::CryptoConfigEntryDebugLevel(CryptoConfigModule *module, QGpgME::CryptoConfigEntry *entry, const QString &entryName, QGridLayout *glay, QWidget *widget) : CryptoConfigEntryGUI(module, entry, entryName) , mComboBox(new QComboBox(widget)) { QLabel *label = new QLabel(i18n("Set the debugging level to"), widget); label->setBuddy(mComboBox); for (unsigned int i = 0; i < numDebugLevels; ++i) { mComboBox->addItem(KLocalizedString(debugLevels[i].label).toString()); } if (entry->isReadOnly()) { label->setEnabled(false); mComboBox->setEnabled(false); } else { connect(mComboBox, qOverload(&QComboBox::currentIndexChanged), this, &CryptoConfigEntryDebugLevel::slotChanged); } const int row = glay->rowCount(); glay->addWidget(label, row, 1); glay->addWidget(mComboBox, row, 2); } void Kleo::CryptoConfigEntryDebugLevel::doSave() { const unsigned int idx = mComboBox->currentIndex(); if (idx < numDebugLevels) { mEntry->setStringValue(QLatin1String(debugLevels[idx].name)); } else { mEntry->setStringValue(QString()); } } void Kleo::CryptoConfigEntryDebugLevel::doLoad() { const QString str = mEntry->stringValue(); for (unsigned int i = 0; i < numDebugLevels; ++i) { if (str == QLatin1String(debugLevels[i].name)) { mComboBox->setCurrentIndex(i); return; } } mComboBox->setCurrentIndex(0); } //// Kleo::CryptoConfigEntryPath::CryptoConfigEntryPath(CryptoConfigModule *module, QGpgME::CryptoConfigEntry *entry, const QString &entryName, QGridLayout *glay, QWidget *widget) : CryptoConfigEntryGUI(module, entry, entryName) , mFileNameRequester(nullptr) { const int row = glay->rowCount(); mFileNameRequester = new FileNameRequester(widget); mFileNameRequester->setExistingOnly(false); mFileNameRequester->setFilter(QDir::Files); QLabel *label = new QLabel(description(), widget); label->setBuddy(mFileNameRequester); glay->addWidget(label, row, 1); glay->addWidget(mFileNameRequester, row, 2); if (entry->isReadOnly()) { label->setEnabled(false); mFileNameRequester->setEnabled(false); } else { connect(mFileNameRequester, &FileNameRequester::fileNameChanged, this, &CryptoConfigEntryPath::slotChanged); } } void Kleo::CryptoConfigEntryPath::doSave() { mEntry->setURLValue(QUrl::fromLocalFile(mFileNameRequester->fileName())); } void Kleo::CryptoConfigEntryPath::doLoad() { if (mEntry->urlValue().isLocalFile()) { mFileNameRequester->setFileName(mEntry->urlValue().toLocalFile()); } else { mFileNameRequester->setFileName(mEntry->urlValue().toString()); } } //// Kleo::CryptoConfigEntryDirPath::CryptoConfigEntryDirPath(CryptoConfigModule *module, QGpgME::CryptoConfigEntry *entry, const QString &entryName, QGridLayout *glay, QWidget *widget) : CryptoConfigEntryGUI(module, entry, entryName) , mFileNameRequester(nullptr) { const int row = glay->rowCount(); mFileNameRequester = new FileNameRequester(widget); mFileNameRequester->setExistingOnly(false); mFileNameRequester->setFilter(QDir::Dirs); QLabel *label = new QLabel(description(), widget); label->setBuddy(mFileNameRequester); glay->addWidget(label, row, 1); glay->addWidget(mFileNameRequester, row, 2); if (entry->isReadOnly()) { label->setEnabled(false); mFileNameRequester->setEnabled(false); } else { connect(mFileNameRequester, &FileNameRequester::fileNameChanged, this, &CryptoConfigEntryDirPath::slotChanged); } } void Kleo::CryptoConfigEntryDirPath::doSave() { mEntry->setURLValue(QUrl::fromLocalFile(mFileNameRequester->fileName())); } void Kleo::CryptoConfigEntryDirPath::doLoad() { mFileNameRequester->setFileName(mEntry->urlValue().toLocalFile()); } //// Kleo::CryptoConfigEntrySpinBox::CryptoConfigEntrySpinBox(CryptoConfigModule *module, QGpgME::CryptoConfigEntry *entry, const QString &entryName, QGridLayout *glay, QWidget *widget) : CryptoConfigEntryGUI(module, entry, entryName) { if (entry->argType() == QGpgME::CryptoConfigEntry::ArgType_None && entry->isList()) { mKind = ListOfNone; } else if (entry->argType() == QGpgME::CryptoConfigEntry::ArgType_UInt) { mKind = UInt; } else { Q_ASSERT(entry->argType() == QGpgME::CryptoConfigEntry::ArgType_Int); mKind = Int; } const int row = glay->rowCount(); mNumInput = new QSpinBox(widget); QLabel *label = new QLabel(description(), widget); label->setBuddy(mNumInput); glay->addWidget(label, row, 1); glay->addWidget(mNumInput, row, 2); if (entry->isReadOnly()) { label->setEnabled(false); mNumInput->setEnabled(false); } else { mNumInput->setMinimum(mKind == Int ? std::numeric_limits::min() : 0); mNumInput->setMaximum(std::numeric_limits::max()); connect(mNumInput, qOverload(&QSpinBox::valueChanged), this, &CryptoConfigEntrySpinBox::slotChanged); } } void Kleo::CryptoConfigEntrySpinBox::doSave() { int value = mNumInput->value(); switch (mKind) { case ListOfNone: mEntry->setNumberOfTimesSet(value); break; case UInt: mEntry->setUIntValue(value); break; case Int: mEntry->setIntValue(value); break; } } void Kleo::CryptoConfigEntrySpinBox::doLoad() { int value = 0; switch (mKind) { case ListOfNone: value = mEntry->numberOfTimesSet(); break; case UInt: value = mEntry->uintValue(); break; case Int: value = mEntry->intValue(); break; } mNumInput->setValue(value); } //// Kleo::CryptoConfigEntryCheckBox::CryptoConfigEntryCheckBox(CryptoConfigModule *module, QGpgME::CryptoConfigEntry *entry, const QString &entryName, QGridLayout *glay, QWidget *widget) : CryptoConfigEntryGUI(module, entry, entryName) { const int row = glay->rowCount(); mCheckBox = new QCheckBox(widget); glay->addWidget(mCheckBox, row, 1, 1, 2); mCheckBox->setText(description()); if (entry->isReadOnly()) { mCheckBox->setEnabled(false); } else { connect(mCheckBox, &QCheckBox::toggled, this, &CryptoConfigEntryCheckBox::slotChanged); } } void Kleo::CryptoConfigEntryCheckBox::doSave() { mEntry->setBoolValue(mCheckBox->isChecked()); } void Kleo::CryptoConfigEntryCheckBox::doLoad() { mCheckBox->setChecked(mEntry->boolValue()); } Kleo::CryptoConfigEntryLDAPURL::CryptoConfigEntryLDAPURL(CryptoConfigModule *module, QGpgME::CryptoConfigEntry *entry, const QString &entryName, QGridLayout *glay, QWidget *widget) : CryptoConfigEntryGUI(module, entry, entryName) { mLabel = new QLabel(widget); mPushButton = new QPushButton(entry->isReadOnly() ? i18n("Show...") : i18n("Edit..."), widget); const int row = glay->rowCount(); QLabel *label = new QLabel(description(), widget); label->setBuddy(mPushButton); glay->addWidget(label, row, 1); auto hlay = new QHBoxLayout; glay->addLayout(hlay, row, 2); hlay->addWidget(mLabel, 1); hlay->addWidget(mPushButton); if (entry->isReadOnly()) { mLabel->setEnabled(false); } connect(mPushButton, &QPushButton::clicked, this, &CryptoConfigEntryLDAPURL::slotOpenDialog); } void Kleo::CryptoConfigEntryLDAPURL::doLoad() { setURLList(mEntry->urlValueList()); } void Kleo::CryptoConfigEntryLDAPURL::doSave() { mEntry->setURLValueList(mURLList); } void prepareURLCfgDialog(QDialog *dialog, DirectoryServicesWidget *dirserv, bool readOnly) { QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok, dialog); if (!readOnly) { buttonBox->addButton(QDialogButtonBox::Cancel); buttonBox->addButton(QDialogButtonBox::RestoreDefaults); QPushButton *defaultsBtn = buttonBox->button(QDialogButtonBox::RestoreDefaults); QObject::connect(defaultsBtn, &QPushButton::clicked, dirserv, &DirectoryServicesWidget::clear); QObject::connect(buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject); } QObject::connect(buttonBox, &QDialogButtonBox::accepted, dialog, &QDialog::accept); auto layout = new QVBoxLayout; layout->addWidget(dirserv); layout->addWidget(buttonBox); dialog->setLayout(layout); } void Kleo::CryptoConfigEntryLDAPURL::slotOpenDialog() { if (!gpgme_check_version("1.16.0")) { KMessageBox::sorry(mPushButton->parentWidget(), i18n("Configuration of directory services is not possible " "because the used gpgme libraries are too old."), i18n("Sorry")); return; } // I'm a bad boy and I do it all on the stack. Enough classes already :) // This is just a simple dialog around the directory-services-widget QDialog dialog(mPushButton->parentWidget()); dialog.setWindowTitle(i18nc("@title:window", "Configure Directory Services")); auto dirserv = new DirectoryServicesWidget(&dialog); prepareURLCfgDialog(&dialog, dirserv, mEntry->isReadOnly()); dirserv->setReadOnly(mEntry->isReadOnly()); std::vector servers; std::transform(std::cbegin(mURLList), std::cend(mURLList), std::back_inserter(servers), [](const auto &url) { return KeyserverConfig::fromUrl(url); }); dirserv->setKeyservers(servers); if (dialog.exec()) { QList urls; const auto servers = dirserv->keyservers(); std::transform(std::begin(servers), std::end(servers), std::back_inserter(urls), [](const auto &server) { return server.toUrl(); }); setURLList(urls); slotChanged(); } } void Kleo::CryptoConfigEntryLDAPURL::setURLList(const QList &urlList) { mURLList = urlList; if (mURLList.isEmpty()) { mLabel->setText(i18n("None configured")); } else { mLabel->setText(i18np("1 server configured", "%1 servers configured", mURLList.count())); } } #include "moc_cryptoconfigmodule_p.cpp" diff --git a/src/ui/directoryserviceswidget.cpp b/src/ui/directoryserviceswidget.cpp index 8eb06253b..b419ba1eb 100644 --- a/src/ui/directoryserviceswidget.cpp +++ b/src/ui/directoryserviceswidget.cpp @@ -1,410 +1,412 @@ /* ui/directoryserviceswidget.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2001, 2002, 2004 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2017 Bundesamnt für Sicherheit in der Informationstechnik SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "directoryserviceswidget.h" #include "editdirectoryservicedialog.h" #include "kleo/keyserverconfig.h" #include "utils/gnupg.h" #include #include #include #include #include #include #include #include #include using namespace Kleo; namespace { bool activeDirectoryIsSupported() { return engineIsVersion(2, 2, 28, GpgME::GpgSMEngine); } bool isStandardActiveDirectory(const KeyserverConfig &keyserver) { return (keyserver.authentication() == KeyserverAuthentication::ActiveDirectory) && keyserver.host().isEmpty(); } bool keyserverIsEditable(const KeyserverConfig &keyserver) { // standard AD is not editable return !isStandardActiveDirectory(keyserver); } class KeyserverModel : public QAbstractListModel { Q_OBJECT public: explicit KeyserverModel(QObject *parent = nullptr) : QAbstractListModel{parent} { } void setKeyservers(const std::vector &servers) { clear(); beginInsertRows(QModelIndex(), 0, servers.size() - 1); m_items = servers; endInsertRows(); } void addKeyserver(const KeyserverConfig &keyserver) { const auto row = m_items.size(); beginInsertRows(QModelIndex(), row, row); m_items.push_back(keyserver); endInsertRows(); } KeyserverConfig getKeyserver(unsigned int id) { if (id >= m_items.size()) { qCDebug(KLEO_UI_LOG) << __func__ << "invalid keyserver id:" << id; return {}; } return m_items[id]; } void updateKeyserver(unsigned int id, const KeyserverConfig &keyserver) { if (id >= m_items.size()) { qCDebug(KLEO_UI_LOG) << __func__ << "invalid keyserver id:" << id; return; } m_items[id] = keyserver; Q_EMIT dataChanged(index(id), index(id)); } void deleteKeyserver(unsigned int id) { if (id >= m_items.size()) { qCDebug(KLEO_UI_LOG) << __func__ << "invalid keyserver id:" << id; return; } beginRemoveRows(QModelIndex(), id, id); m_items.erase(m_items.begin() + id); endRemoveRows(); } void clear() { if (m_items.empty()) { return; } beginRemoveRows(QModelIndex(), 0, m_items.size() - 1); m_items.clear(); endRemoveRows(); } int rowCount(const QModelIndex & = QModelIndex()) const override { return m_items.size(); } QVariant data(const QModelIndex &index, int role) const override { if (!index.isValid()) { return {}; } switch (role) { case Qt::DisplayRole: case Qt::EditRole: { const auto keyserver = m_items[index.row()]; return isStandardActiveDirectory(keyserver) ? i18n("Active Directory") : keyserver.host(); } } return {}; } bool hasActiveDirectory() { // check whether any of the model items represents an Active Directory keyserver return std::any_of(std::cbegin(m_items), std::cend(m_items), isStandardActiveDirectory); } private: using QAbstractListModel::setData; private: std::vector m_items; }; } class DirectoryServicesWidget::Private { DirectoryServicesWidget *const q; struct { QListView *keyserverList = nullptr; QToolButton *newButton = nullptr; QAction *addActiveDirectoryAction = nullptr; QAction *addLdapServerAction = nullptr; QPushButton *editButton = nullptr; QPushButton *deleteButton = nullptr; } ui; KeyserverModel *keyserverModel = nullptr; bool readOnly = false; public: Private(DirectoryServicesWidget *qq) : q(qq) { auto mainLayout = new QVBoxLayout{q}; auto gridLayout = new QGridLayout{}; gridLayout->setColumnStretch(0, 1); gridLayout->setRowStretch(1, 1); keyserverModel = new KeyserverModel{q}; ui.keyserverList = new QListView(); ui.keyserverList->setModel(keyserverModel); ui.keyserverList->setModelColumn(0); ui.keyserverList->setSelectionBehavior(QAbstractItemView::SelectRows); ui.keyserverList->setSelectionMode(QAbstractItemView::SingleSelection); ui.keyserverList->setWhatsThis(i18nc("@info:whatsthis", "This is a list of all directory services that are configured for use with X.509.")); gridLayout->addWidget(ui.keyserverList, 1, 0); auto groupsButtonLayout = new QVBoxLayout(); auto menu = new QMenu{q}; ui.addActiveDirectoryAction = menu->addAction(i18n("Active Directory"), [this]() { addActiveDirectory(); }); ui.addActiveDirectoryAction->setToolTip(i18nc("@info:tooltip", "Click to use a directory service running on your Active Directory. " "This works only on Windows and requires GnuPG 2.2.28 or later.")); ui.addActiveDirectoryAction->setEnabled(activeDirectoryIsSupported()); ui.addLdapServerAction = menu->addAction(i18n("LDAP Server"), [this]() { addLdapServer(); }); ui.addLdapServerAction->setToolTip(i18nc("@info:tooltip", "Click to add a directory service provided by an LDAP server.")); ui.newButton = new QToolButton{q}; ui.newButton->setText(i18n("Add")); ui.newButton->setToolTip(i18nc("@info:tooltip", "Click to add a directory service.")); ui.newButton->setWhatsThis(i18nc("@info:whatsthis", "Click this button to add a directory service to the list of services. " "The change will only take effect once you acknowledge the configuration dialog.")); ui.newButton->setToolButtonStyle(Qt::ToolButtonTextOnly); ui.newButton->setPopupMode(QToolButton::InstantPopup); ui.newButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); // expand horizontally like the QPushButtons ui.newButton->setMenu(menu); groupsButtonLayout->addWidget(ui.newButton); ui.editButton = new QPushButton(i18n("Edit")); ui.editButton->setToolTip(i18nc("@info:tooltip", "Click to edit the selected service.")); ui.editButton->setWhatsThis(i18nc("@info:whatsthis", "Click this button to edit the settings of the currently selected directory service. " "The changes will only take effect once you acknowledge the configuration dialog.")); ui.editButton->setEnabled(false); groupsButtonLayout->addWidget(ui.editButton); ui.deleteButton = new QPushButton(i18n("Delete")); ui.deleteButton->setToolTip(i18nc("@info:tooltip", "Click to remove the selected service.")); ui.deleteButton->setWhatsThis(i18nc("@info:whatsthis", "Click this button to remove the currently selected directory service. " "The change will only take effect once you acknowledge the configuration dialog.")); ui.deleteButton->setEnabled(false); groupsButtonLayout->addWidget(ui.deleteButton); groupsButtonLayout->addStretch(1); gridLayout->addLayout(groupsButtonLayout, 1, 1); mainLayout->addLayout(gridLayout, /*stretch=*/1); connect(keyserverModel, &QAbstractItemModel::dataChanged, q, [this]() { modelChanged(); }); connect(keyserverModel, &QAbstractItemModel::rowsInserted, q, [this]() { modelChanged(); }); connect(keyserverModel, &QAbstractItemModel::rowsRemoved, q, [this]() { modelChanged(); }); connect(ui.keyserverList->selectionModel(), &QItemSelectionModel::selectionChanged, q, [this]() { selectionChanged(); }); connect(ui.keyserverList, &QListView::doubleClicked, q, [this](const QModelIndex &index) { if (!readOnly) { editKeyserver(index); } }); connect(ui.editButton, &QPushButton::clicked, q, [this]() { editKeyserver(); }); connect(ui.deleteButton, &QPushButton::clicked, q, [this]() { deleteKeyserver(); }); } void setReadOnly(bool ro) { readOnly = ro; updateActions(); } void setKeyservers(const std::vector &servers) { keyserverModel->setKeyservers(servers); } std::vector keyservers() const { std::vector result; result.reserve(keyserverModel->rowCount()); for (int row = 0; row < keyserverModel->rowCount(); ++row) { result.push_back(keyserverModel->getKeyserver(row)); } return result; } void clear() { if (keyserverModel->rowCount() == 0) { return; } keyserverModel->clear(); } private: auto selectedIndex() { const auto indexes = ui.keyserverList->selectionModel()->selectedRows(); return indexes.empty() ? QModelIndex() : indexes[0]; } void modelChanged() { updateActions(); Q_EMIT q->changed(); } void selectionChanged() { updateActions(); } void updateActions() { const auto index = selectedIndex(); ui.newButton->setEnabled(!readOnly); ui.addActiveDirectoryAction->setEnabled(activeDirectoryIsSupported() && !keyserverModel->hasActiveDirectory()); ui.editButton->setEnabled(!readOnly && index.isValid() && keyserverIsEditable(keyserverModel->getKeyserver(index.row()))); ui.deleteButton->setEnabled(!readOnly && index.isValid()); } void handleEditKeyserverDialogResult(const int id, const EditDirectoryServiceDialog *dialog) { if (id >= 0) { keyserverModel->updateKeyserver(id, dialog->keyserver()); } else { keyserverModel->addKeyserver(dialog->keyserver()); } } void showEditKeyserverDialog(const int id, const KeyserverConfig &keyserver, const QString &windowTitle) { QPointer dialog{new EditDirectoryServiceDialog{q}}; dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setWindowModality(Qt::WindowModal); dialog->setWindowTitle(windowTitle); dialog->setKeyserver(keyserver); connect(dialog, &QDialog::accepted, q, [dialog, id, this] { handleEditKeyserverDialogResult(id, dialog); }); dialog->show(); } void addActiveDirectory() { KeyserverConfig keyserver; keyserver.setAuthentication(KeyserverAuthentication::ActiveDirectory); keyserverModel->addKeyserver(keyserver); } void addLdapServer() { showEditKeyserverDialog(-1, {}, i18nc("@title:window", "LDAP Directory Service")); } void editKeyserver(const QModelIndex &index = {}) { const auto serverIndex = index.isValid() ? index : selectedIndex(); if (!serverIndex.isValid()) { qCDebug(KLEO_UI_LOG) << __func__ << "selection is empty"; return; } const auto id = serverIndex.row(); const KeyserverConfig keyserver = keyserverModel->getKeyserver(id); if (!keyserverIsEditable(keyserver)) { qCDebug(KLEO_UI_LOG) << __func__ << "selected keyserver (id:" << id << ") cannot be modified"; return; } showEditKeyserverDialog(id, keyserver, i18nc("@title:window", "LDAP Directory Service")); } void deleteKeyserver() { const QModelIndex serverIndex = selectedIndex(); if (!serverIndex.isValid()) { qCDebug(KLEO_UI_LOG) << __func__ << "selection is empty"; return; } keyserverModel->deleteKeyserver(serverIndex.row()); } }; DirectoryServicesWidget::DirectoryServicesWidget(QWidget *parent) : QWidget{parent} , d{std::make_unique(this)} { } DirectoryServicesWidget::~DirectoryServicesWidget() = default; void DirectoryServicesWidget::setKeyservers(const std::vector &servers) { d->setKeyservers(servers); } std::vector DirectoryServicesWidget::keyservers() const { return d->keyservers(); } void DirectoryServicesWidget::setReadOnly(bool readOnly) { d->setReadOnly(readOnly); } void DirectoryServicesWidget::clear() { d->clear(); } #include "directoryserviceswidget.moc" diff --git a/src/ui/dnattributeorderconfigwidget.cpp b/src/ui/dnattributeorderconfigwidget.cpp index 33e1c2bfa..e371edabb 100644 --- a/src/ui/dnattributeorderconfigwidget.cpp +++ b/src/ui/dnattributeorderconfigwidget.cpp @@ -1,341 +1,343 @@ /* -*- c++ -*- dnattributeorderconfigwidget.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "dnattributeorderconfigwidget.h" #include "libkleo/dn.h" #include #include #include #include #include #include #include #include #include class Kleo::DNAttributeOrderConfigWidget::DNAttributeOrderConfigWidgetPrivate { public: enum { UUp = 0, Up = 1, Left = 2, Right = 3, Down = 4, DDown = 5 }; #ifndef QT_NO_TREEWIDGET QTreeWidget *availableLV = nullptr; QTreeWidget *currentLV = nullptr; #endif QToolButton *navTB[6]; #ifndef QT_NO_TREEWIDGET QTreeWidgetItem *placeHolderItem = nullptr; #endif }; #ifndef QT_NO_TREEWIDGET static void prepare(QTreeWidget *lv) { lv->setAllColumnsShowFocus(true); lv->header()->setStretchLastSection(true); lv->setHeaderLabels(QStringList() << QString() << i18n("Description")); } #endif Kleo::DNAttributeOrderConfigWidget::DNAttributeOrderConfigWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f) , d(new DNAttributeOrderConfigWidgetPrivate) { auto glay = new QGridLayout(this); glay->setContentsMargins(0, 0, 0, 0); glay->setColumnStretch(0, 1); glay->setColumnStretch(2, 1); int row = -1; ++row; glay->addWidget(new QLabel(i18n("Available attributes:"), this), row, 0); glay->addWidget(new QLabel(i18n("Current attribute order:"), this), row, 2); ++row; glay->setRowStretch(row, 1); #ifndef QT_NO_TREEWIDGET d->availableLV = new QTreeWidget(this); prepare(d->availableLV); d->availableLV->sortItems(0, Qt::AscendingOrder); glay->addWidget(d->availableLV, row, 0); d->currentLV = new QTreeWidget(this); prepare(d->currentLV); glay->addWidget(d->currentLV, row, 2); connect(d->availableLV, &QTreeWidget::itemClicked, this, &DNAttributeOrderConfigWidget::slotAvailableSelectionChanged); connect(d->currentLV, &QTreeWidget::itemClicked, this, &DNAttributeOrderConfigWidget::slotCurrentOrderSelectionChanged); d->placeHolderItem = new QTreeWidgetItem(d->availableLV); d->placeHolderItem->setText(0, QStringLiteral("_X_")); d->placeHolderItem->setText(1, i18n("All others")); #endif // the up/down/left/right arrow cross: auto xlay = new QGridLayout(); xlay->setSpacing(0); xlay->setObjectName(QStringLiteral("xlay")); xlay->setAlignment(Qt::AlignCenter); static const struct { const char *icon; int row, col; const KLazyLocalizedString tooltip; void (DNAttributeOrderConfigWidget::*slot)(); bool autorepeat; } navButtons[] = { {"go-top", 0, 1, kli18n("Move to top"), &DNAttributeOrderConfigWidget::slotDoubleUpButtonClicked, false}, {"go-up", 1, 1, kli18n("Move one up"), &DNAttributeOrderConfigWidget::slotUpButtonClicked, true}, {"go-previous", 2, 0, kli18n("Remove from current attribute order"), &DNAttributeOrderConfigWidget::slotLeftButtonClicked, false}, {"go-next", 2, 2, kli18n("Add to current attribute order"), &DNAttributeOrderConfigWidget::slotRightButtonClicked, false}, {"go-down", 3, 1, kli18n("Move one down"), &DNAttributeOrderConfigWidget::slotDownButtonClicked, true}, {"go-bottom", 4, 1, kli18n("Move to bottom"), &DNAttributeOrderConfigWidget::slotDoubleDownButtonClicked, false}, }; for (unsigned int i = 0; i < sizeof navButtons / sizeof *navButtons; ++i) { QToolButton *tb = d->navTB[i] = new QToolButton(this); tb->setIcon(QIcon::fromTheme(QLatin1String(navButtons[i].icon))); tb->setEnabled(false); tb->setToolTip(KLocalizedString(navButtons[i].tooltip).toString()); xlay->addWidget(tb, navButtons[i].row, navButtons[i].col); tb->setAutoRepeat(navButtons[i].autorepeat); connect(tb, &QToolButton::clicked, this, navButtons[i].slot); } glay->addLayout(xlay, row, 1); } Kleo::DNAttributeOrderConfigWidget::~DNAttributeOrderConfigWidget() = default; void Kleo::DNAttributeOrderConfigWidget::setAttributeOrder(const QStringList &order) { #ifndef QT_NO_TREEWIDGET // save the _X_ item: takePlaceHolderItem(); // clear the rest: d->availableLV->clear(); d->currentLV->clear(); // fill the RHS listview: QTreeWidgetItem *last = nullptr; for (QStringList::const_iterator it = order.begin(); it != order.end(); ++it) { const QString attr = (*it).toUpper(); if (attr == QLatin1String("_X_")) { takePlaceHolderItem(); d->currentLV->insertTopLevelItem(d->currentLV->topLevelItemCount(), d->placeHolderItem); last = d->placeHolderItem; } else { last = new QTreeWidgetItem(d->currentLV, last); last->setText(0, attr); last->setText(1, DN::attributeNameToLabel(attr)); } } // fill the LHS listview with what's left: const QStringList all = DN::attributeNames(); const QStringList::const_iterator end(all.end()); for (QStringList::const_iterator it = all.begin(); it != end; ++it) { if (!order.contains(*it)) { auto item = new QTreeWidgetItem(d->availableLV); item->setText(0, *it); item->setText(1, DN::attributeNameToLabel(*it)); } } if (!d->placeHolderItem->treeWidget()) { d->availableLV->addTopLevelItem(d->placeHolderItem); } #endif } void Kleo::DNAttributeOrderConfigWidget::takePlaceHolderItem() { #ifndef QT_NO_TREEWIDGET if (QTreeWidget *lv = d->placeHolderItem->treeWidget()) { lv->takeTopLevelItem(lv->indexOfTopLevelItem(d->placeHolderItem)); } #endif } QStringList Kleo::DNAttributeOrderConfigWidget::attributeOrder() const { QStringList order; #ifndef QT_NO_TREEWIDGET for (QTreeWidgetItemIterator it(d->currentLV); (*it); ++it) { order.push_back((*it)->text(0)); } #endif return order; } void Kleo::DNAttributeOrderConfigWidget::slotAvailableSelectionChanged(QTreeWidgetItem *item) { d->navTB[DNAttributeOrderConfigWidgetPrivate::Right]->setEnabled(item); } void Kleo::DNAttributeOrderConfigWidget::slotCurrentOrderSelectionChanged(QTreeWidgetItem *item) { enableDisableButtons(item); } void Kleo::DNAttributeOrderConfigWidget::enableDisableButtons(QTreeWidgetItem *item) { #ifndef QT_NO_TREEWIDGET d->navTB[DNAttributeOrderConfigWidgetPrivate::UUp]->setEnabled(item && d->currentLV->itemAbove(item)); d->navTB[DNAttributeOrderConfigWidgetPrivate::Up]->setEnabled(item && d->currentLV->itemAbove(item)); d->navTB[DNAttributeOrderConfigWidgetPrivate::Left]->setEnabled(item); d->navTB[DNAttributeOrderConfigWidgetPrivate::Down]->setEnabled(item && d->currentLV->itemBelow(item)); d->navTB[DNAttributeOrderConfigWidgetPrivate::DDown]->setEnabled(item && d->currentLV->itemBelow(item)); #endif } void Kleo::DNAttributeOrderConfigWidget::slotUpButtonClicked() { #ifndef QT_NO_TREEWIDGET if (d->currentLV->selectedItems().isEmpty()) { return; } QTreeWidgetItem *item = d->currentLV->selectedItems().first(); int itemIndex = d->currentLV->indexOfTopLevelItem(item); if (itemIndex <= 0) { return; } d->currentLV->takeTopLevelItem(itemIndex); d->currentLV->insertTopLevelItem(itemIndex - 1, item); d->currentLV->clearSelection(); item->setSelected(true); enableDisableButtons(item); Q_EMIT changed(); #endif } void Kleo::DNAttributeOrderConfigWidget::slotDoubleUpButtonClicked() { #ifndef QT_NO_TREEWIDGET if (d->currentLV->selectedItems().isEmpty()) { return; } QTreeWidgetItem *item = d->currentLV->selectedItems().first(); int itemIndex = d->currentLV->indexOfTopLevelItem(item); if (itemIndex == 0) { return; } d->currentLV->takeTopLevelItem(itemIndex); d->currentLV->insertTopLevelItem(0, item); d->currentLV->clearSelection(); item->setSelected(true); enableDisableButtons(item); Q_EMIT changed(); #endif } void Kleo::DNAttributeOrderConfigWidget::slotDownButtonClicked() { #ifndef QT_NO_TREEWIDGET if (d->currentLV->selectedItems().isEmpty()) { return; } QTreeWidgetItem *item = d->currentLV->selectedItems().first(); int itemIndex = d->currentLV->indexOfTopLevelItem(item); if (itemIndex + 1 >= d->currentLV->topLevelItemCount()) { return; } d->currentLV->takeTopLevelItem(itemIndex); d->currentLV->insertTopLevelItem(itemIndex + 1, item); d->currentLV->clearSelection(); item->setSelected(true); enableDisableButtons(item); Q_EMIT changed(); #endif } void Kleo::DNAttributeOrderConfigWidget::slotDoubleDownButtonClicked() { #ifndef QT_NO_TREEWIDGET if (d->currentLV->selectedItems().isEmpty()) { return; } QTreeWidgetItem *item = d->currentLV->selectedItems().first(); const int itemIndex = d->currentLV->indexOfTopLevelItem(item); if (itemIndex + 1 >= d->currentLV->topLevelItemCount()) { return; } d->currentLV->takeTopLevelItem(itemIndex); d->currentLV->addTopLevelItem(item); d->currentLV->clearSelection(); item->setSelected(true); enableDisableButtons(item); Q_EMIT changed(); #endif } void Kleo::DNAttributeOrderConfigWidget::slotLeftButtonClicked() { #ifndef QT_NO_TREEWIDGET if (d->currentLV->selectedItems().isEmpty()) { return; } QTreeWidgetItem *right = d->currentLV->selectedItems().first(); QTreeWidgetItem *next = d->currentLV->itemBelow(right); if (!next) { next = d->currentLV->itemAbove(right); } d->currentLV->takeTopLevelItem(d->currentLV->indexOfTopLevelItem(right)); d->availableLV->addTopLevelItem(right); d->availableLV->sortItems(0, Qt::AscendingOrder); if (next) { next->setSelected(true); } enableDisableButtons(next); Q_EMIT changed(); #endif } void Kleo::DNAttributeOrderConfigWidget::slotRightButtonClicked() { #ifndef QT_NO_TREEWIDGET if (d->availableLV->selectedItems().isEmpty()) { return; } QTreeWidgetItem *left = d->availableLV->selectedItems().first(); QTreeWidgetItem *next = d->availableLV->itemBelow(left); if (!next) { next = d->availableLV->itemAbove(left); } d->availableLV->takeTopLevelItem(d->availableLV->indexOfTopLevelItem(left)); int newRightIndex = d->currentLV->topLevelItemCount(); if (!d->currentLV->selectedItems().isEmpty()) { QTreeWidgetItem *right = d->currentLV->selectedItems().first(); newRightIndex = d->currentLV->indexOfTopLevelItem(right); right->setSelected(false); } d->currentLV->insertTopLevelItem(newRightIndex, left); left->setSelected(true); enableDisableButtons(left); d->navTB[DNAttributeOrderConfigWidgetPrivate::Right]->setEnabled(next); if (next) { next->setSelected(true); } Q_EMIT changed(); #endif } void Kleo::DNAttributeOrderConfigWidget::virtual_hook(int, void *) { } diff --git a/src/ui/editdirectoryservicedialog.cpp b/src/ui/editdirectoryservicedialog.cpp index ae802ed1a..6d3379dac 100644 --- a/src/ui/editdirectoryservicedialog.cpp +++ b/src/ui/editdirectoryservicedialog.cpp @@ -1,407 +1,409 @@ /* ui/editdirectoryservicedialog.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "editdirectoryservicedialog.h" #include "kleo/keyserverconfig.h" #include "utils/algorithm.h" #include "utils/gnupg.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; namespace { int defaultPort(KeyserverConnection connection) { return connection == KeyserverConnection::TunnelThroughTLS ? 636 : 389; } } class EditDirectoryServiceDialog::Private { EditDirectoryServiceDialog *const q; struct Ui { QLineEdit *hostEdit = nullptr; QSpinBox *portSpinBox = nullptr; QCheckBox *useDefaultPortCheckBox = nullptr; QButtonGroup *authenticationGroup = nullptr; QLineEdit *userEdit = nullptr; KPasswordLineEdit *passwordEdit = nullptr; QButtonGroup *connectionGroup = nullptr; KCollapsibleGroupBox *advancedSettings = nullptr; QLineEdit *baseDnEdit = nullptr; QLineEdit *additionalFlagsEdit = nullptr; QDialogButtonBox *buttonBox = nullptr; Ui(QWidget *parent) : hostEdit{new QLineEdit{parent}} , portSpinBox{new QSpinBox{parent}} , useDefaultPortCheckBox{new QCheckBox{parent}} , authenticationGroup{new QButtonGroup{parent}} , userEdit{new QLineEdit{parent}} , passwordEdit{new KPasswordLineEdit{parent}} , connectionGroup{new QButtonGroup{parent}} , advancedSettings{new KCollapsibleGroupBox{parent}} , baseDnEdit{new QLineEdit{parent}} , additionalFlagsEdit{new QLineEdit{parent}} , buttonBox{new QDialogButtonBox{parent}} { #define SET_OBJECT_NAME(x) x->setObjectName(QStringLiteral(#x)); SET_OBJECT_NAME(hostEdit) SET_OBJECT_NAME(portSpinBox) SET_OBJECT_NAME(useDefaultPortCheckBox) SET_OBJECT_NAME(authenticationGroup) SET_OBJECT_NAME(userEdit) SET_OBJECT_NAME(passwordEdit) SET_OBJECT_NAME(connectionGroup) SET_OBJECT_NAME(advancedSettings) SET_OBJECT_NAME(baseDnEdit) SET_OBJECT_NAME(additionalFlagsEdit) SET_OBJECT_NAME(buttonBox) #undef SET_OBJECT_NAME auto mainLayout = new QVBoxLayout{parent}; auto serverWidget = new QWidget{parent}; { auto layout = new QGridLayout{serverWidget}; layout->setColumnStretch(2, 1); int row = 0; layout->addWidget(new QLabel{i18n("Host:")}, row, 0); hostEdit->setToolTip(i18nc("@info:tooltip", "Enter the name or IP address of the server hosting the directory service.")); hostEdit->setClearButtonEnabled(true); layout->addWidget(hostEdit, row, 1, 1, -1); ++row; layout->addWidget(new QLabel{i18n("Port:")}, row, 0); portSpinBox->setRange(1, USHRT_MAX); portSpinBox->setToolTip(i18nc("@info:tooltip", "(Optional, the default is fine in most cases) " "Pick the port number the directory service is listening on.")); layout->addWidget(portSpinBox, row, 1); useDefaultPortCheckBox->setText(i18n("Use default")); useDefaultPortCheckBox->setChecked(true); layout->addWidget(useDefaultPortCheckBox, row, 2); } mainLayout->addWidget(serverWidget); auto authenticationWidget = new QGroupBox{i18n("Authentication"), parent}; { auto layout = new QVBoxLayout{authenticationWidget}; { auto radioButton = new QRadioButton{i18n("Anonymous")}; radioButton->setToolTip(i18nc("@info:tooltip", "Use an anonymous LDAP server that does not require authentication.")); radioButton->setChecked(true); authenticationGroup->addButton(radioButton, static_cast(KeyserverAuthentication::Anonymous)); layout->addWidget(radioButton); } { auto radioButton = new QRadioButton{i18n("Authenticate via Active Directory")}; if (!engineIsVersion(2, 2, 28, GpgME::GpgSMEngine)) { radioButton->setText(i18n("Authenticate via Active Directory (requires GnuPG 2.2.28 or later)")); } radioButton->setToolTip( i18nc("@info:tooltip", "On Windows, authenticate to the LDAP server using the Active Directory with the current user.")); authenticationGroup->addButton(radioButton, static_cast(KeyserverAuthentication::ActiveDirectory)); layout->addWidget(radioButton); } { auto radioButton = new QRadioButton{i18n("Authenticate with user and password")}; radioButton->setToolTip(i18nc("@info:tooltip", "Authenticate to the LDAP server with your LDAP credentials.")); authenticationGroup->addButton(radioButton, static_cast(KeyserverAuthentication::Password)); layout->addWidget(radioButton); } auto credentialsWidget = new QWidget{parent}; { auto layout = new QGridLayout{credentialsWidget}; layout->setColumnStretch(1, 1); int row = 0; layout->addWidget(new QLabel{i18n("User:")}, row, 0); userEdit->setToolTip(i18nc("@info:tooltip", "Enter your LDAP user resp. Bind DN for authenticating to the LDAP server.")); userEdit->setClearButtonEnabled(true); layout->addWidget(userEdit, row, 1); ++row; layout->addWidget(new QLabel{i18n("Password:")}, row, 0); passwordEdit->setToolTip(xi18nc("@info:tooltip", "Enter your password for authenticating to the LDAP server." "The password will be saved in the clear " "in a configuration file in your home directory.")); passwordEdit->setClearButtonEnabled(true); layout->addWidget(passwordEdit, row, 1); } layout->addWidget(credentialsWidget); } mainLayout->addWidget(authenticationWidget); auto securityWidget = new QGroupBox{i18n("Connection Security"), parent}; if (!engineIsVersion(2, 2, 28, GpgME::GpgSMEngine)) { securityWidget->setTitle(i18n("Connection Security (requires GnuPG 2.2.28 or later)")); } { auto layout = new QVBoxLayout{securityWidget}; { auto radioButton = new QRadioButton{i18n("Use default connection (probably not TLS secured)")}; radioButton->setToolTip(i18nc("@info:tooltip", "Use GnuPG's default to connect to the LDAP server. " "By default, GnuPG 2.3 and earlier use a plain, not TLS secured connection. " "(Not recommended)")); radioButton->setChecked(true); connectionGroup->addButton(radioButton, static_cast(KeyserverConnection::Default)); layout->addWidget(radioButton); } { auto radioButton = new QRadioButton{i18n("Do not use a TLS secured connection")}; radioButton->setToolTip(i18nc("@info:tooltip", "Use a plain, not TLS secured connection to connect to the LDAP server. " "(Not recommended)")); connectionGroup->addButton(radioButton, static_cast(KeyserverConnection::Plain)); layout->addWidget(radioButton); } { auto radioButton = new QRadioButton{i18n("Use TLS secured connection")}; radioButton->setToolTip(i18nc("@info:tooltip", "Use a standard TLS secured connection (initiated with STARTTLS) " "to connect to the LDAP server. " "(Recommended)")); connectionGroup->addButton(radioButton, static_cast(KeyserverConnection::UseSTARTTLS)); layout->addWidget(radioButton); } { auto radioButton = new QRadioButton{i18n("Tunnel LDAP through a TLS connection")}; radioButton->setToolTip(i18nc("@info:tooltip", "Use a TLS secured connection through which the connection to the " "LDAP server is tunneled. " "(Not recommended)")); connectionGroup->addButton(radioButton, static_cast(KeyserverConnection::TunnelThroughTLS)); layout->addWidget(radioButton); } } mainLayout->addWidget(securityWidget); advancedSettings->setTitle(i18n("Advanced Settings")); { auto layout = new QGridLayout{advancedSettings}; layout->setColumnStretch(1, 1); int row = 0; layout->addWidget(new QLabel{i18n("Base DN:")}, row, 0); baseDnEdit->setToolTip(i18nc("@info:tooltip", "(Optional, can usually be left empty) " "Enter the base DN for this LDAP server to limit searches " "to only that subtree of the directory.")); baseDnEdit->setClearButtonEnabled(true); layout->addWidget(baseDnEdit, row, 1); ++row; layout->addWidget(new QLabel{i18n("Additional flags:")}, row, 0); additionalFlagsEdit->setToolTip(i18nc("@info:tooltip", "Here you can enter additional flags that are not yet (or no longer) " "supported by Kleopatra. For example, older versions of GnuPG use " "ldaps to request a TLS secured connection.")); additionalFlagsEdit->setClearButtonEnabled(true); layout->addWidget(additionalFlagsEdit, row, 1); } mainLayout->addWidget(advancedSettings); mainLayout->addStretch(1); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); KGuiItem::assign(okButton, KStandardGuiItem::ok()); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel()); mainLayout->addWidget(buttonBox); }; } ui; QString host() const { return ui.hostEdit->text().trimmed(); } int port() const { return ui.useDefaultPortCheckBox->isChecked() ? -1 : ui.portSpinBox->value(); } KeyserverAuthentication authentication() const { return KeyserverAuthentication{ui.authenticationGroup->checkedId()}; } QString user() const { return ui.userEdit->text().trimmed(); } QString password() const { return ui.passwordEdit->password(); // not trimmed } KeyserverConnection connection() const { return KeyserverConnection{ui.connectionGroup->checkedId()}; } QString baseDn() const { return ui.baseDnEdit->text().trimmed(); } QStringList additionalFlags() const { return transformInPlace(ui.additionalFlagsEdit->text().split(QLatin1Char{','}, Qt::SkipEmptyParts), [](const auto &flag) { return flag.trimmed(); }); } bool inputIsAcceptable() const { const bool hostIsSet = !host().isEmpty(); const bool requiredCredentialsAreSet = authentication() != KeyserverAuthentication::Password || (!user().isEmpty() && !password().isEmpty()); return hostIsSet && requiredCredentialsAreSet; } void updateWidgets() { ui.portSpinBox->setEnabled(!ui.useDefaultPortCheckBox->isChecked()); if (ui.useDefaultPortCheckBox->isChecked()) { ui.portSpinBox->setValue(defaultPort(connection())); } ui.userEdit->setEnabled(authentication() == KeyserverAuthentication::Password); ui.passwordEdit->setEnabled(authentication() == KeyserverAuthentication::Password); ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(inputIsAcceptable()); } public: Private(EditDirectoryServiceDialog *q) : q{q} , ui{q} { connect(ui.hostEdit, &QLineEdit::textEdited, q, [this]() { updateWidgets(); }); connect(ui.useDefaultPortCheckBox, &QCheckBox::toggled, q, [this]() { updateWidgets(); }); connect(ui.authenticationGroup, &QButtonGroup::idToggled, q, [this]() { updateWidgets(); }); connect(ui.userEdit, &QLineEdit::textEdited, q, [this]() { updateWidgets(); }); connect(ui.passwordEdit, &KPasswordLineEdit::passwordChanged, q, [this]() { updateWidgets(); }); connect(ui.connectionGroup, &QButtonGroup::idToggled, q, [this]() { updateWidgets(); }); connect(ui.buttonBox, &QDialogButtonBox::accepted, q, &EditDirectoryServiceDialog::accept); connect(ui.buttonBox, &QDialogButtonBox::rejected, q, &EditDirectoryServiceDialog::reject); updateWidgets(); restoreLayout(); } ~Private() { saveLayout(); } void setKeyserver(const KeyserverConfig &keyserver) { ui.hostEdit->setText(keyserver.host()); ui.useDefaultPortCheckBox->setChecked(keyserver.port() == -1); ui.portSpinBox->setValue(keyserver.port() == -1 ? defaultPort(keyserver.connection()) : keyserver.port()); ui.authenticationGroup->button(static_cast(keyserver.authentication()))->setChecked(true); ui.userEdit->setText(keyserver.user()); ui.passwordEdit->setPassword(keyserver.password()); ui.connectionGroup->button(static_cast(keyserver.connection()))->setChecked(true); ui.baseDnEdit->setText(keyserver.ldapBaseDn()); ui.additionalFlagsEdit->setText(keyserver.additionalFlags().join(QLatin1Char{','})); ui.advancedSettings->setExpanded(!keyserver.ldapBaseDn().isEmpty() || !keyserver.additionalFlags().empty()); updateWidgets(); } KeyserverConfig keyserver() const { KeyserverConfig keyserver; keyserver.setHost(host()); keyserver.setPort(port()); keyserver.setAuthentication(authentication()); keyserver.setUser(user()); keyserver.setPassword(password()); keyserver.setConnection(connection()); keyserver.setLdapBaseDn(baseDn()); keyserver.setAdditionalFlags(additionalFlags()); return keyserver; } private: void saveLayout() { KConfigGroup configGroup{KSharedConfig::openStateConfig(), "EditDirectoryServiceDialog"}; configGroup.writeEntry("Size", q->size()); configGroup.sync(); } void restoreLayout() { const KConfigGroup configGroup{KSharedConfig::openStateConfig(), "EditDirectoryServiceDialog"}; const auto size = configGroup.readEntry("Size", QSize{}); if (size.isValid()) { q->resize(size); } } }; EditDirectoryServiceDialog::EditDirectoryServiceDialog(QWidget *parent, Qt::WindowFlags f) : QDialog{parent, f} , d{std::make_unique(this)} { setWindowTitle(i18nc("@title:window", "Edit Directory Service")); } EditDirectoryServiceDialog::~EditDirectoryServiceDialog() = default; void EditDirectoryServiceDialog::setKeyserver(const KeyserverConfig &keyserver) { d->setKeyserver(keyserver); } KeyserverConfig EditDirectoryServiceDialog::keyserver() const { return d->keyserver(); } diff --git a/src/ui/filenamerequester.cpp b/src/ui/filenamerequester.cpp index 1a7a67bd7..bb1ec2006 100644 --- a/src/ui/filenamerequester.cpp +++ b/src/ui/filenamerequester.cpp @@ -1,205 +1,207 @@ /* -*- mode: c++; c-basic-offset:4 -*- ui/filenamerequester.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "filenamerequester.h" #include #include #include #include #include #include #include #include #include using namespace Kleo; class Q_DECL_HIDDEN FileNameRequester::FileNameRequesterPrivate { friend class ::Kleo::FileNameRequester; FileNameRequester *const q; public: explicit FileNameRequesterPrivate(FileNameRequester *qq); ~FileNameRequesterPrivate(); private: void slotButtonClicked(); private: #ifndef QT_NO_DIRMODEL QFileSystemModel dirmodel; QCompleter completer; #else QDir::Filters filter; #endif QLineEdit lineedit; QToolButton button; QHBoxLayout hlay; QString nameFilter; bool existingOnly; }; FileNameRequester::FileNameRequesterPrivate::FileNameRequesterPrivate(FileNameRequester *qq) : q(qq) , #ifndef QT_NO_DIRMODEL dirmodel() , completer(&dirmodel) , #else filter() , #endif lineedit(q) , button(q) , hlay(q) , nameFilter() , existingOnly(true) { #ifndef QT_NO_DIRMODEL dirmodel.setObjectName(QStringLiteral("dirmodel")); completer.setObjectName(QStringLiteral("completer")); #endif lineedit.setObjectName(QStringLiteral("lineedit")); button.setObjectName(QStringLiteral("button")); hlay.setObjectName(QStringLiteral("hlay")); button.setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); button.setToolTip(i18n("Open file dialog")); button.setAccessibleName(i18n("Open file dialog")); #ifndef QT_NO_DIRMODEL lineedit.setCompleter(&completer); #endif lineedit.setClearButtonEnabled(true); hlay.setContentsMargins(0, 0, 0, 0); hlay.addWidget(&lineedit); hlay.addWidget(&button); q->setFocusPolicy(lineedit.focusPolicy()); q->setFocusProxy(&lineedit); connect(&button, &QToolButton::clicked, q, [this]() { slotButtonClicked(); }); connect(&lineedit, &QLineEdit::textChanged, q, &FileNameRequester::fileNameChanged); } FileNameRequester::FileNameRequesterPrivate::~FileNameRequesterPrivate() { } FileNameRequester::FileNameRequester(QWidget *p) : QWidget(p) , d(new FileNameRequesterPrivate(this)) { } FileNameRequester::FileNameRequester(QDir::Filters f, QWidget *p) : QWidget(p) , d(new FileNameRequesterPrivate(this)) { #ifndef QT_NO_DIRMODEL d->dirmodel.setFilter(f); #else d->filter = f; #endif } FileNameRequester::~FileNameRequester() = default; void FileNameRequester::setFileName(const QString &file) { d->lineedit.setText(file); } QString FileNameRequester::fileName() const { return d->lineedit.text(); } void FileNameRequester::setExistingOnly(bool on) { d->existingOnly = on; } bool FileNameRequester::existingOnly() const { return d->existingOnly; } void FileNameRequester::setFilter(QDir::Filters f) { #ifndef QT_NO_DIRMODEL d->dirmodel.setFilter(f); #else d->filter = f; #endif } QDir::Filters FileNameRequester::filter() const { #ifndef QT_NO_DIRMODEL return d->dirmodel.filter(); #else return d->filter; #endif } void FileNameRequester::setNameFilter(const QString &nameFilter) { d->nameFilter = nameFilter; } QString FileNameRequester::nameFilter() const { return d->nameFilter; } void FileNameRequester::setAccessibleNameOfLineEdit(const QString &name) { d->lineedit.setAccessibleName(name); } void FileNameRequester::FileNameRequesterPrivate::slotButtonClicked() { const QString fileName = q->requestFileName(); if (!fileName.isEmpty()) { q->setFileName(fileName); } } bool FileNameRequester::event(QEvent *e) { if (e->type() == QEvent::ToolTipChange) { d->lineedit.setToolTip(toolTip()); } return QWidget::event(e); } QString FileNameRequester::requestFileName() { #ifndef QT_NO_FILEDIALOG const QDir::Filters filters = filter(); if ((filters & QDir::Dirs) && !(filters & QDir::Files)) { return QFileDialog::getExistingDirectory(this); } else if (d->existingOnly) { return QFileDialog::getOpenFileName(this, QString(), QString(), d->nameFilter); } else { return QFileDialog::getSaveFileName(this, QString(), fileName(), d->nameFilter); } #else return QString(); #endif } #include "moc_filenamerequester.cpp" diff --git a/src/ui/keyapprovaldialog.cpp b/src/ui/keyapprovaldialog.cpp index 76678e156..c5660bd64 100644 --- a/src/ui/keyapprovaldialog.cpp +++ b/src/ui/keyapprovaldialog.cpp @@ -1,205 +1,207 @@ /* -*- c++ -*- keyapprovaldialog.h This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB Based on kpgpui.h SPDX-FileCopyrightText: 2001, 2002 the KPGP authors See file libkdenetwork/AUTHORS.kpgp for details SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "keyapprovaldialog.h" #include "keyrequester.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static Kleo::EncryptionPreference cb2pref(int i) { switch (i) { default: case 0: return Kleo::UnknownPreference; case 1: return Kleo::NeverEncrypt; case 2: return Kleo::AlwaysEncrypt; case 3: return Kleo::AlwaysEncryptIfPossible; case 4: return Kleo::AlwaysAskForEncryption; case 5: return Kleo::AskWheneverPossible; } } static int pref2cb(Kleo::EncryptionPreference p) { switch (p) { default: return 0; case Kleo::NeverEncrypt: return 1; case Kleo::AlwaysEncrypt: return 2; case Kleo::AlwaysEncryptIfPossible: return 3; case Kleo::AlwaysAskForEncryption: return 4; case Kleo::AskWheneverPossible: return 5; } } static QStringList preferencesStrings() { return QStringList() << xi18n("none") // << i18n("Never Encrypt with This Key") // << i18n("Always Encrypt with This Key") // << i18n("Encrypt Whenever Encryption is Possible") // << i18n("Always Ask") // << i18n("Ask Whenever Encryption is Possible"); } class Q_DECL_HIDDEN Kleo::KeyApprovalDialog::KeyApprovalDialogPrivate { public: KeyApprovalDialogPrivate() { } Kleo::KeyRequester *selfRequester = nullptr; QStringList addresses; std::vector requesters; std::vector preferences; bool prefsChanged = false; }; Kleo::KeyApprovalDialog::KeyApprovalDialog(const std::vector &recipients, const std::vector &sender, QWidget *parent) : QDialog(parent) , d(new KeyApprovalDialogPrivate()) { setWindowTitle(i18nc("@title:window", "Encryption Key Approval")); auto mainLayout = new QVBoxLayout(this); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &KeyApprovalDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &KeyApprovalDialog::reject); okButton->setDefault(true); Q_ASSERT(!recipients.empty()); QFrame *page = new QFrame(this); mainLayout->addWidget(page); mainLayout->addWidget(buttonBox); auto vlay = new QVBoxLayout(page); vlay->setContentsMargins(0, 0, 0, 0); vlay->addWidget(new QLabel(i18n("The following keys will be used for encryption:"), page)); auto sv = new QScrollArea(page); sv->setWidgetResizable(true); vlay->addWidget(sv); QWidget *view = new QWidget(sv->viewport()); auto glay = new QGridLayout(view); glay->setColumnStretch(1, 1); sv->setWidget(view); int row = -1; if (!sender.empty()) { ++row; glay->addWidget(new QLabel(i18n("Your keys:"), view), row, 0); d->selfRequester = new EncryptionKeyRequester(true, EncryptionKeyRequester::AllProtocols, view); d->selfRequester->setKeys(sender); glay->addWidget(d->selfRequester, row, 1); ++row; glay->addWidget(new KSeparator(Qt::Horizontal, view), row, 0, 1, 2); } const QStringList prefs = preferencesStrings(); for (auto it = recipients.begin(); it != recipients.end(); ++it) { ++row; glay->addWidget(new QLabel(i18n("Recipient:"), view), row, 0); glay->addWidget(new QLabel(it->address, view), row, 1); d->addresses.push_back(it->address); ++row; glay->addWidget(new QLabel(i18n("Encryption keys:"), view), row, 0); KeyRequester *req = new EncryptionKeyRequester(true, EncryptionKeyRequester::AllProtocols, view); req->setKeys(it->keys); glay->addWidget(req, row, 1); d->requesters.push_back(req); ++row; glay->addWidget(new QLabel(i18n("Encryption preference:"), view), row, 0); auto cb = new QComboBox(view); cb->setEditable(false); cb->addItems(prefs); glay->addWidget(cb, row, 1); cb->setCurrentIndex(pref2cb(it->pref)); connect(cb, qOverload(&QComboBox::activated), this, &KeyApprovalDialog::slotPrefsChanged); d->preferences.push_back(cb); } QSize size = sizeHint(); // don't make the dialog too large const QSize desk = screen()->size(); resize(QSize(qMin(size.width(), 3 * desk.width() / 4), qMin(size.height(), 7 * desk.height() / 8))); } Kleo::KeyApprovalDialog::~KeyApprovalDialog() = default; std::vector Kleo::KeyApprovalDialog::senderKeys() const { return d->selfRequester ? d->selfRequester->keys() : std::vector(); } std::vector Kleo::KeyApprovalDialog::items() const { Q_ASSERT(d->requesters.size() == static_cast(d->addresses.size())); Q_ASSERT(d->requesters.size() == d->preferences.size()); std::vector result; result.reserve(d->requesters.size()); QStringList::const_iterator ait = d->addresses.constBegin(); auto rit = d->requesters.begin(); auto cit = d->preferences.begin(); while (ait != d->addresses.constEnd()) { result.push_back(Item(*ait++, (*rit++)->keys(), cb2pref((*cit++)->currentIndex()))); } return result; } bool Kleo::KeyApprovalDialog::preferencesChanged() const { return d->prefsChanged; } void Kleo::KeyApprovalDialog::slotPrefsChanged() { d->prefsChanged = true; } diff --git a/src/ui/keylistview.cpp b/src/ui/keylistview.cpp index 5c478313e..b595ec5a9 100644 --- a/src/ui/keylistview.cpp +++ b/src/ui/keylistview.cpp @@ -1,556 +1,558 @@ /* keylistview.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "keylistview.h" #include #include #include #include #include #include #include #include #include #include #include static const int updateDelayMilliSecs = 500; class Q_DECL_HIDDEN Kleo::KeyListView::KeyListViewPrivate { public: KeyListViewPrivate() : updateTimer(nullptr) { } std::vector keyBuffer; QTimer *updateTimer = nullptr; std::map itemMap; }; // a list of signals where we want to replace QListViewItem with // Kleo:KeyListViewItem: static const struct { const char *source; const char *target; } signalReplacements[] = { { SIGNAL(itemDoubleClicked(QTreeWidgetItem *, int)), SLOT(slotEmitDoubleClicked(QTreeWidgetItem *, int)), }, { SIGNAL(itemSelectionChanged()), SLOT(slotEmitSelectionChanged()), }, { SIGNAL(customContextMenuRequested(QPoint)), SLOT(slotEmitContextMenu(QPoint)), }, }; static const int numSignalReplacements = sizeof signalReplacements / sizeof *signalReplacements; Kleo::KeyListView::KeyListView(const ColumnStrategy *columnStrategy, const DisplayStrategy *displayStrategy, QWidget *parent, Qt::WindowFlags f) : QTreeWidget(parent) , mColumnStrategy(columnStrategy) , mDisplayStrategy(displayStrategy) , mHierarchical(false) , d(new KeyListViewPrivate()) { setWindowFlags(f); setContextMenuPolicy(Qt::CustomContextMenu); d->updateTimer = new QTimer(this); d->updateTimer->setSingleShot(true); connect(d->updateTimer, &QTimer::timeout, this, &KeyListView::slotUpdateTimeout); if (!columnStrategy) { qCWarning(KLEO_UI_LOG) << "Kleo::KeyListView: need a column strategy to work with!"; return; } const QFontMetrics fm = fontMetrics(); for (int col = 0; !columnStrategy->title(col).isEmpty(); ++col) { headerItem()->setText(col, columnStrategy->title(col)); header()->resizeSection(col, columnStrategy->width(col, fm)); header()->setSectionResizeMode(col, columnStrategy->resizeMode(col)); } setAllColumnsShowFocus(true); for (int i = 0; i < numSignalReplacements; ++i) { connect(this, signalReplacements[i].source, signalReplacements[i].target); } this->setToolTip(QString()); viewport()->setToolTip(QString()); // make double sure :) } Kleo::KeyListView::~KeyListView() { d->updateTimer->stop(); // need to clear here, since in ~QListView, our children won't have // a valid listView() pointing to us anymore, and their dtors try to // unregister from us. clear(); Q_ASSERT(d->itemMap.size() == 0); // need to delete the tooltip ourselves, as ~QToolTip isn't virtual :o delete mColumnStrategy; mColumnStrategy = nullptr; delete mDisplayStrategy; mDisplayStrategy = nullptr; } void Kleo::KeyListView::takeItem(QTreeWidgetItem *qlvi) { // qCDebug(KLEO_UI_LOG) <<"Kleo::KeyListView::takeItem(" << qlvi <<" )"; if (auto *item = lvi_cast(qlvi)) { deregisterItem(item); } takeTopLevelItem(indexOfTopLevelItem(qlvi)); } void Kleo::KeyListView::setHierarchical(bool hier) { if (hier == mHierarchical) { return; } mHierarchical = hier; if (hier) { gatherScattered(); } else { scatterGathered(firstChild()); } } void Kleo::KeyListView::slotAddKey(const GpgME::Key &key) { if (key.isNull()) { return; } d->keyBuffer.push_back(key); if (!d->updateTimer->isActive()) { d->updateTimer->start(updateDelayMilliSecs); } } void Kleo::KeyListView::slotUpdateTimeout() { if (d->keyBuffer.empty()) { return; } const bool wasUpdatesEnabled = viewport()->updatesEnabled(); if (wasUpdatesEnabled) { viewport()->setUpdatesEnabled(false); } qCDebug(KLEO_UI_LOG) << "Kleo::KeyListView::slotUpdateTimeout(): processing" << d->keyBuffer.size() << "items en block"; if (hierarchical()) { for (std::vector::const_iterator it = d->keyBuffer.begin(); it != d->keyBuffer.end(); ++it) { doHierarchicalInsert(*it); } gatherScattered(); } else { for (std::vector::const_iterator it = d->keyBuffer.begin(); it != d->keyBuffer.end(); ++it) { (void)new KeyListViewItem(this, *it); } } if (wasUpdatesEnabled) { viewport()->setUpdatesEnabled(true); } d->keyBuffer.clear(); } void Kleo::KeyListView::clear() { d->updateTimer->stop(); d->keyBuffer.clear(); while (QTreeWidgetItem *item = topLevelItem(0)) { delete item; } QTreeWidget::clear(); } void Kleo::KeyListView::registerItem(KeyListViewItem *item) { // qCDebug(KLEO_UI_LOG) <<"registerItem(" << item <<" )"; if (!item) { return; } const QByteArray fpr = item->key().primaryFingerprint(); if (!fpr.isEmpty()) { d->itemMap.insert(std::make_pair(fpr, item)); } } void Kleo::KeyListView::deregisterItem(const KeyListViewItem *item) { // qCDebug(KLEO_UI_LOG) <<"deregisterItem( KeyLVI:" << item <<" )"; if (!item) { return; } auto it = d->itemMap.find(item->key().primaryFingerprint()); if (it == d->itemMap.end()) { return; } // This Q_ASSERT triggers, though it shouldn't. Print some more // information when it happens. // Q_ASSERT( it->second == item ); if (it->second != item) { qCWarning(KLEO_UI_LOG) << "deregisterItem:" << "item " << item->key().primaryFingerprint() // << "it->second" << (it->second ? it->second->key().primaryFingerprint() : "is null"); return; } d->itemMap.erase(it); } void Kleo::KeyListView::doHierarchicalInsert(const GpgME::Key &key) { const QByteArray fpr = key.primaryFingerprint(); if (fpr.isEmpty()) { return; } KeyListViewItem *item = nullptr; if (!key.isRoot()) { if (KeyListViewItem *parent = itemByFingerprint(key.chainID())) { item = new KeyListViewItem(parent, key); parent->setExpanded(true); } } if (!item) { item = new KeyListViewItem(this, key); // top-level (for now) } d->itemMap.insert(std::make_pair(fpr, item)); } void Kleo::KeyListView::gatherScattered() { KeyListViewItem *item = firstChild(); while (item) { KeyListViewItem *cur = item; item = item->nextSibling(); if (cur->key().isRoot()) { continue; } if (KeyListViewItem *parent = itemByFingerprint(cur->key().chainID())) { // found a new parent... // ### todo: optimize by suppressing removing/adding the item to the itemMap... takeTopLevelItem(indexOfTopLevelItem(cur)); parent->addChild(cur); parent->setExpanded(true); } } } void Kleo::KeyListView::scatterGathered(KeyListViewItem *start) { KeyListViewItem *item = start; while (item) { KeyListViewItem *cur = item; item = item->nextSibling(); scatterGathered(Kleo::lvi_cast(cur->child(0))); Q_ASSERT(cur->childCount() == 0); // ### todo: optimize by suppressing removing/adding the item to the itemMap... if (cur->parent()) { static_cast(cur->parent())->takeItem(cur); } else { takeItem(cur); } addTopLevelItem(cur); } } Kleo::KeyListViewItem *Kleo::KeyListView::itemByFingerprint(const QByteArray &s) const { if (s.isEmpty()) { return nullptr; } const std::map::const_iterator it = d->itemMap.find(s); if (it == d->itemMap.end()) { return nullptr; } return it->second; } void Kleo::KeyListView::slotRefreshKey(const GpgME::Key &key) { const char *fpr = key.primaryFingerprint(); if (!fpr) { return; } if (KeyListViewItem *item = itemByFingerprint(fpr)) { item->setKey(key); } else { // none found -> add it slotAddKey(key); } } // slots for the emission of covariant Q_SIGNALS: void Kleo::KeyListView::slotEmitDoubleClicked(QTreeWidgetItem *item, int col) { if (!item || lvi_cast(item)) { Q_EMIT doubleClicked(static_cast(item), col); } } void Kleo::KeyListView::slotEmitReturnPressed(QTreeWidgetItem *item) { if (!item || lvi_cast(item)) { Q_EMIT returnPressed(static_cast(item)); } } void Kleo::KeyListView::slotEmitSelectionChanged() { Q_EMIT selectionChanged(selectedItem()); } void Kleo::KeyListView::slotEmitContextMenu(const QPoint &pos) { QTreeWidgetItem *item = itemAt(pos); if (!item || lvi_cast(item)) { Q_EMIT contextMenu(static_cast(item), viewport()->mapToGlobal(pos)); } } // // // KeyListViewItem // // Kleo::KeyListViewItem::KeyListViewItem(KeyListView *parent, const GpgME::Key &key) : QTreeWidgetItem(parent, RTTI) { Q_ASSERT(parent); setKey(key); } Kleo::KeyListViewItem::KeyListViewItem(KeyListView *parent, KeyListViewItem *after, const GpgME::Key &key) : QTreeWidgetItem(parent, after, RTTI) { Q_ASSERT(parent); setKey(key); } Kleo::KeyListViewItem::KeyListViewItem(KeyListViewItem *parent, const GpgME::Key &key) : QTreeWidgetItem(parent, RTTI) { Q_ASSERT(parent && parent->listView()); setKey(key); } Kleo::KeyListViewItem::KeyListViewItem(KeyListViewItem *parent, KeyListViewItem *after, const GpgME::Key &key) : QTreeWidgetItem(parent, after, RTTI) { Q_ASSERT(parent && parent->listView()); setKey(key); } Kleo::KeyListViewItem::~KeyListViewItem() { // delete the children first... When children are deleted in the // QLVI dtor, they don't have listView() anymore, thus they don't // call deregister( this ), leading to stale entries in the // itemMap... while (QTreeWidgetItem *item = child(0)) { delete item; } // better do this here, too, since deletion is top-down and thus // we're deleted when our parent item is no longer a // KeyListViewItem, but a mere QListViewItem, so our takeItem() // overload is gone by that time... if (KeyListView *lv = listView()) { lv->deregisterItem(this); } } void Kleo::KeyListViewItem::setKey(const GpgME::Key &key) { KeyListView *lv = listView(); if (lv) { lv->deregisterItem(this); } mKey = key; if (lv) { lv->registerItem(this); } // the ColumnStrategy operations might be very slow, so cache their // result here, where we're non-const :) const Kleo::KeyListView::ColumnStrategy *cs = lv ? lv->columnStrategy() : nullptr; if (!cs) { return; } const KeyListView::DisplayStrategy *ds = lv->displayStrategy(); const int numCols = lv ? lv->columnCount() : 0; for (int i = 0; i < numCols; ++i) { setText(i, cs->text(key, i)); setToolTip(i, cs->toolTip(key, i)); const QIcon icon = cs->icon(key, i); if (!icon.isNull()) { setIcon(i, icon); } if (ds) { setForeground(i, QBrush(ds->keyForeground(key, foreground(i).color()))); setBackground(i, QBrush(ds->keyBackground(key, background(i).color()))); setFont(i, ds->keyFont(key, font(i))); } } } QString Kleo::KeyListViewItem::toolTip(int col) const { return listView() && listView()->columnStrategy() ? listView()->columnStrategy()->toolTip(key(), col) : QString(); } bool Kleo::KeyListViewItem::operator<(const QTreeWidgetItem &other) const { if (other.type() != RTTI || !listView() || !listView()->columnStrategy()) { return QTreeWidgetItem::operator<(other); } const auto that = static_cast(&other); return listView()->columnStrategy()->compare(this->key(), that->key(), treeWidget()->sortColumn()) < 0; } void Kleo::KeyListViewItem::takeItem(QTreeWidgetItem *qlvi) { // qCDebug(KLEO_UI_LOG) <<"Kleo::KeyListViewItem::takeItem(" << qlvi <<" )"; if (auto *item = lvi_cast(qlvi)) { listView()->deregisterItem(item); } takeChild(indexOfChild(qlvi)); } // // // ColumnStrategy // // Kleo::KeyListView::ColumnStrategy::~ColumnStrategy() { } int Kleo::KeyListView::ColumnStrategy::compare(const GpgME::Key &key1, const GpgME::Key &key2, const int col) const { return QString::localeAwareCompare(text(key1, col), text(key2, col)); } int Kleo::KeyListView::ColumnStrategy::width(int col, const QFontMetrics &fm) const { return fm.horizontalAdvance(title(col)) * 2; } QString Kleo::KeyListView::ColumnStrategy::toolTip(const GpgME::Key &key, int col) const { return text(key, col); } // // // DisplayStrategy // // Kleo::KeyListView::DisplayStrategy::~DisplayStrategy() { } // font QFont Kleo::KeyListView::DisplayStrategy::keyFont(const GpgME::Key &, const QFont &font) const { return font; } // foreground QColor Kleo::KeyListView::DisplayStrategy::keyForeground(const GpgME::Key &, const QColor &fg) const { return fg; } // background QColor Kleo::KeyListView::DisplayStrategy::keyBackground(const GpgME::Key &, const QColor &bg) const { return bg; } // // // Collection of covariant return reimplementations of QListView(Item) // members: // // Kleo::KeyListView *Kleo::KeyListViewItem::listView() const { return static_cast(QTreeWidgetItem::treeWidget()); } Kleo::KeyListViewItem *Kleo::KeyListViewItem::nextSibling() const { if (parent()) { const int myIndex = parent()->indexOfChild(const_cast(this)); return static_cast(parent()->child(myIndex + 1)); } const int myIndex = treeWidget()->indexOfTopLevelItem(const_cast(this)); return static_cast(treeWidget()->topLevelItem(myIndex + 1)); } Kleo::KeyListViewItem *Kleo::KeyListView::firstChild() const { return static_cast(topLevelItem(0)); } Kleo::KeyListViewItem *Kleo::KeyListView::selectedItem() const { QList selection = selectedItems(); if (selection.isEmpty()) { return nullptr; } return selection.first(); } QList Kleo::KeyListView::selectedItems() const { QList result; const auto selectedItems = QTreeWidget::selectedItems(); for (QTreeWidgetItem *selectedItem : selectedItems) { if (auto *i = Kleo::lvi_cast(selectedItem)) { result.append(i); } } return result; } bool Kleo::KeyListView::isMultiSelection() const { return selectionMode() == ExtendedSelection || selectionMode() == MultiSelection; } void Kleo::KeyListView::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { if (selectedItem()) { slotEmitReturnPressed(selectedItem()); } } QTreeView::keyPressEvent(event); } diff --git a/src/ui/keyrequester.cpp b/src/ui/keyrequester.cpp index bba970b28..792ae993e 100644 --- a/src/ui/keyrequester.cpp +++ b/src/ui/keyrequester.cpp @@ -1,490 +1,492 @@ /* -*- c++ -*- keyrequester.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB Based on kpgpui.cpp SPDX-FileCopyrightText: 2001, 2002 the KPGP authors See file libkdenetwork/AUTHORS.kpgp for details This file is part of KPGP, the KDE PGP/GnuPG support library. SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "keyrequester.h" #include "keyselectiondialog.h" #include "libkleo/dn.h" // gpgme++ #include #include #include // KDE #include #include #include #include // Qt #include #include #include using namespace QGpgME; Kleo::KeyRequester::KeyRequester(unsigned int allowedKeys, bool multipleKeys, QWidget *parent) : QWidget(parent) , mOpenPGPBackend(nullptr) , mSMIMEBackend(nullptr) , mMulti(multipleKeys) , mKeyUsage(allowedKeys) , mJobs(0) , d(nullptr) { init(); } Kleo::KeyRequester::KeyRequester(QWidget *parent) : QWidget(parent) , mOpenPGPBackend(nullptr) , mSMIMEBackend(nullptr) , mMulti(false) , mKeyUsage(0) , mJobs(0) , d(nullptr) { init(); } void Kleo::KeyRequester::init() { auto hlay = new QHBoxLayout(this); hlay->setContentsMargins(0, 0, 0, 0); // the label where the key id is to be displayed: mLabel = new QLabel(this); mLabel->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); // the button to unset any key: mEraseButton = new QPushButton(this); mEraseButton->setAutoDefault(false); mEraseButton->setSizePolicy(QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum)); mEraseButton->setIcon( QIcon::fromTheme(QApplication::isRightToLeft() ? QStringLiteral("edit-clear-locationbar-ltr") : QStringLiteral("edit-clear-locationbar-rtl"))); mEraseButton->setToolTip(i18n("Clear")); // the button to call the KeySelectionDialog: mDialogButton = new QPushButton(i18n("Change..."), this); mDialogButton->setAutoDefault(false); hlay->addWidget(mLabel, 1); hlay->addWidget(mEraseButton); hlay->addWidget(mDialogButton); connect(mEraseButton, &QPushButton::clicked, this, &SigningKeyRequester::slotEraseButtonClicked); connect(mDialogButton, &QPushButton::clicked, this, &SigningKeyRequester::slotDialogButtonClicked); setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed)); setAllowedKeys(mKeyUsage); } Kleo::KeyRequester::~KeyRequester() { } const std::vector &Kleo::KeyRequester::keys() const { return mKeys; } const GpgME::Key &Kleo::KeyRequester::key() const { static const GpgME::Key null = GpgME::Key::null; if (mKeys.empty()) { return null; } else { return mKeys.front(); } } void Kleo::KeyRequester::setKeys(const std::vector &keys) { mKeys.clear(); for (auto it = keys.begin(); it != keys.end(); ++it) { if (!it->isNull()) { mKeys.push_back(*it); } } updateKeys(); } void Kleo::KeyRequester::setKey(const GpgME::Key &key) { mKeys.clear(); if (!key.isNull()) { mKeys.push_back(key); } updateKeys(); } QString Kleo::KeyRequester::fingerprint() const { if (mKeys.empty()) { return QString(); } else { return QLatin1String(mKeys.front().primaryFingerprint()); } } QStringList Kleo::KeyRequester::fingerprints() const { QStringList result; for (auto it = mKeys.begin(); it != mKeys.end(); ++it) { if (!it->isNull()) { if (const char *fpr = it->primaryFingerprint()) { result.push_back(QLatin1String(fpr)); } } } return result; } void Kleo::KeyRequester::setFingerprint(const QString &fingerprint) { startKeyListJob(QStringList(fingerprint)); } void Kleo::KeyRequester::setFingerprints(const QStringList &fingerprints) { startKeyListJob(fingerprints); } void Kleo::KeyRequester::updateKeys() { if (mKeys.empty()) { mLabel->clear(); return; } if (mKeys.size() > 1) { setMultipleKeysEnabled(true); } QStringList labelTexts; QString toolTipText; for (std::vector::const_iterator it = mKeys.begin(); it != mKeys.end(); ++it) { if (it->isNull()) { continue; } const QString fpr = QLatin1String(it->primaryFingerprint()); labelTexts.push_back(fpr.right(8)); toolTipText += fpr.right(8) + QLatin1String(": "); if (const char *uid = it->userID(0).id()) { if (it->protocol() == GpgME::OpenPGP) { toolTipText += QString::fromUtf8(uid); } else { toolTipText += Kleo::DN(uid).prettyDN(); } } else { toolTipText += xi18n("unknown"); } toolTipText += QLatin1Char('\n'); } mLabel->setText(labelTexts.join(QLatin1String(", "))); mLabel->setToolTip(toolTipText); } #ifndef __KLEO_UI_SHOW_KEY_LIST_ERROR_H__ #define __KLEO_UI_SHOW_KEY_LIST_ERROR_H__ static void showKeyListError(QWidget *parent, const GpgME::Error &err) { Q_ASSERT(err); const QString msg = i18n( "

An error occurred while fetching " "the keys from the backend:

" "

%1

", QString::fromLocal8Bit(err.asString())); KMessageBox::error(parent, msg, i18n("Key Listing Failed")); } #endif // __KLEO_UI_SHOW_KEY_LIST_ERROR_H__ void Kleo::KeyRequester::startKeyListJob(const QStringList &fingerprints) { if (!mSMIMEBackend && !mOpenPGPBackend) { return; } mTmpKeys.clear(); mJobs = 0; unsigned int count = 0; for (QStringList::const_iterator it = fingerprints.begin(); it != fingerprints.end(); ++it) { if (!(*it).trimmed().isEmpty()) { ++count; } } if (!count) { // don't fall into the trap that an empty pattern means // "return all keys" :) setKey(GpgME::Key::null); return; } if (mOpenPGPBackend) { KeyListJob *job = mOpenPGPBackend->keyListJob(false); // local, no sigs if (!job) { KMessageBox::error(this, i18n("The OpenPGP backend does not support listing keys. " "Check your installation."), i18n("Key Listing Failed")); } else { connect(job, &KeyListJob::result, this, &SigningKeyRequester::slotKeyListResult); connect(job, &KeyListJob::nextKey, this, &SigningKeyRequester::slotNextKey); const GpgME::Error err = job->start(fingerprints, mKeyUsage & Kleo::KeySelectionDialog::SecretKeys && !(mKeyUsage & Kleo::KeySelectionDialog::PublicKeys)); if (err) { showKeyListError(this, err); } else { ++mJobs; } } } if (mSMIMEBackend) { KeyListJob *job = mSMIMEBackend->keyListJob(false); // local, no sigs if (!job) { KMessageBox::error(this, i18n("The S/MIME backend does not support listing keys. " "Check your installation."), i18n("Key Listing Failed")); } else { connect(job, &KeyListJob::result, this, &SigningKeyRequester::slotKeyListResult); connect(job, &KeyListJob::nextKey, this, &SigningKeyRequester::slotNextKey); const GpgME::Error err = job->start(fingerprints, mKeyUsage & Kleo::KeySelectionDialog::SecretKeys && !(mKeyUsage & Kleo::KeySelectionDialog::PublicKeys)); if (err) { showKeyListError(this, err); } else { ++mJobs; } } } if (mJobs > 0) { mEraseButton->setEnabled(false); mDialogButton->setEnabled(false); } } void Kleo::KeyRequester::slotNextKey(const GpgME::Key &key) { if (!key.isNull()) { mTmpKeys.push_back(key); } } void Kleo::KeyRequester::slotKeyListResult(const GpgME::KeyListResult &res) { if (res.error()) { showKeyListError(this, res.error()); } if (--mJobs <= 0) { mEraseButton->setEnabled(true); mDialogButton->setEnabled(true); setKeys(mTmpKeys); mTmpKeys.clear(); } } void Kleo::KeyRequester::slotDialogButtonClicked() { KeySelectionDialog *dlg = mKeys.empty() ? new KeySelectionDialog(mDialogCaption, mDialogMessage, mInitialQuery, mKeyUsage, mMulti, false, this) : new KeySelectionDialog(mDialogCaption, mDialogCaption, mKeys, mKeyUsage, mMulti, false, this); if (dlg->exec() == QDialog::Accepted) { if (mMulti) { setKeys(dlg->selectedKeys()); } else { setKey(dlg->selectedKey()); } Q_EMIT changed(); } delete dlg; } void Kleo::KeyRequester::slotEraseButtonClicked() { if (!mKeys.empty()) { Q_EMIT changed(); } mKeys.clear(); updateKeys(); } void Kleo::KeyRequester::setDialogCaption(const QString &caption) { mDialogCaption = caption; } void Kleo::KeyRequester::setDialogMessage(const QString &msg) { mDialogMessage = msg; } bool Kleo::KeyRequester::isMultipleKeysEnabled() const { return mMulti; } void Kleo::KeyRequester::setMultipleKeysEnabled(bool multi) { if (multi == mMulti) { return; } if (!multi && !mKeys.empty()) { mKeys.erase(mKeys.begin() + 1, mKeys.end()); } mMulti = multi; updateKeys(); } unsigned int Kleo::KeyRequester::allowedKeys() const { return mKeyUsage; } void Kleo::KeyRequester::setAllowedKeys(unsigned int keyUsage) { mKeyUsage = keyUsage; mOpenPGPBackend = nullptr; mSMIMEBackend = nullptr; if (mKeyUsage & KeySelectionDialog::OpenPGPKeys) { mOpenPGPBackend = openpgp(); } if (mKeyUsage & KeySelectionDialog::SMIMEKeys) { mSMIMEBackend = smime(); } if (mOpenPGPBackend && !mSMIMEBackend) { mDialogCaption = i18n("OpenPGP Key Selection"); mDialogMessage = i18n("Please select an OpenPGP key to use."); } else if (!mOpenPGPBackend && mSMIMEBackend) { mDialogCaption = i18n("S/MIME Key Selection"); mDialogMessage = i18n("Please select an S/MIME key to use."); } else { mDialogCaption = i18n("Key Selection"); mDialogMessage = i18n("Please select an (OpenPGP or S/MIME) key to use."); } } QPushButton *Kleo::KeyRequester::dialogButton() { return mDialogButton; } QPushButton *Kleo::KeyRequester::eraseButton() { return mEraseButton; } static inline unsigned int foo(bool openpgp, bool smime, bool trusted, bool valid) { unsigned int result = 0; if (openpgp) { result |= Kleo::KeySelectionDialog::OpenPGPKeys; } if (smime) { result |= Kleo::KeySelectionDialog::SMIMEKeys; } if (trusted) { result |= Kleo::KeySelectionDialog::TrustedKeys; } if (valid) { result |= Kleo::KeySelectionDialog::ValidKeys; } return result; } static inline unsigned int encryptionKeyUsage(bool openpgp, bool smime, bool trusted, bool valid) { return foo(openpgp, smime, trusted, valid) | Kleo::KeySelectionDialog::EncryptionKeys | Kleo::KeySelectionDialog::PublicKeys; } static inline unsigned int signingKeyUsage(bool openpgp, bool smime, bool trusted, bool valid) { return foo(openpgp, smime, trusted, valid) | Kleo::KeySelectionDialog::SigningKeys | Kleo::KeySelectionDialog::SecretKeys; } Kleo::EncryptionKeyRequester::EncryptionKeyRequester(bool multi, unsigned int proto, QWidget *parent, bool onlyTrusted, bool onlyValid) : KeyRequester(encryptionKeyUsage(proto & OpenPGP, proto & SMIME, onlyTrusted, onlyValid), multi, parent) , d(nullptr) { } Kleo::EncryptionKeyRequester::EncryptionKeyRequester(QWidget *parent) : KeyRequester(0, false, parent) , d(nullptr) { } Kleo::EncryptionKeyRequester::~EncryptionKeyRequester() { } void Kleo::EncryptionKeyRequester::setAllowedKeys(unsigned int proto, bool onlyTrusted, bool onlyValid) { KeyRequester::setAllowedKeys(encryptionKeyUsage(proto & OpenPGP, proto & SMIME, onlyTrusted, onlyValid)); } Kleo::SigningKeyRequester::SigningKeyRequester(bool multi, unsigned int proto, QWidget *parent, bool onlyTrusted, bool onlyValid) : KeyRequester(signingKeyUsage(proto & OpenPGP, proto & SMIME, onlyTrusted, onlyValid), multi, parent) , d(nullptr) { } Kleo::SigningKeyRequester::SigningKeyRequester(QWidget *parent) : KeyRequester(0, false, parent) , d(nullptr) { } Kleo::SigningKeyRequester::~SigningKeyRequester() { } void Kleo::SigningKeyRequester::setAllowedKeys(unsigned int proto, bool onlyTrusted, bool onlyValid) { KeyRequester::setAllowedKeys(signingKeyUsage(proto & OpenPGP, proto & SMIME, onlyTrusted, onlyValid)); } void Kleo::KeyRequester::virtual_hook(int, void *) { } void Kleo::EncryptionKeyRequester::virtual_hook(int id, void *data) { KeyRequester::virtual_hook(id, data); } void Kleo::SigningKeyRequester::virtual_hook(int id, void *data) { KeyRequester::virtual_hook(id, data); } diff --git a/src/ui/keyselectioncombo.cpp b/src/ui/keyselectioncombo.cpp index 2525e4858..fced86e91 100644 --- a/src/ui/keyselectioncombo.cpp +++ b/src/ui/keyselectioncombo.cpp @@ -1,652 +1,654 @@ /* This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "keyselectioncombo.h" #include "kleo/defaultkeyfilter.h" #include "kleo/dn.h" #include "models/keycache.h" #include "models/keylist.h" #include "models/keylistmodel.h" #include "models/keylistsortfilterproxymodel.h" #include "progressbar.h" #include "utils/formatting.h" #include #include #include #include #include #include using namespace Kleo; Q_DECLARE_METATYPE(GpgME::Key) namespace { class SortFilterProxyModel : public KeyListSortFilterProxyModel { Q_OBJECT public: using KeyListSortFilterProxyModel::KeyListSortFilterProxyModel; void setAlwaysAcceptedKey(const QString &fingerprint) { if (fingerprint == mFingerprint) { return; } mFingerprint = fingerprint; invalidate(); } protected: bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override { if (!mFingerprint.isEmpty()) { const QModelIndex index = sourceModel()->index(source_row, 0, source_parent); const auto fingerprint = sourceModel()->data(index, KeyList::FingerprintRole).toString(); if (fingerprint == mFingerprint) { return true; } } return KeyListSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); } private: QString mFingerprint; }; class ProxyModel : public QSortFilterProxyModel { Q_OBJECT private: struct CustomItem { QIcon icon; QString text; QVariant data; QString toolTip; }; public: ProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) { } ~ProxyModel() override { qDeleteAll(mFrontItems); qDeleteAll(mBackItems); } bool lessThan(const QModelIndex &left, const QModelIndex &right) const override { const auto leftKey = sourceModel()->data(left, KeyList::KeyRole).value(); const auto rightKey = sourceModel()->data(right, KeyList::KeyRole).value(); if (leftKey.isNull()) { return false; } if (rightKey.isNull()) { return true; } // As we display UID(0) this is ok. We probably need a get Best UID at some point. const auto lUid = leftKey.userID(0); const auto rUid = rightKey.userID(0); if (lUid.isNull()) { return false; } if (rUid.isNull()) { return true; } int cmp = strcmp(lUid.id(), rUid.id()); if (cmp) { return cmp < 0; } if (lUid.validity() == rUid.validity()) { /* Both are the same check which one is newer. */ time_t oldTime = 0; for (const GpgME::Subkey &s : leftKey.subkeys()) { if (s.isRevoked() || s.isInvalid() || s.isDisabled()) { continue; } if (s.creationTime() > oldTime) { oldTime = s.creationTime(); } } time_t newTime = 0; for (const GpgME::Subkey &s : rightKey.subkeys()) { if (s.isRevoked() || s.isInvalid() || s.isDisabled()) { continue; } if (s.creationTime() > newTime) { newTime = s.creationTime(); } } return newTime < oldTime; } return lUid.validity() > rUid.validity(); } bool isCustomItem(const int row) const { return row < mFrontItems.count() || row >= mFrontItems.count() + QSortFilterProxyModel::rowCount(); } void prependItem(const QIcon &icon, const QString &text, const QVariant &data, const QString &toolTip) { beginInsertRows(QModelIndex(), 0, 0); mFrontItems.push_front(new CustomItem{icon, text, data, toolTip}); endInsertRows(); } void appendItem(const QIcon &icon, const QString &text, const QVariant &data, const QString &toolTip) { beginInsertRows(QModelIndex(), rowCount(), rowCount()); mBackItems.push_back(new CustomItem{icon, text, data, toolTip}); endInsertRows(); } void removeCustomItem(const QVariant &data) { for (int i = 0; i < mFrontItems.count(); ++i) { if (mFrontItems[i]->data == data) { beginRemoveRows(QModelIndex(), i, i); delete mFrontItems.takeAt(i); endRemoveRows(); return; } } for (int i = 0; i < mBackItems.count(); ++i) { if (mBackItems[i]->data == data) { const int index = mFrontItems.count() + QSortFilterProxyModel::rowCount() + i; beginRemoveRows(QModelIndex(), index, index); delete mBackItems.takeAt(i); endRemoveRows(); return; } } } int rowCount(const QModelIndex &parent = QModelIndex()) const override { return mFrontItems.count() + QSortFilterProxyModel::rowCount(parent) + mBackItems.count(); } int columnCount(const QModelIndex &parent = QModelIndex()) const override { Q_UNUSED(parent) // pretend that there is only one column to workaround a bug in // QAccessibleTable which provides the accessibility interface for the // pop-up of the combo box return 1; } QModelIndex mapToSource(const QModelIndex &index) const override { if (!isCustomItem(index.row())) { const int row = index.row() - mFrontItems.count(); return sourceModel()->index(row, index.column()); } else { return {}; } } QModelIndex mapFromSource(const QModelIndex &source_index) const override { const QModelIndex idx = QSortFilterProxyModel::mapFromSource(source_index); return createIndex(mFrontItems.count() + idx.row(), idx.column(), idx.internalPointer()); } QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override { if (row < 0 || row >= rowCount()) { return {}; } if (row < mFrontItems.count()) { return createIndex(row, column, mFrontItems[row]); } else if (row >= mFrontItems.count() + QSortFilterProxyModel::rowCount()) { return createIndex(row, column, mBackItems[row - mFrontItems.count() - QSortFilterProxyModel::rowCount()]); } else { const QModelIndex mi = QSortFilterProxyModel::index(row - mFrontItems.count(), column, parent); return createIndex(row, column, mi.internalPointer()); } } Qt::ItemFlags flags(const QModelIndex &index) const override { Q_UNUSED(index) return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemNeverHasChildren; } QModelIndex parent(const QModelIndex &) const override { // Flat list return {}; } QVariant data(const QModelIndex &index, int role) const override { if (!index.isValid()) { return QVariant(); } if (isCustomItem(index.row())) { Q_ASSERT(!mFrontItems.isEmpty() || !mBackItems.isEmpty()); auto ci = static_cast(index.internalPointer()); switch (role) { case Qt::DisplayRole: return ci->text; case Qt::DecorationRole: return ci->icon; case Qt::UserRole: return ci->data; case Qt::ToolTipRole: return ci->toolTip; default: return QVariant(); } } const auto key = QSortFilterProxyModel::data(index, KeyList::KeyRole).value(); Q_ASSERT(!key.isNull()); if (key.isNull()) { return QVariant(); } switch (role) { case Qt::DisplayRole: { const auto userID = key.userID(0); QString name; QString email; if (key.protocol() == GpgME::OpenPGP) { name = QString::fromUtf8(userID.name()); email = QString::fromUtf8(userID.email()); } else { const Kleo::DN dn(userID.id()); name = dn[QStringLiteral("CN")]; email = dn[QStringLiteral("EMAIL")]; } const auto nameAndEmail = email.isEmpty() ? name : name.isEmpty() ? email : i18nc("Name ", "%1 <%2>", name, email); if (Kleo::KeyCache::instance()->pgpOnly()) { return i18nc("Name (validity, created: date)", "%1 (%2, created: %3)", nameAndEmail, Kleo::Formatting::complianceStringShort(key), Kleo::Formatting::creationDateString(key)); } else { return i18nc("Name (validity, type, created: date)", "%1 (%2, %3, created: %4)", nameAndEmail, Kleo::Formatting::complianceStringShort(key), Formatting::displayName(key.protocol()), Kleo::Formatting::creationDateString(key)); } } case Qt::ToolTipRole: { using namespace Kleo::Formatting; return Kleo::Formatting::toolTip(key, Validity | Issuer | Subject | Fingerprint | ExpiryDates | UserIDs); } case Qt::DecorationRole: return Kleo::Formatting::iconForUid(key.userID(0)); default: return QSortFilterProxyModel::data(index, role); } } private: QVector mFrontItems; QVector mBackItems; }; } // anonymous namespace namespace Kleo { class KeySelectionComboPrivate { public: KeySelectionComboPrivate(KeySelectionCombo *parent) : wasEnabled(true) , q(parent) { } /* Selects the first key with a UID addrSpec that matches * the mPerfectMatchMbox variable. * * The idea here is that if there are keys like: * * tom-store@abc.com * susi-store@abc.com * store@abc.com * * And the user wants to send a mail to "store@abc.com" * the filter should still show tom and susi (because they * both are part of store) but the key for "store" should * be preselected. * * Returns true if one was selected. False otherwise. */ bool selectPerfectIdMatch() const { if (mPerfectMatchMbox.isEmpty()) { return false; } for (int i = 0; i < proxyModel->rowCount(); ++i) { const auto idx = proxyModel->index(i, 0, QModelIndex()); const auto key = proxyModel->data(idx, KeyList::KeyRole).value(); if (key.isNull()) { // WTF? continue; } for (const auto &uid : key.userIDs()) { if (QString::fromStdString(uid.addrSpec()) == mPerfectMatchMbox) { q->setCurrentIndex(i); return true; } } } return false; } /* Updates the current key with the default key if the key matches * the current key filter. */ void updateWithDefaultKey() { GpgME::Protocol filterProto = GpgME::UnknownProtocol; const auto filter = dynamic_cast(sortFilterProxy->keyFilter().get()); if (filter && filter->isOpenPGP() == DefaultKeyFilter::Set) { filterProto = GpgME::OpenPGP; } else if (filter && filter->isOpenPGP() == DefaultKeyFilter::NotSet) { filterProto = GpgME::CMS; } QString defaultKey = defaultKeys.value(filterProto); if (defaultKey.isEmpty()) { // Fallback to unknown protocol defaultKey = defaultKeys.value(GpgME::UnknownProtocol); } // make sure that the default key is not filtered out unless it has the wrong protocol if (filterProto == GpgME::UnknownProtocol) { sortFilterProxy->setAlwaysAcceptedKey(defaultKey); } else { const auto key = KeyCache::instance()->findByFingerprint(defaultKey.toLatin1().constData()); if (!key.isNull() && key.protocol() == filterProto) { sortFilterProxy->setAlwaysAcceptedKey(defaultKey); } else { sortFilterProxy->setAlwaysAcceptedKey({}); } } q->setCurrentKey(defaultKey); } void storeCurrentSelectionBeforeModelChange() { keyBeforeModelChange = q->currentKey(); customItemBeforeModelChange = q->currentData(); } void restoreCurrentSelectionAfterModelChange() { if (!keyBeforeModelChange.isNull()) { q->setCurrentKey(keyBeforeModelChange); } else if (customItemBeforeModelChange.isValid()) { const auto index = q->findData(customItemBeforeModelChange); if (index != -1) { q->setCurrentIndex(index); } else { updateWithDefaultKey(); } } } Kleo::AbstractKeyListModel *model = nullptr; SortFilterProxyModel *sortFilterProxy = nullptr; ProxyModel *proxyModel = nullptr; std::shared_ptr cache; QMap defaultKeys; bool wasEnabled = false; bool useWasEnabled = false; bool secretOnly = false; bool initialKeyListingDone = false; QString mPerfectMatchMbox; GpgME::Key keyBeforeModelChange; QVariant customItemBeforeModelChange; private: KeySelectionCombo *const q; }; } using namespace Kleo; KeySelectionCombo::KeySelectionCombo(QWidget *parent) : KeySelectionCombo(true, parent) { } KeySelectionCombo::KeySelectionCombo(bool secretOnly, QWidget *parent) : QComboBox(parent) , d(new KeySelectionComboPrivate(this)) { d->model = Kleo::AbstractKeyListModel::createFlatKeyListModel(this); d->secretOnly = secretOnly; d->sortFilterProxy = new SortFilterProxyModel(this); d->sortFilterProxy->setSourceModel(d->model); d->proxyModel = new ProxyModel(this); d->proxyModel->setSourceModel(d->sortFilterProxy); setModel(d->proxyModel); connect(this, qOverload(&QComboBox::currentIndexChanged), this, [this](int row) { if (row >= 0 && row < d->proxyModel->rowCount()) { if (d->proxyModel->isCustomItem(row)) { Q_EMIT customItemSelected(currentData(Qt::UserRole)); } else { Q_EMIT currentKeyChanged(currentKey()); } } }); d->cache = Kleo::KeyCache::mutableInstance(); connect(model(), &QAbstractItemModel::rowsAboutToBeInserted, this, [this]() { d->storeCurrentSelectionBeforeModelChange(); }); connect(model(), &QAbstractItemModel::rowsInserted, this, [this]() { d->restoreCurrentSelectionAfterModelChange(); }); connect(model(), &QAbstractItemModel::rowsAboutToBeRemoved, this, [this]() { d->storeCurrentSelectionBeforeModelChange(); }); connect(model(), &QAbstractItemModel::rowsRemoved, this, [this]() { d->restoreCurrentSelectionAfterModelChange(); }); connect(model(), &QAbstractItemModel::modelAboutToBeReset, this, [this]() { d->storeCurrentSelectionBeforeModelChange(); }); connect(model(), &QAbstractItemModel::modelReset, this, [this]() { d->restoreCurrentSelectionAfterModelChange(); }); QTimer::singleShot(0, this, &KeySelectionCombo::init); } KeySelectionCombo::~KeySelectionCombo() = default; void KeySelectionCombo::init() { connect(d->cache.get(), &Kleo::KeyCache::keyListingDone, this, [this]() { // Set useKeyCache ensures that the cache is populated // so this can be a blocking call if the cache is not initialized if (!d->initialKeyListingDone) { d->model->useKeyCache(true, d->secretOnly ? KeyList::SecretKeysOnly : KeyList::AllKeys); d->proxyModel->removeCustomItem(QStringLiteral("-libkleo-loading-keys")); } // We use the useWasEnabled state variable to decide if we should // change the enable / disable state based on the keylist done signal. // If we triggered the refresh useWasEnabled is true and we want to // enable / disable again after our refresh, as the refresh disabled it. // // But if a keyListingDone signal comes from just a generic refresh // triggered by someone else we don't want to change the enable / disable // state. if (d->useWasEnabled) { setEnabled(d->wasEnabled); d->useWasEnabled = false; } Q_EMIT keyListingFinished(); }); connect(this, &KeySelectionCombo::keyListingFinished, this, [this]() { if (!d->initialKeyListingDone) { d->updateWithDefaultKey(); d->initialKeyListingDone = true; } }); if (!d->cache->initialized()) { refreshKeys(); } else { d->model->useKeyCache(true, d->secretOnly ? KeyList::SecretKeysOnly : KeyList::AllKeys); Q_EMIT keyListingFinished(); } connect(this, qOverload(&QComboBox::currentIndexChanged), this, [this]() { setToolTip(currentData(Qt::ToolTipRole).toString()); }); } void KeySelectionCombo::setKeyFilter(const std::shared_ptr &kf) { d->sortFilterProxy->setKeyFilter(kf); d->proxyModel->sort(0); d->updateWithDefaultKey(); } std::shared_ptr KeySelectionCombo::keyFilter() const { return d->sortFilterProxy->keyFilter(); } void KeySelectionCombo::setIdFilter(const QString &id) { d->sortFilterProxy->setFilterRegularExpression(id); d->mPerfectMatchMbox = id; d->updateWithDefaultKey(); } QString KeySelectionCombo::idFilter() const { return d->sortFilterProxy->filterRegularExpression().pattern(); } GpgME::Key Kleo::KeySelectionCombo::currentKey() const { return currentData(KeyList::KeyRole).value(); } void Kleo::KeySelectionCombo::setCurrentKey(const GpgME::Key &key) { const int idx = findData(QString::fromLatin1(key.primaryFingerprint()), KeyList::FingerprintRole, Qt::MatchExactly); if (idx > -1) { setCurrentIndex(idx); } else if (!d->selectPerfectIdMatch()) { d->updateWithDefaultKey(); } setToolTip(currentData(Qt::ToolTipRole).toString()); } void Kleo::KeySelectionCombo::setCurrentKey(const QString &fingerprint) { const auto cur = currentKey(); if (!cur.isNull() && !fingerprint.isEmpty() && fingerprint == QLatin1String(cur.primaryFingerprint())) { // already set; still emit a changed signal because the current key may // have become the item at the current index by changes in the underlying model Q_EMIT currentKeyChanged(cur); return; } const int idx = findData(fingerprint, KeyList::FingerprintRole, Qt::MatchExactly); if (idx > -1) { setCurrentIndex(idx); } else if (!d->selectPerfectIdMatch()) { setCurrentIndex(0); } setToolTip(currentData(Qt::ToolTipRole).toString()); } void KeySelectionCombo::refreshKeys() { d->wasEnabled = isEnabled(); d->useWasEnabled = true; setEnabled(false); const bool wasBlocked = blockSignals(true); prependCustomItem(QIcon(), i18n("Loading keys ..."), QStringLiteral("-libkleo-loading-keys")); setCurrentIndex(0); blockSignals(wasBlocked); d->cache->startKeyListing(); } void KeySelectionCombo::appendCustomItem(const QIcon &icon, const QString &text, const QVariant &data, const QString &toolTip) { d->proxyModel->appendItem(icon, text, data, toolTip); } void KeySelectionCombo::appendCustomItem(const QIcon &icon, const QString &text, const QVariant &data) { appendCustomItem(icon, text, data, QString()); } void KeySelectionCombo::prependCustomItem(const QIcon &icon, const QString &text, const QVariant &data, const QString &toolTip) { d->proxyModel->prependItem(icon, text, data, toolTip); } void KeySelectionCombo::prependCustomItem(const QIcon &icon, const QString &text, const QVariant &data) { prependCustomItem(icon, text, data, QString()); } void KeySelectionCombo::removeCustomItem(const QVariant &data) { d->proxyModel->removeCustomItem(data); } void Kleo::KeySelectionCombo::setDefaultKey(const QString &fingerprint, GpgME::Protocol proto) { d->defaultKeys.insert(proto, fingerprint); d->updateWithDefaultKey(); } void Kleo::KeySelectionCombo::setDefaultKey(const QString &fingerprint) { setDefaultKey(fingerprint, GpgME::UnknownProtocol); } QString Kleo::KeySelectionCombo::defaultKey(GpgME::Protocol proto) const { return d->defaultKeys.value(proto); } QString Kleo::KeySelectionCombo::defaultKey() const { return defaultKey(GpgME::UnknownProtocol); } #include "keyselectioncombo.moc" diff --git a/src/ui/keyselectiondialog.cpp b/src/ui/keyselectiondialog.cpp index a0fd62134..88dbf599d 100644 --- a/src/ui/keyselectiondialog.cpp +++ b/src/ui/keyselectiondialog.cpp @@ -1,999 +1,1001 @@ /* -*- c++ -*- keyselectiondialog.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB Based on kpgpui.cpp SPDX-FileCopyrightText: 2001, 2002 the KPGP authors See file libkdenetwork/AUTHORS.kpgp for details SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "keyselectiondialog.h" #include "keylistview.h" #include "progressdialog.h" #include "libkleo/dn.h" #include // gpgme++ #include #include #include // KDE #include #include #include #include #include // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static bool checkKeyUsage(const GpgME::Key &key, unsigned int keyUsage, QString *statusString = nullptr) { auto setStatusString = [statusString](const QString &status) { if (statusString) { *statusString = status; } }; if (keyUsage & Kleo::KeySelectionDialog::ValidKeys) { if (key.isInvalid()) { if (key.keyListMode() & GpgME::Validate) { qCDebug(KLEO_UI_LOG) << "key is invalid"; setStatusString(i18n("The key is not valid.")); return false; } else { qCDebug(KLEO_UI_LOG) << "key is invalid - ignoring"; } } if (key.isExpired()) { qCDebug(KLEO_UI_LOG) << "key is expired"; setStatusString(i18n("The key is expired.")); return false; } else if (key.isRevoked()) { qCDebug(KLEO_UI_LOG) << "key is revoked"; setStatusString(i18n("The key is revoked.")); return false; } else if (key.isDisabled()) { qCDebug(KLEO_UI_LOG) << "key is disabled"; setStatusString(i18n("The key is disabled.")); return false; } } if (keyUsage & Kleo::KeySelectionDialog::EncryptionKeys && !key.canEncrypt()) { qCDebug(KLEO_UI_LOG) << "key can't encrypt"; setStatusString(i18n("The key is not designated for encryption.")); return false; } if (keyUsage & Kleo::KeySelectionDialog::SigningKeys && !key.canSign()) { qCDebug(KLEO_UI_LOG) << "key can't sign"; setStatusString(i18n("The key is not designated for signing.")); return false; } if (keyUsage & Kleo::KeySelectionDialog::CertificationKeys && !key.canCertify()) { qCDebug(KLEO_UI_LOG) << "key can't certify"; setStatusString(i18n("The key is not designated for certifying.")); return false; } if (keyUsage & Kleo::KeySelectionDialog::AuthenticationKeys && !key.canAuthenticate()) { qCDebug(KLEO_UI_LOG) << "key can't authenticate"; setStatusString(i18n("The key is not designated for authentication.")); return false; } if (keyUsage & Kleo::KeySelectionDialog::SecretKeys && !(keyUsage & Kleo::KeySelectionDialog::PublicKeys) && !key.hasSecret()) { qCDebug(KLEO_UI_LOG) << "key isn't secret"; setStatusString(i18n("The key is not secret.")); return false; } if (keyUsage & Kleo::KeySelectionDialog::TrustedKeys && key.protocol() == GpgME::OpenPGP && // only check this for secret keys for now. // Seems validity isn't checked for secret keylistings... !key.hasSecret()) { std::vector uids = key.userIDs(); for (std::vector::const_iterator it = uids.begin(); it != uids.end(); ++it) { if (!it->isRevoked() && it->validity() >= GpgME::UserID::Marginal) { return true; } } qCDebug(KLEO_UI_LOG) << "key has no UIDs with validity >= Marginal"; setStatusString(i18n("The key is not trusted enough.")); return false; } // X.509 keys are always trusted, else they won't be the keybox. // PENDING(marc) check that this ^ is correct setStatusString(i18n("The key can be used.")); return true; } static bool checkKeyUsage(const std::vector &keys, unsigned int keyUsage) { for (auto it = keys.begin(); it != keys.end(); ++it) { if (!checkKeyUsage(*it, keyUsage)) { return false; } } return true; } static inline QString time_t2string(time_t t) { return QDateTime::fromSecsSinceEpoch(t).toString(); } namespace { class ColumnStrategy : public Kleo::KeyListView::ColumnStrategy { public: ColumnStrategy(unsigned int keyUsage); QString title(int col) const override; int width(int col, const QFontMetrics &fm) const override; QString text(const GpgME::Key &key, int col) const override; QString toolTip(const GpgME::Key &key, int col) const override; QIcon icon(const GpgME::Key &key, int col) const override; private: const QIcon mKeyGoodPix, mKeyBadPix, mKeyUnknownPix, mKeyValidPix; const unsigned int mKeyUsage; }; static QString iconPath(const QString &name) { return QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("libkleopatra/pics/") + name + QStringLiteral(".png")); } ColumnStrategy::ColumnStrategy(unsigned int keyUsage) : Kleo::KeyListView::ColumnStrategy() , mKeyGoodPix(iconPath(QStringLiteral("key_ok"))) , mKeyBadPix(iconPath(QStringLiteral("key_bad"))) , mKeyUnknownPix(iconPath(QStringLiteral("key_unknown"))) , mKeyValidPix(iconPath(QStringLiteral("key"))) , mKeyUsage(keyUsage) { if (keyUsage == 0) { qCWarning(KLEO_UI_LOG) << "KeySelectionDialog: keyUsage == 0. You want to use AllKeys instead."; } } QString ColumnStrategy::title(int col) const { switch (col) { case 0: return i18n("Key ID"); case 1: return i18n("User ID"); default: return QString(); } } int ColumnStrategy::width(int col, const QFontMetrics &fm) const { if (col == 0) { static const char hexchars[] = "0123456789ABCDEF"; int maxWidth = 0; for (unsigned int i = 0; i < 16; ++i) { maxWidth = qMax(fm.boundingRect(QLatin1Char(hexchars[i])).width(), maxWidth); } return 8 * maxWidth + 2 * 16 /* KIconLoader::SizeSmall */; } return Kleo::KeyListView::ColumnStrategy::width(col, fm); } QString ColumnStrategy::text(const GpgME::Key &key, int col) const { switch (col) { case 0: { if (key.shortKeyID()) { return QString::fromUtf8(key.shortKeyID()); } else { return xi18n("unknown"); } } case 1: { const char *uid = key.userID(0).id(); if (key.protocol() == GpgME::OpenPGP) { return uid && *uid ? QString::fromUtf8(uid) : QString(); } else { // CMS return Kleo::DN(uid).prettyDN(); } } default: return QString(); } } QString ColumnStrategy::toolTip(const GpgME::Key &key, int) const { const char *uid = key.userID(0).id(); const char *fpr = key.primaryFingerprint(); const char *issuer = key.issuerName(); const GpgME::Subkey subkey = key.subkey(0); const QString expiry = subkey.neverExpires() ? i18n("never") : time_t2string(subkey.expirationTime()); const QString creation = time_t2string(subkey.creationTime()); QString keyStatusString; if (!checkKeyUsage(key, mKeyUsage, &keyStatusString)) { // Show the status in bold if there is a problem keyStatusString = QLatin1String("") % keyStatusString % QLatin1String(""); } QString html = QStringLiteral("

"); if (key.protocol() == GpgME::OpenPGP) { html += i18n("OpenPGP key for %1", uid ? QString::fromUtf8(uid) : i18n("unknown")); } else { html += i18n("S/MIME key for %1", uid ? Kleo::DN(uid).prettyDN() : i18n("unknown")); } html += QStringLiteral("

"); const auto addRow = [&html](const QString &name, const QString &value) { html += QStringLiteral("").arg(name, value); }; addRow(i18nc("Key creation date", "Created"), creation); addRow(i18nc("Key Expiration date", "Expiry"), expiry); addRow(i18nc("Key fingerprint", "Fingerprint"), fpr ? QString::fromLatin1(fpr) : i18n("unknown")); if (key.protocol() != GpgME::OpenPGP) { addRow(i18nc("Key issuer", "Issuer"), issuer ? Kleo::DN(issuer).prettyDN() : i18n("unknown")); } addRow(i18nc("Key status", "Status"), keyStatusString); html += QStringLiteral("
%1: %2
"); return html; } QIcon ColumnStrategy::icon(const GpgME::Key &key, int col) const { if (col != 0) { return QIcon(); } // this key did not undergo a validating keylisting yet: if (!(key.keyListMode() & GpgME::Validate)) { return mKeyUnknownPix; } if (!checkKeyUsage(key, mKeyUsage)) { return mKeyBadPix; } if (key.protocol() == GpgME::CMS) { return mKeyGoodPix; } switch (key.userID(0).validity()) { default: case GpgME::UserID::Unknown: case GpgME::UserID::Undefined: return mKeyUnknownPix; case GpgME::UserID::Never: return mKeyValidPix; case GpgME::UserID::Marginal: case GpgME::UserID::Full: case GpgME::UserID::Ultimate: return mKeyGoodPix; } } } static const int sCheckSelectionDelay = 250; Kleo::KeySelectionDialog::KeySelectionDialog(QWidget *parent, Options options) : QDialog(parent) , mOpenPGPBackend(QGpgME::openpgp()) , mSMIMEBackend(QGpgME::smime()) , mKeyUsage(AllKeys) { qCDebug(KLEO_UI_LOG) << "mTruncated:" << mTruncated << "mSavedOffsetY:" << mSavedOffsetY; setUpUI(options, QString()); } Kleo::KeySelectionDialog::KeySelectionDialog(const QString &title, const QString &text, const std::vector &selectedKeys, unsigned int keyUsage, bool extendedSelection, bool rememberChoice, QWidget *parent, bool modal) : QDialog(parent) , mSelectedKeys(selectedKeys) , mKeyUsage(keyUsage) { setWindowTitle(title); setModal(modal); init(rememberChoice, extendedSelection, text, QString()); } Kleo::KeySelectionDialog::KeySelectionDialog(const QString &title, const QString &text, const QString &initialQuery, const std::vector &selectedKeys, unsigned int keyUsage, bool extendedSelection, bool rememberChoice, QWidget *parent, bool modal) : QDialog(parent) , mSelectedKeys(selectedKeys) , mKeyUsage(keyUsage) , mSearchText(initialQuery) , mInitialQuery(initialQuery) { setWindowTitle(title); setModal(modal); init(rememberChoice, extendedSelection, text, initialQuery); } Kleo::KeySelectionDialog::KeySelectionDialog(const QString &title, const QString &text, const QString &initialQuery, unsigned int keyUsage, bool extendedSelection, bool rememberChoice, QWidget *parent, bool modal) : QDialog(parent) , mKeyUsage(keyUsage) , mSearchText(initialQuery) , mInitialQuery(initialQuery) { setWindowTitle(title); setModal(modal); init(rememberChoice, extendedSelection, text, initialQuery); } void Kleo::KeySelectionDialog::setUpUI(Options options, const QString &initialQuery) { auto mainLayout = new QVBoxLayout(this); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); mOkButton = buttonBox->button(QDialogButtonBox::Ok); mOkButton->setDefault(true); mOkButton->setShortcut(Qt::CTRL | Qt::Key_Return); mCheckSelectionTimer = new QTimer(this); mStartSearchTimer = new QTimer(this); QFrame *page = new QFrame(this); mainLayout->addWidget(page); mainLayout->addWidget(buttonBox); mTopLayout = new QVBoxLayout(page); mTopLayout->setContentsMargins(0, 0, 0, 0); mTextLabel = new QLabel(page); mTextLabel->setWordWrap(true); // Setting the size policy is necessary as a workaround for https://issues.kolab.org/issue4429 // and http://bugreports.qt.nokia.com/browse/QTBUG-8740 mTextLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); connect(mTextLabel, &QLabel::linkActivated, this, &KeySelectionDialog::slotStartCertificateManager); mTopLayout->addWidget(mTextLabel); mTextLabel->hide(); QPushButton *const searchExternalPB = new QPushButton(i18n("Search for &External Certificates"), page); mTopLayout->addWidget(searchExternalPB, 0, Qt::AlignLeft); connect(searchExternalPB, &QAbstractButton::clicked, this, &KeySelectionDialog::slotStartSearchForExternalCertificates); if (initialQuery.isEmpty()) { searchExternalPB->hide(); } auto hlay = new QHBoxLayout(); mTopLayout->addLayout(hlay); auto le = new QLineEdit(page); le->setClearButtonEnabled(true); le->setText(initialQuery); QLabel *lbSearchFor = new QLabel(i18n("&Search for:"), page); lbSearchFor->setBuddy(le); hlay->addWidget(lbSearchFor); hlay->addWidget(le, 1); le->setFocus(); connect(le, &QLineEdit::textChanged, this, [this](const QString &s) { slotSearch(s); }); connect(mStartSearchTimer, &QTimer::timeout, this, &KeySelectionDialog::slotFilter); mKeyListView = new KeyListView(new ColumnStrategy(mKeyUsage), nullptr, page); mKeyListView->setObjectName(QStringLiteral("mKeyListView")); mKeyListView->header()->stretchLastSection(); mKeyListView->setRootIsDecorated(true); mKeyListView->setSortingEnabled(true); mKeyListView->header()->setSortIndicatorShown(true); mKeyListView->header()->setSortIndicator(1, Qt::AscendingOrder); // sort by User ID if (options & ExtendedSelection) { mKeyListView->setSelectionMode(QAbstractItemView::ExtendedSelection); } mTopLayout->addWidget(mKeyListView, 10); if (options & RememberChoice) { mRememberCB = new QCheckBox(i18n("&Remember choice"), page); mTopLayout->addWidget(mRememberCB); mRememberCB->setWhatsThis( i18n("

If you check this box your choice will " "be stored and you will not be asked again." "

")); } connect(mCheckSelectionTimer, &QTimer::timeout, this, [this]() { slotCheckSelection(); }); connectSignals(); connect(mKeyListView, &Kleo::KeyListView::doubleClicked, this, &KeySelectionDialog::slotTryOk); connect(mKeyListView, &KeyListView::contextMenu, this, &KeySelectionDialog::slotRMB); if (options & RereadKeys) { QPushButton *button = new QPushButton(i18n("&Reread Keys")); buttonBox->addButton(button, QDialogButtonBox::ActionRole); connect(button, &QPushButton::clicked, this, &KeySelectionDialog::slotRereadKeys); } if (options & ExternalCertificateManager) { QPushButton *button = new QPushButton(i18n("&Start Certificate Manager")); buttonBox->addButton(button, QDialogButtonBox::ActionRole); connect(button, &QPushButton::clicked, this, [this]() { slotStartCertificateManager(); }); } connect(mOkButton, &QPushButton::clicked, this, &KeySelectionDialog::slotOk); connect(buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &KeySelectionDialog::slotCancel); mTopLayout->activate(); if (qApp) { QSize dialogSize(sizeHint()); KConfigGroup dialogConfig(KSharedConfig::openStateConfig(), "Key Selection Dialog"); dialogSize = dialogConfig.readEntry("Dialog size", dialogSize); const QByteArray headerState = dialogConfig.readEntry("header", QByteArray()); if (!headerState.isEmpty()) { mKeyListView->header()->restoreState(headerState); } resize(dialogSize); } } void Kleo::KeySelectionDialog::init(bool rememberChoice, bool extendedSelection, const QString &text, const QString &initialQuery) { Options options = {RereadKeys, ExternalCertificateManager}; options.setFlag(ExtendedSelection, extendedSelection); options.setFlag(RememberChoice, rememberChoice); setUpUI(options, initialQuery); setText(text); if (mKeyUsage & OpenPGPKeys) { mOpenPGPBackend = QGpgME::openpgp(); } if (mKeyUsage & SMIMEKeys) { mSMIMEBackend = QGpgME::smime(); } slotRereadKeys(); } Kleo::KeySelectionDialog::~KeySelectionDialog() { disconnectSignals(); KConfigGroup dialogConfig(KSharedConfig::openStateConfig(), "Key Selection Dialog"); dialogConfig.writeEntry("Dialog size", size()); dialogConfig.writeEntry("header", mKeyListView->header()->saveState()); dialogConfig.sync(); } void Kleo::KeySelectionDialog::setText(const QString &text) { mTextLabel->setText(text); mTextLabel->setVisible(!text.isEmpty()); } void Kleo::KeySelectionDialog::setKeys(const std::vector &keys) { for (const GpgME::Key &key : keys) { mKeyListView->slotAddKey(key); } } void Kleo::KeySelectionDialog::connectSignals() { if (mKeyListView->isMultiSelection()) { connect(mKeyListView, &QTreeWidget::itemSelectionChanged, this, &KeySelectionDialog::slotSelectionChanged); } else { connect(mKeyListView, qOverload(&KeyListView::selectionChanged), this, qOverload(&KeySelectionDialog::slotCheckSelection)); } } void Kleo::KeySelectionDialog::disconnectSignals() { if (mKeyListView->isMultiSelection()) { disconnect(mKeyListView, &QTreeWidget::itemSelectionChanged, this, &KeySelectionDialog::slotSelectionChanged); } else { disconnect(mKeyListView, qOverload(&KeyListView::selectionChanged), this, qOverload(&KeySelectionDialog::slotCheckSelection)); } } const GpgME::Key &Kleo::KeySelectionDialog::selectedKey() const { static const GpgME::Key null = GpgME::Key::null; if (mKeyListView->isMultiSelection() || !mKeyListView->selectedItem()) { return null; } return mKeyListView->selectedItem()->key(); } QString Kleo::KeySelectionDialog::fingerprint() const { return QLatin1String(selectedKey().primaryFingerprint()); } QStringList Kleo::KeySelectionDialog::fingerprints() const { QStringList result; for (auto it = mSelectedKeys.begin(); it != mSelectedKeys.end(); ++it) { if (const char *fpr = it->primaryFingerprint()) { result.push_back(QLatin1String(fpr)); } } return result; } QStringList Kleo::KeySelectionDialog::pgpKeyFingerprints() const { QStringList result; for (auto it = mSelectedKeys.begin(); it != mSelectedKeys.end(); ++it) { if (it->protocol() == GpgME::OpenPGP) { if (const char *fpr = it->primaryFingerprint()) { result.push_back(QLatin1String(fpr)); } } } return result; } QStringList Kleo::KeySelectionDialog::smimeFingerprints() const { QStringList result; for (auto it = mSelectedKeys.begin(); it != mSelectedKeys.end(); ++it) { if (it->protocol() == GpgME::CMS) { if (const char *fpr = it->primaryFingerprint()) { result.push_back(QLatin1String(fpr)); } } } return result; } void Kleo::KeySelectionDialog::slotRereadKeys() { mKeyListView->clear(); mListJobCount = 0; mTruncated = 0; mSavedOffsetY = mKeyListView->verticalScrollBar()->value(); disconnectSignals(); mKeyListView->setEnabled(false); // FIXME: save current selection if (mOpenPGPBackend) { startKeyListJobForBackend(mOpenPGPBackend, std::vector(), false /*non-validating*/); } if (mSMIMEBackend) { startKeyListJobForBackend(mSMIMEBackend, std::vector(), false /*non-validating*/); } if (mListJobCount == 0) { mKeyListView->setEnabled(true); KMessageBox::information(this, i18n("No backends found for listing keys. " "Check your installation."), i18n("Key Listing Failed")); connectSignals(); } } void Kleo::KeySelectionDialog::slotStartCertificateManager(const QString &query) { QStringList args; if (!query.isEmpty()) { args << QStringLiteral("--search") << query; } const QString exec = QStandardPaths::findExecutable(QStringLiteral("kleopatra")); if (exec.isEmpty()) { qCWarning(KLEO_UI_LOG) << "Could not find kleopatra executable in PATH"; KMessageBox::error(this, i18n("Could not start certificate manager; " "please check your installation."), i18n("Certificate Manager Error")); } else { QProcess::startDetached(QStringLiteral("kleopatra"), args); qCDebug(KLEO_UI_LOG) << "\nslotStartCertManager(): certificate manager started."; } } #ifndef __KLEO_UI_SHOW_KEY_LIST_ERROR_H__ #define __KLEO_UI_SHOW_KEY_LIST_ERROR_H__ static void showKeyListError(QWidget *parent, const GpgME::Error &err) { Q_ASSERT(err); const QString msg = i18n( "

An error occurred while fetching " "the keys from the backend:

" "

%1

", QString::fromLocal8Bit(err.asString())); KMessageBox::error(parent, msg, i18n("Key Listing Failed")); } #endif // __KLEO_UI_SHOW_KEY_LIST_ERROR_H__ namespace { struct ExtractFingerprint { QString operator()(const GpgME::Key &key) { return QLatin1String(key.primaryFingerprint()); } }; } void Kleo::KeySelectionDialog::startKeyListJobForBackend(const QGpgME::Protocol *backend, const std::vector &keys, bool validate) { Q_ASSERT(backend); QGpgME::KeyListJob *job = backend->keyListJob(false, false, validate); // local, w/o sigs, validation as given if (!job) { return; } connect(job, &QGpgME::KeyListJob::result, this, &KeySelectionDialog::slotKeyListResult); if (validate) { connect(job, &QGpgME::KeyListJob::nextKey, mKeyListView, &KeyListView::slotRefreshKey); } else { connect(job, &QGpgME::KeyListJob::nextKey, mKeyListView, &KeyListView::slotAddKey); } QStringList fprs; std::transform(keys.begin(), keys.end(), std::back_inserter(fprs), ExtractFingerprint()); const GpgME::Error err = job->start(fprs, mKeyUsage & SecretKeys && !(mKeyUsage & PublicKeys)); if (err) { return showKeyListError(this, err); } #ifndef LIBKLEO_NO_PROGRESSDIALOG // FIXME: create a MultiProgressDialog: (void)new ProgressDialog(job, validate ? i18n("Checking selected keys...") : i18n("Fetching keys..."), this); #endif ++mListJobCount; } static void selectKeys(Kleo::KeyListView *klv, const std::vector &selectedKeys) { klv->clearSelection(); if (selectedKeys.empty()) { return; } for (auto it = selectedKeys.begin(); it != selectedKeys.end(); ++it) { if (Kleo::KeyListViewItem *item = klv->itemByFingerprint(it->primaryFingerprint())) { item->setSelected(true); } } } void Kleo::KeySelectionDialog::slotKeyListResult(const GpgME::KeyListResult &res) { if (res.error()) { showKeyListError(this, res.error()); } else if (res.isTruncated()) { ++mTruncated; } if (--mListJobCount > 0) { return; // not yet finished... } if (mTruncated > 0) { KMessageBox::information(this, i18np("One backend returned truncated output.

" "Not all available keys are shown

", "%1 backends returned truncated output.

" "Not all available keys are shown

", mTruncated), i18n("Key List Result")); } mKeyListView->flushKeys(); mKeyListView->setEnabled(true); mListJobCount = mTruncated = 0; mKeysToCheck.clear(); selectKeys(mKeyListView, mSelectedKeys); slotFilter(); connectSignals(); slotSelectionChanged(); // restore the saved position of the contents mKeyListView->verticalScrollBar()->setValue(mSavedOffsetY); mSavedOffsetY = 0; } void Kleo::KeySelectionDialog::slotSelectionChanged() { qCDebug(KLEO_UI_LOG) << "KeySelectionDialog::slotSelectionChanged()"; // (re)start the check selection timer. Checking the selection is delayed // because else drag-selection doesn't work very good (checking key trust // is slow). mCheckSelectionTimer->start(sCheckSelectionDelay); } namespace { struct AlreadyChecked { bool operator()(const GpgME::Key &key) const { return key.keyListMode() & GpgME::Validate; } }; } void Kleo::KeySelectionDialog::slotCheckSelection(KeyListViewItem *item) { qCDebug(KLEO_UI_LOG) << "KeySelectionDialog::slotCheckSelection()"; mCheckSelectionTimer->stop(); mSelectedKeys.clear(); if (!mKeyListView->isMultiSelection()) { if (item) { mSelectedKeys.push_back(item->key()); } } for (KeyListViewItem *it = mKeyListView->firstChild(); it; it = it->nextSibling()) { if (it->isSelected()) { mSelectedKeys.push_back(it->key()); } } mKeysToCheck.clear(); std::remove_copy_if(mSelectedKeys.begin(), mSelectedKeys.end(), std::back_inserter(mKeysToCheck), AlreadyChecked()); if (mKeysToCheck.empty()) { mOkButton->setEnabled(!mSelectedKeys.empty() && checkKeyUsage(mSelectedKeys, mKeyUsage)); return; } // performed all fast checks - now for validating key listing: startValidatingKeyListing(); } void Kleo::KeySelectionDialog::startValidatingKeyListing() { if (mKeysToCheck.empty()) { return; } mListJobCount = 0; mTruncated = 0; mSavedOffsetY = mKeyListView->verticalScrollBar()->value(); disconnectSignals(); mKeyListView->setEnabled(false); std::vector smime; std::vector openpgp; for (std::vector::const_iterator it = mKeysToCheck.begin(); it != mKeysToCheck.end(); ++it) { if (it->protocol() == GpgME::OpenPGP) { openpgp.push_back(*it); } else { smime.push_back(*it); } } if (!openpgp.empty()) { Q_ASSERT(mOpenPGPBackend); startKeyListJobForBackend(mOpenPGPBackend, openpgp, true /*validate*/); } if (!smime.empty()) { Q_ASSERT(mSMIMEBackend); startKeyListJobForBackend(mSMIMEBackend, smime, true /*validate*/); } Q_ASSERT(mListJobCount > 0); } bool Kleo::KeySelectionDialog::rememberSelection() const { return mRememberCB && mRememberCB->isChecked(); } void Kleo::KeySelectionDialog::slotRMB(Kleo::KeyListViewItem *item, const QPoint &p) { if (!item) { return; } mCurrentContextMenuItem = item; QMenu menu; menu.addAction(i18n("Recheck Key"), this, &KeySelectionDialog::slotRecheckKey); menu.exec(p); } void Kleo::KeySelectionDialog::slotRecheckKey() { if (!mCurrentContextMenuItem || mCurrentContextMenuItem->key().isNull()) { return; } mKeysToCheck.clear(); mKeysToCheck.push_back(mCurrentContextMenuItem->key()); } void Kleo::KeySelectionDialog::slotTryOk() { if (!mSelectedKeys.empty() && checkKeyUsage(mSelectedKeys, mKeyUsage)) { slotOk(); } } void Kleo::KeySelectionDialog::slotOk() { if (mCheckSelectionTimer->isActive()) { slotCheckSelection(); } #if 0 // Laurent I don't understand why we returns here. // button could be disabled again after checking the selected key1 if (!mSelectedKeys.empty() && checkKeyUsage(mSelectedKeys, mKeyUsage)) { return; } #endif mStartSearchTimer->stop(); accept(); } void Kleo::KeySelectionDialog::slotCancel() { mCheckSelectionTimer->stop(); mStartSearchTimer->stop(); reject(); } void Kleo::KeySelectionDialog::slotSearch(const QString &text) { mSearchText = text.trimmed().toUpper(); slotSearch(); } void Kleo::KeySelectionDialog::slotSearch() { mStartSearchTimer->setSingleShot(true); mStartSearchTimer->start(sCheckSelectionDelay); } void Kleo::KeySelectionDialog::slotFilter() { if (mSearchText.isEmpty()) { showAllItems(); return; } // OK, so we need to filter: QRegExp keyIdRegExp(QLatin1String("(?:0x)?[A-F0-9]{1,8}"), Qt::CaseInsensitive); if (keyIdRegExp.exactMatch(mSearchText)) { if (mSearchText.startsWith(QLatin1String("0X"))) // search for keyID only: { filterByKeyID(mSearchText.mid(2)); } else // search for UID and keyID: { filterByKeyIDOrUID(mSearchText); } } else { // search in UID: filterByUID(mSearchText); } } void Kleo::KeySelectionDialog::filterByKeyID(const QString &keyID) { Q_ASSERT(keyID.length() <= 8); Q_ASSERT(!keyID.isEmpty()); // regexp in slotFilter should prevent these if (keyID.isEmpty()) { showAllItems(); } else { for (KeyListViewItem *item = mKeyListView->firstChild(); item; item = item->nextSibling()) { item->setHidden(!item->text(0).toUpper().startsWith(keyID)); } } } static bool anyUIDMatches(const Kleo::KeyListViewItem *item, QRegExp &rx) { if (!item) { return false; } const std::vector uids = item->key().userIDs(); for (auto it = uids.begin(); it != uids.end(); ++it) { if (it->id() && rx.indexIn(QString::fromUtf8(it->id())) >= 0) { return true; } } return false; } void Kleo::KeySelectionDialog::filterByKeyIDOrUID(const QString &str) { Q_ASSERT(!str.isEmpty()); // match beginnings of words: QRegExp rx(QLatin1String("\\b") + QRegExp::escape(str), Qt::CaseInsensitive); for (KeyListViewItem *item = mKeyListView->firstChild(); item; item = item->nextSibling()) { item->setHidden(!item->text(0).toUpper().startsWith(str) && !anyUIDMatches(item, rx)); } } void Kleo::KeySelectionDialog::filterByUID(const QString &str) { Q_ASSERT(!str.isEmpty()); // match beginnings of words: QRegExp rx(QLatin1String("\\b") + QRegExp::escape(str), Qt::CaseInsensitive); for (KeyListViewItem *item = mKeyListView->firstChild(); item; item = item->nextSibling()) { item->setHidden(!anyUIDMatches(item, rx)); } } void Kleo::KeySelectionDialog::showAllItems() { for (KeyListViewItem *item = mKeyListView->firstChild(); item; item = item->nextSibling()) { item->setHidden(false); } } diff --git a/src/ui/messagebox.cpp b/src/ui/messagebox.cpp index a791fb95a..71955dec7 100644 --- a/src/ui/messagebox.cpp +++ b/src/ui/messagebox.cpp @@ -1,247 +1,249 @@ /* messagebox.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "messagebox.h" #include "auditlogviewer.h" #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::Private; using namespace GpgME; using namespace QGpgME; // static void MessageBox::auditLog(QWidget *parent, const Job *job, const QString &caption) { if (!job) { return; } if (!GpgME::hasFeature(AuditLogFeature, 0) || !job->isAuditLogSupported()) { KMessageBox::information(parent, i18n("Your system does not have support for GnuPG Audit Logs"), i18n("System Error")); return; } const GpgME::Error err = job->auditLogError(); if (err && err.code() != GPG_ERR_NO_DATA) { KMessageBox::information(parent, i18n("An error occurred while trying to retrieve the GnuPG Audit Log:\n%1", QString::fromLocal8Bit(err.asString())), i18n("GnuPG Audit Log Error")); return; } const QString log = job->auditLogAsHtml(); if (log.isEmpty()) { KMessageBox::information(parent, i18n("No GnuPG Audit Log available for this operation."), i18n("No GnuPG Audit Log")); return; } auditLog(parent, log, caption); } // static void MessageBox::auditLog(QWidget *parent, const QString &log, const QString &caption) { auto const alv = new AuditLogViewer(log, parent); alv->setAttribute(Qt::WA_DeleteOnClose); alv->setObjectName(QStringLiteral("alv")); alv->setWindowTitle(caption); alv->show(); } // static void MessageBox::auditLog(QWidget *parent, const Job *job) { auditLog(parent, job, i18n("GnuPG Audit Log Viewer")); } // static void MessageBox::auditLog(QWidget *parent, const QString &log) { auditLog(parent, log, i18n("GnuPG Audit Log Viewer")); } static QString to_information_string(const SigningResult &result) { return (result.error() // ? i18n("Signing failed: %1", QString::fromLocal8Bit(result.error().asString())) : i18n("Signing successful")); } static QString to_error_string(const SigningResult &result) { return to_information_string(result); } static QString to_information_string(const EncryptionResult &result) { return (result.error() // ? i18n("Encryption failed: %1", QString::fromLocal8Bit(result.error().asString())) : i18n("Encryption successful")); } static QString to_error_string(const EncryptionResult &result) { return to_information_string(result); } static QString to_information_string(const SigningResult &sresult, const EncryptionResult &eresult) { return to_information_string(sresult) + QLatin1Char('\n') + to_information_string(eresult); } static QString to_error_string(const SigningResult &sresult, const EncryptionResult &eresult) { return to_information_string(sresult, eresult); } // static void MessageBox::information(QWidget *parent, const SigningResult &result, const Job *job, KMessageBox::Options options) { information(parent, result, job, i18n("Signing Result"), options); } // static void MessageBox::information(QWidget *parent, const SigningResult &result, const Job *job, const QString &caption, KMessageBox::Options options) { make(parent, QMessageBox::Information, to_information_string(result), job, caption, options); } // static void MessageBox::error(QWidget *parent, const SigningResult &result, const Job *job, KMessageBox::Options options) { error(parent, result, job, i18n("Signing Error"), options); } // static void MessageBox::error(QWidget *parent, const SigningResult &result, const Job *job, const QString &caption, KMessageBox::Options options) { make(parent, QMessageBox::Critical, to_error_string(result), job, caption, options); } // static void MessageBox::information(QWidget *parent, const EncryptionResult &result, const Job *job, KMessageBox::Options options) { information(parent, result, job, i18n("Encryption Result"), options); } // static void MessageBox::information(QWidget *parent, const EncryptionResult &result, const Job *job, const QString &caption, KMessageBox::Options options) { make(parent, QMessageBox::Information, to_information_string(result), job, caption, options); } // static void MessageBox::error(QWidget *parent, const EncryptionResult &result, const Job *job, KMessageBox::Options options) { error(parent, result, job, i18n("Encryption Error"), options); } // static void MessageBox::error(QWidget *parent, const EncryptionResult &result, const Job *job, const QString &caption, KMessageBox::Options options) { make(parent, QMessageBox::Critical, to_error_string(result), job, caption, options); } // static void MessageBox::information(QWidget *parent, const SigningResult &sresult, const EncryptionResult &eresult, const Job *job, KMessageBox::Options options) { information(parent, sresult, eresult, job, i18n("Encryption Result"), options); } // static void MessageBox::information(QWidget *parent, const SigningResult &sresult, const EncryptionResult &eresult, const Job *job, const QString &caption, KMessageBox::Options options) { make(parent, QMessageBox::Information, to_information_string(sresult, eresult), job, caption, options); } // static void MessageBox::error(QWidget *parent, const SigningResult &sresult, const EncryptionResult &eresult, const Job *job, KMessageBox::Options options) { error(parent, sresult, eresult, job, i18n("Encryption Error"), options); } // static void MessageBox::error(QWidget *parent, const SigningResult &sresult, const EncryptionResult &eresult, const Job *job, const QString &caption, KMessageBox::Options options) { make(parent, QMessageBox::Critical, to_error_string(sresult, eresult), job, caption, options); } // static bool MessageBox::showAuditLogButton(const QGpgME::Job *job) { if (!job) { qCDebug(KLEO_UI_LOG) << "not showing audit log button (no job instance)"; return false; } if (!GpgME::hasFeature(GpgME::AuditLogFeature, 0)) { qCDebug(KLEO_UI_LOG) << "not showing audit log button (gpgme too old)"; return false; } if (!job->isAuditLogSupported()) { qCDebug(KLEO_UI_LOG) << "not showing audit log button (not supported)"; return false; } if (job->auditLogError().code() == GPG_ERR_NO_DATA) { qCDebug(KLEO_UI_LOG) << "not showing audit log button (GPG_ERR_NO_DATA)"; return false; } if (!job->auditLogError() && job->auditLogAsHtml().isEmpty()) { qCDebug(KLEO_UI_LOG) << "not showing audit log button (success, but result empty)"; return false; } return true; } // static void MessageBox::make(QWidget *parent, QMessageBox::Icon icon, const QString &text, const Job *job, const QString &caption, KMessageBox::Options options) { QDialog *dialog = new QDialog(parent); dialog->setWindowTitle(caption); QDialogButtonBox *box = new QDialogButtonBox(showAuditLogButton(job) ? (QDialogButtonBox::Yes | QDialogButtonBox::No) : QDialogButtonBox::Yes, parent); QPushButton *yesButton = box->button(QDialogButtonBox::Yes); yesButton->setDefault(true); // dialog->setEscapeButton(KDialog::Yes); dialog->setObjectName(QStringLiteral("error")); dialog->setModal(true); KGuiItem::assign(yesButton, KStandardGuiItem::ok()); if (GpgME::hasFeature(GpgME::AuditLogFeature, 0)) { KGuiItem::assign(box->button(QDialogButtonBox::No), KGuiItem(i18n("&Show Audit Log"))); } if (QDialogButtonBox::No == KMessageBox::createKMessageBox(dialog, box, icon, text, QStringList(), QString(), nullptr, options)) { auditLog(nullptr, job); } } diff --git a/src/ui/newkeyapprovaldialog.cpp b/src/ui/newkeyapprovaldialog.cpp index 197cba9a2..875e288f3 100644 --- a/src/ui/newkeyapprovaldialog.cpp +++ b/src/ui/newkeyapprovaldialog.cpp @@ -1,929 +1,931 @@ /* -*- c++ -*- newkeyapprovaldialog.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2018 Intevation GmbH SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "newkeyapprovaldialog.h" #include "keyselectioncombo.h" #include "progressdialog.h" #include "kleo/debug.h" #include "kleo/defaultkeyfilter.h" #include "utils/formatting.h" #include "utils/gnupg.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace GpgME; namespace { class EncryptFilter : public DefaultKeyFilter { public: EncryptFilter() : DefaultKeyFilter() { setCanEncrypt(DefaultKeyFilter::Set); } }; static std::shared_ptr s_encryptFilter = std::shared_ptr(new EncryptFilter); class OpenPGPFilter : public DefaultKeyFilter { public: OpenPGPFilter() : DefaultKeyFilter() { setIsOpenPGP(DefaultKeyFilter::Set); setCanEncrypt(DefaultKeyFilter::Set); } }; static std::shared_ptr s_pgpEncryptFilter = std::shared_ptr(new OpenPGPFilter); class OpenPGPSignFilter : public DefaultKeyFilter { public: OpenPGPSignFilter() : DefaultKeyFilter() { /* Also list unusable keys to make it transparent why they are unusable */ setDisabled(DefaultKeyFilter::NotSet); setRevoked(DefaultKeyFilter::NotSet); setExpired(DefaultKeyFilter::NotSet); setCanSign(DefaultKeyFilter::Set); setHasSecret(DefaultKeyFilter::Set); setIsOpenPGP(DefaultKeyFilter::Set); } }; static std::shared_ptr s_pgpSignFilter = std::shared_ptr(new OpenPGPSignFilter); class SMIMEFilter : public DefaultKeyFilter { public: SMIMEFilter() : DefaultKeyFilter() { setIsOpenPGP(DefaultKeyFilter::NotSet); setCanEncrypt(DefaultKeyFilter::Set); } }; static std::shared_ptr s_smimeEncryptFilter = std::shared_ptr(new SMIMEFilter); class SMIMESignFilter : public DefaultKeyFilter { public: SMIMESignFilter() : DefaultKeyFilter() { setDisabled(DefaultKeyFilter::NotSet); setRevoked(DefaultKeyFilter::NotSet); setExpired(DefaultKeyFilter::NotSet); setCanSign(DefaultKeyFilter::Set); setIsOpenPGP(DefaultKeyFilter::NotSet); setHasSecret(DefaultKeyFilter::Set); } }; static std::shared_ptr s_smimeSignFilter = std::shared_ptr(new SMIMESignFilter); /* Some decoration and a button to remove the filter for a keyselectioncombo */ class ComboWidget : public QWidget { Q_OBJECT public: explicit ComboWidget(KeySelectionCombo *combo) : mCombo(combo) , mFilterBtn(new QPushButton) { auto hLay = new QHBoxLayout(this); auto infoBtn = new QPushButton; infoBtn->setIcon(QIcon::fromTheme(QStringLiteral("help-contextual"))); infoBtn->setIconSize(QSize(22, 22)); infoBtn->setFlat(true); hLay->addWidget(infoBtn); hLay->addWidget(combo, 1); hLay->addWidget(mFilterBtn, 0); connect(infoBtn, &QPushButton::clicked, this, [this, infoBtn]() { QToolTip::showText(infoBtn->mapToGlobal(QPoint()) + QPoint(infoBtn->width(), 0), mCombo->currentData(Qt::ToolTipRole).toString(), infoBtn, QRect(), 30000); }); // FIXME: This is ugly to enforce but otherwise the // icon is broken. combo->setMinimumHeight(22); mFilterBtn->setMinimumHeight(23); updateFilterButton(); connect(mFilterBtn, &QPushButton::clicked, this, [this]() { const QString curFilter = mCombo->idFilter(); if (curFilter.isEmpty()) { setIdFilter(mLastIdFilter); mLastIdFilter = QString(); } else { setIdFilter(QString()); mLastIdFilter = curFilter; } }); } void setIdFilter(const QString &id) { mCombo->setIdFilter(id); updateFilterButton(); } void updateFilterButton() { if (mCombo->idFilter().isEmpty()) { mFilterBtn->setIcon(QIcon::fromTheme(QStringLiteral("kt-add-filters"))); mFilterBtn->setToolTip(i18n("Show keys matching the email address")); } else { mFilterBtn->setIcon(QIcon::fromTheme(QStringLiteral("kt-remove-filters"))); mFilterBtn->setToolTip(i18n("Show all keys")); } } KeySelectionCombo *combo() { return mCombo; } GpgME::Protocol fixedProtocol() const { return mFixedProtocol; } void setFixedProtocol(GpgME::Protocol proto) { mFixedProtocol = proto; } private: KeySelectionCombo *mCombo; QPushButton *mFilterBtn; QString mLastIdFilter; GpgME::Protocol mFixedProtocol = GpgME::UnknownProtocol; }; static enum GpgME::UserID::Validity keyValidity(const GpgME::Key &key) { enum GpgME::UserID::Validity validity = GpgME::UserID::Validity::Unknown; for (const auto &uid : key.userIDs()) { if (validity == GpgME::UserID::Validity::Unknown || validity > uid.validity()) { validity = uid.validity(); } } return validity; } static bool key_has_addr(const GpgME::Key &key, const QString &addr) { for (const auto &uid : key.userIDs()) { if (QString::fromStdString(uid.addrSpec()).toLower() == addr.toLower()) { return true; } } return false; } bool anyKeyHasProtocol(const std::vector &keys, GpgME::Protocol protocol) { return std::any_of(std::begin(keys), std::end(keys), [protocol](const auto &key) { return key.protocol() == protocol; }); } Key findfirstKeyOfType(const std::vector &keys, GpgME::Protocol protocol) { const auto it = std::find_if(std::begin(keys), std::end(keys), [protocol](const auto &key) { return key.protocol() == protocol; }); return it != std::end(keys) ? *it : Key(); } } // namespace class NewKeyApprovalDialog::Private { private: enum Action { Unset, GenerateKey, IgnoreKey, }; public: enum { OpenPGPButtonId = 1, SMIMEButtonId = 2, }; Private(NewKeyApprovalDialog *qq, bool encrypt, bool sign, GpgME::Protocol forcedProtocol, GpgME::Protocol presetProtocol, const QString &sender, bool allowMixed) : mForcedProtocol{forcedProtocol} , mSender{sender} , mSign{sign} , mEncrypt{encrypt} , mAllowMixed{allowMixed} , q{qq} { Q_ASSERT(forcedProtocol == GpgME::UnknownProtocol || presetProtocol == GpgME::UnknownProtocol || presetProtocol == forcedProtocol); Q_ASSERT(!allowMixed || (allowMixed && forcedProtocol == GpgME::UnknownProtocol)); Q_ASSERT(!(!allowMixed && presetProtocol == GpgME::UnknownProtocol)); // We do the translation here to avoid having the same string multiple times. mGenerateTooltip = i18nc( "@info:tooltip for a 'Generate new key pair' action " "in a combobox when a user does not yet have an OpenPGP or S/MIME key.", "Generate a new key using your E-Mail address.

" "The key is necessary to decrypt and sign E-Mails. " "You will be asked for a passphrase to protect this key and the protected key " "will be stored in your home directory."); mMainLay = new QVBoxLayout; QDialogButtonBox *btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); mOkButton = btnBox->button(QDialogButtonBox::Ok); #ifndef NDEBUG mOkButton->setObjectName(QStringLiteral("ok button")); #endif QObject::connect(btnBox, &QDialogButtonBox::accepted, q, [this]() { accepted(); }); QObject::connect(btnBox, &QDialogButtonBox::rejected, q, &QDialog::reject); mScrollArea = new QScrollArea; mScrollArea->setWidget(new QWidget); mScrollLayout = new QVBoxLayout; mScrollArea->widget()->setLayout(mScrollLayout); mScrollArea->setWidgetResizable(true); mScrollArea->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContentsOnFirstShow); mScrollArea->setFrameStyle(QFrame::NoFrame); mScrollLayout->setContentsMargins(0, 0, 0, 0); q->setWindowTitle(i18nc("@title:window", "Security approval")); auto fmtLayout = new QHBoxLayout; mFormatBtns = new QButtonGroup(qq); QAbstractButton *pgpBtn; QAbstractButton *smimeBtn; if (mAllowMixed) { pgpBtn = new QCheckBox(i18n("OpenPGP")); smimeBtn = new QCheckBox(i18n("S/MIME")); } else { pgpBtn = new QRadioButton(i18n("OpenPGP")); smimeBtn = new QRadioButton(i18n("S/MIME")); } #ifndef NDEBUG pgpBtn->setObjectName(QStringLiteral("openpgp button")); smimeBtn->setObjectName(QStringLiteral("smime button")); #endif mFormatBtns->addButton(pgpBtn, OpenPGPButtonId); mFormatBtns->addButton(smimeBtn, SMIMEButtonId); mFormatBtns->setExclusive(!mAllowMixed); fmtLayout->addStretch(-1); fmtLayout->addWidget(pgpBtn); fmtLayout->addWidget(smimeBtn); mMainLay->addLayout(fmtLayout); if (mForcedProtocol != GpgME::UnknownProtocol) { pgpBtn->setChecked(mForcedProtocol == GpgME::OpenPGP); smimeBtn->setChecked(mForcedProtocol == GpgME::CMS); pgpBtn->setVisible(false); smimeBtn->setVisible(false); } else { pgpBtn->setChecked(presetProtocol == GpgME::OpenPGP || presetProtocol == GpgME::UnknownProtocol); smimeBtn->setChecked(presetProtocol == GpgME::CMS || presetProtocol == GpgME::UnknownProtocol); } QObject::connect(mFormatBtns, &QButtonGroup::idClicked, q, [this](int buttonId) { // ensure that at least one protocol button is checked if (mAllowMixed && !mFormatBtns->button(OpenPGPButtonId)->isChecked() && !mFormatBtns->button(SMIMEButtonId)->isChecked()) { mFormatBtns->button(buttonId == OpenPGPButtonId ? SMIMEButtonId : OpenPGPButtonId)->setChecked(true); } updateWidgets(); }); mMainLay->addWidget(mScrollArea); mComplianceLbl = new QLabel; mComplianceLbl->setVisible(false); #ifndef NDEBUG mComplianceLbl->setObjectName(QStringLiteral("compliance label")); #endif auto btnLayout = new QHBoxLayout; btnLayout->addWidget(mComplianceLbl); btnLayout->addWidget(btnBox); mMainLay->addLayout(btnLayout); q->setLayout(mMainLay); } ~Private() = default; GpgME::Protocol currentProtocol() { const bool openPGPButtonChecked = mFormatBtns->button(OpenPGPButtonId)->isChecked(); const bool smimeButtonChecked = mFormatBtns->button(SMIMEButtonId)->isChecked(); if (mAllowMixed) { if (openPGPButtonChecked && !smimeButtonChecked) { return GpgME::OpenPGP; } if (!openPGPButtonChecked && smimeButtonChecked) { return GpgME::CMS; } } else if (openPGPButtonChecked) { return GpgME::OpenPGP; } else if (smimeButtonChecked) { return GpgME::CMS; } return GpgME::UnknownProtocol; } auto findVisibleKeySelectionComboWithGenerateKey() { const auto it = std::find_if(std::begin(mAllCombos), std::end(mAllCombos), [](auto combo) { return combo->isVisible() && combo->currentData(Qt::UserRole).toInt() == GenerateKey; }); return it != std::end(mAllCombos) ? *it : nullptr; } void generateKey(KeySelectionCombo *combo) { const auto &addr = combo->property("address").toString(); auto job = new QGpgME::DefaultKeyGenerationJob(q); auto progress = new Kleo::ProgressDialog(job, i18n("Generating key for '%1'...", addr) + QStringLiteral("\n\n") + i18n("This can take several minutes."), q); progress->setWindowFlags(progress->windowFlags() & ~Qt::WindowContextHelpButtonHint); progress->setWindowTitle(i18nc("@title:window", "Key generation")); progress->setModal(true); progress->setAutoClose(true); progress->setMinimumDuration(0); progress->setValue(0); mRunningJobs << job; connect(job, &QGpgME::DefaultKeyGenerationJob::result, q, [this, job, combo](const GpgME::KeyGenerationResult &result) { handleKeyGenResult(result, job, combo); }); job->start(addr, QString()); return; } void handleKeyGenResult(const GpgME::KeyGenerationResult &result, QGpgME::Job *job, KeySelectionCombo *combo) { mLastError = result.error(); if (!mLastError || mLastError.isCanceled()) { combo->setDefaultKey(QString::fromLatin1(result.fingerprint()), GpgME::OpenPGP); connect(combo, &KeySelectionCombo::keyListingFinished, q, [this, job]() { mRunningJobs.removeAll(job); }); combo->refreshKeys(); } else { mRunningJobs.removeAll(job); } } void checkAccepted() { if (mLastError || mLastError.isCanceled()) { KMessageBox::error(q, QString::fromLocal8Bit(mLastError.asString()), i18n("Operation Failed")); mRunningJobs.clear(); return; } if (!mRunningJobs.empty()) { return; } /* Save the keys */ mAcceptedResult.protocol = currentProtocol(); for (const auto combo : std::as_const(mEncCombos)) { const auto addr = combo->property("address").toString(); const auto key = combo->currentKey(); if (!combo->isVisible() || key.isNull()) { continue; } mAcceptedResult.encryptionKeys[addr].push_back(key); } for (const auto combo : std::as_const(mSigningCombos)) { const auto key = combo->currentKey(); if (!combo->isVisible() || key.isNull()) { continue; } mAcceptedResult.signingKeys.push_back(key); } q->accept(); } void accepted() { // We can assume everything was validly resolved, otherwise // the OK button would have been disabled. // Handle custom items now. if (auto combo = findVisibleKeySelectionComboWithGenerateKey()) { generateKey(combo); return; } checkAccepted(); } auto encryptionKeyFilter(GpgME::Protocol protocol) { switch (protocol) { case OpenPGP: return s_pgpEncryptFilter; case CMS: return s_smimeEncryptFilter; default: return s_encryptFilter; } } void updateWidgets() { const GpgME::Protocol protocol = currentProtocol(); const auto encryptionFilter = encryptionKeyFilter(protocol); for (auto combo : std::as_const(mSigningCombos)) { auto widget = qobject_cast(combo->parentWidget()); if (!widget) { qCDebug(LIBKLEO_LOG) << "Failed to find signature combo widget"; continue; } widget->setVisible(protocol == GpgME::UnknownProtocol || widget->fixedProtocol() == GpgME::UnknownProtocol || widget->fixedProtocol() == protocol); } for (auto combo : std::as_const(mEncCombos)) { auto widget = qobject_cast(combo->parentWidget()); if (!widget) { qCDebug(LIBKLEO_LOG) << "Failed to find combo widget"; continue; } widget->setVisible(protocol == GpgME::UnknownProtocol || widget->fixedProtocol() == GpgME::UnknownProtocol || widget->fixedProtocol() == protocol); if (widget->isVisible() && combo->property("address") != mSender) { combo->setKeyFilter(encryptionFilter); } } // hide the labels indicating the protocol of the sender's keys if only a single protocol is active const auto protocolLabels = q->findChildren(QStringLiteral("protocol label")); for (auto label : protocolLabels) { label->setVisible(protocol == GpgME::UnknownProtocol); } } auto createProtocolLabel(GpgME::Protocol protocol) { auto label = new QLabel(Formatting::displayName(protocol)); label->setObjectName(QStringLiteral("protocol label")); return label; } ComboWidget *createSigningCombo(const QString &addr, const GpgME::Key &key, GpgME::Protocol protocol = GpgME::UnknownProtocol) { Q_ASSERT(!key.isNull() || protocol != UnknownProtocol); protocol = !key.isNull() ? key.protocol() : protocol; auto combo = new KeySelectionCombo(); auto comboWidget = new ComboWidget(combo); #ifndef NDEBUG combo->setObjectName(QStringLiteral("signing key")); #endif if (protocol == GpgME::OpenPGP) { combo->setKeyFilter(s_pgpSignFilter); } else if (protocol == GpgME::CMS) { combo->setKeyFilter(s_smimeSignFilter); } if (key.isNull() || key_has_addr(key, mSender)) { comboWidget->setIdFilter(mSender); } comboWidget->setFixedProtocol(protocol); if (!key.isNull()) { combo->setDefaultKey(QString::fromLatin1(key.primaryFingerprint()), protocol); } if (key.isNull() && protocol == OpenPGP) { combo->appendCustomItem(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Generate a new key pair"), GenerateKey, mGenerateTooltip); } combo->appendCustomItem(QIcon::fromTheme(QStringLiteral("emblem-unavailable")), i18n("Don't confirm identity and integrity"), IgnoreKey, i18nc("@info:tooltip for not selecting a key for signing.", "The E-Mail will not be cryptographically signed.")); mSigningCombos << combo; mAllCombos << combo; combo->setProperty("address", addr); connect(combo, &KeySelectionCombo::currentKeyChanged, q, [this]() { updateOkButton(); }); connect(combo, qOverload(&QComboBox::currentIndexChanged), q, [this]() { updateOkButton(); }); return comboWidget; } void setSigningKeys(const std::vector &preferredKeys, const std::vector &alternativeKeys) { auto group = new QGroupBox(i18nc("Caption for signing key selection", "Confirm identity '%1' as:", mSender)); group->setAlignment(Qt::AlignLeft); auto sigLayout = new QVBoxLayout(group); const bool mayNeedOpenPGP = mForcedProtocol != CMS; const bool mayNeedCMS = mForcedProtocol != OpenPGP; if (mayNeedOpenPGP) { if (mAllowMixed) { sigLayout->addWidget(createProtocolLabel(OpenPGP)); } const Key preferredKey = findfirstKeyOfType(preferredKeys, OpenPGP); const Key alternativeKey = findfirstKeyOfType(alternativeKeys, OpenPGP); if (!preferredKey.isNull()) { qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for" << preferredKey; auto comboWidget = createSigningCombo(mSender, preferredKey); sigLayout->addWidget(comboWidget); } else if (!alternativeKey.isNull()) { qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for" << alternativeKey; auto comboWidget = createSigningCombo(mSender, alternativeKey); sigLayout->addWidget(comboWidget); } else { qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for OpenPGP key"; auto comboWidget = createSigningCombo(mSender, Key(), OpenPGP); sigLayout->addWidget(comboWidget); } } if (mayNeedCMS) { if (mAllowMixed) { sigLayout->addWidget(createProtocolLabel(CMS)); } const Key preferredKey = findfirstKeyOfType(preferredKeys, CMS); const Key alternativeKey = findfirstKeyOfType(alternativeKeys, CMS); if (!preferredKey.isNull()) { qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for" << preferredKey; auto comboWidget = createSigningCombo(mSender, preferredKey); sigLayout->addWidget(comboWidget); } else if (!alternativeKey.isNull()) { qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for" << alternativeKey; auto comboWidget = createSigningCombo(mSender, alternativeKey); sigLayout->addWidget(comboWidget); } else { qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for S/MIME key"; auto comboWidget = createSigningCombo(mSender, Key(), CMS); sigLayout->addWidget(comboWidget); } } mScrollLayout->addWidget(group); } ComboWidget *createEncryptionCombo(const QString &addr, const GpgME::Key &key, GpgME::Protocol fixedProtocol) { auto combo = new KeySelectionCombo(false); auto comboWidget = new ComboWidget(combo); #ifndef NDEBUG combo->setObjectName(QStringLiteral("encryption key")); #endif if (fixedProtocol == GpgME::OpenPGP) { combo->setKeyFilter(s_pgpEncryptFilter); } else if (fixedProtocol == GpgME::CMS) { combo->setKeyFilter(s_smimeEncryptFilter); } else { combo->setKeyFilter(s_encryptFilter); } if (key.isNull() || key_has_addr(key, addr)) { comboWidget->setIdFilter(addr); } comboWidget->setFixedProtocol(fixedProtocol); if (!key.isNull()) { combo->setDefaultKey(QString::fromLatin1(key.primaryFingerprint()), fixedProtocol); } if (addr == mSender && key.isNull() && fixedProtocol == OpenPGP) { combo->appendCustomItem(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Generate a new key pair"), GenerateKey, mGenerateTooltip); } combo->appendCustomItem(QIcon::fromTheme(QStringLiteral("emblem-unavailable")), i18n("No key. Recipient will be unable to decrypt."), IgnoreKey, i18nc("@info:tooltip for No Key selected for a specific recipient.", "Do not select a key for this recipient.

" "The recipient will receive the encrypted E-Mail, but it can only " "be decrypted with the other keys selected in this dialog.")); connect(combo, &KeySelectionCombo::currentKeyChanged, q, [this]() { updateOkButton(); }); connect(combo, qOverload(&QComboBox::currentIndexChanged), q, [this]() { updateOkButton(); }); mEncCombos << combo; mAllCombos << combo; combo->setProperty("address", addr); return comboWidget; } void addEncryptionAddr(const QString &addr, GpgME::Protocol preferredKeysProtocol, const std::vector &preferredKeys, GpgME::Protocol alternativeKeysProtocol, const std::vector &alternativeKeys, QGridLayout *encGrid) { if (addr == mSender) { const bool mayNeedOpenPGP = mForcedProtocol != CMS; const bool mayNeedCMS = mForcedProtocol != OpenPGP; if (mayNeedOpenPGP) { if (mAllowMixed) { encGrid->addWidget(createProtocolLabel(OpenPGP), encGrid->rowCount(), 0); } for (const auto &key : preferredKeys) { if (key.protocol() == OpenPGP) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key; auto comboWidget = createEncryptionCombo(addr, key, OpenPGP); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } } for (const auto &key : alternativeKeys) { if (key.protocol() == OpenPGP) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key; auto comboWidget = createEncryptionCombo(addr, key, OpenPGP); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } } if (!anyKeyHasProtocol(preferredKeys, OpenPGP) && !anyKeyHasProtocol(alternativeKeys, OpenPGP)) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for OpenPGP key"; auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), OpenPGP); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } } if (mayNeedCMS) { if (mAllowMixed) { encGrid->addWidget(createProtocolLabel(CMS), encGrid->rowCount(), 0); } for (const auto &key : preferredKeys) { if (key.protocol() == CMS) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key; auto comboWidget = createEncryptionCombo(addr, key, CMS); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } } for (const auto &key : alternativeKeys) { if (key.protocol() == CMS) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key; auto comboWidget = createEncryptionCombo(addr, key, CMS); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } } if (!anyKeyHasProtocol(preferredKeys, CMS) && !anyKeyHasProtocol(alternativeKeys, CMS)) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for S/MIME key"; auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), CMS); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } } } else { encGrid->addWidget(new QLabel(addr), encGrid->rowCount(), 0); for (const auto &key : preferredKeys) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key; auto comboWidget = createEncryptionCombo(addr, key, preferredKeysProtocol); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } for (const auto &key : alternativeKeys) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key; auto comboWidget = createEncryptionCombo(addr, key, alternativeKeysProtocol); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } if (!mAllowMixed) { if (preferredKeys.empty()) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << Formatting::displayName(preferredKeysProtocol) << "key"; auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), preferredKeysProtocol); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } if (alternativeKeys.empty() && alternativeKeysProtocol != UnknownProtocol) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << Formatting::displayName(alternativeKeysProtocol) << "key"; auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), alternativeKeysProtocol); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } } else { if (preferredKeys.empty() && alternativeKeys.empty()) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for any key"; auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), UnknownProtocol); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } } } } void setEncryptionKeys(GpgME::Protocol preferredKeysProtocol, const QMap> &preferredKeys, GpgME::Protocol alternativeKeysProtocol, const QMap> &alternativeKeys) { { auto group = new QGroupBox(i18nc("Encrypt to self (email address):", "Encrypt to self (%1):", mSender)); #ifndef NDEBUG group->setObjectName(QStringLiteral("encrypt-to-self box")); #endif group->setAlignment(Qt::AlignLeft); auto encGrid = new QGridLayout(group); addEncryptionAddr(mSender, preferredKeysProtocol, preferredKeys.value(mSender), alternativeKeysProtocol, alternativeKeys.value(mSender), encGrid); encGrid->setColumnStretch(1, -1); mScrollLayout->addWidget(group); } const bool hasOtherRecipients = std::any_of(preferredKeys.keyBegin(), preferredKeys.keyEnd(), [this](const auto &recipient) { return recipient != mSender; }); if (hasOtherRecipients) { auto group = new QGroupBox(i18n("Encrypt to others:")); #ifndef NDEBUG group->setObjectName(QStringLiteral("encrypt-to-others box")); #endif group->setAlignment(Qt::AlignLeft); auto encGrid = new QGridLayout{group}; for (auto it = std::begin(preferredKeys); it != std::end(preferredKeys); ++it) { const auto &address = it.key(); const auto &keys = it.value(); if (address != mSender) { addEncryptionAddr(address, preferredKeysProtocol, keys, alternativeKeysProtocol, alternativeKeys.value(address), encGrid); } } encGrid->setColumnStretch(1, -1); mScrollLayout->addWidget(group); } mScrollLayout->addStretch(-1); } void updateOkButton() { static QString origOkText = mOkButton->text(); const bool isGenerate = bool(findVisibleKeySelectionComboWithGenerateKey()); const bool allVisibleEncryptionKeysAreIgnored = std::all_of(std::begin(mEncCombos), std::end(mEncCombos), [](auto combo) { return !combo->isVisible() || combo->currentData(Qt::UserRole).toInt() == IgnoreKey; }); // If we don't encrypt the ok button is always enabled. But otherwise // we only enable it if we encrypt to at least one recipient. mOkButton->setEnabled(!mEncrypt || !allVisibleEncryptionKeysAreIgnored); mOkButton->setText(isGenerate ? i18n("Generate") : origOkText); if (!Kleo::gnupgUsesDeVsCompliance()) { return; } // Handle compliance bool de_vs = Kleo::gnupgIsDeVsCompliant(); if (de_vs) { const GpgME::Protocol protocol = currentProtocol(); for (const auto combo : std::as_const(mAllCombos)) { if (!combo->isVisible()) { continue; } const auto key = combo->currentKey(); if (key.isNull()) { continue; } if (protocol != GpgME::UnknownProtocol && key.protocol() != protocol) { continue; } if (!Formatting::isKeyDeVs(key) || keyValidity(key) < GpgME::UserID::Validity::Full) { de_vs = false; break; } } } if (de_vs) { mOkButton->setIcon(QIcon::fromTheme(QStringLiteral("security-high"))); mOkButton->setStyleSheet( QStringLiteral("background-color: ") + QStringLiteral( "#D5FAE2")); // KColorScheme(QPalette::Active, KColorScheme::View).background(KColorScheme::PositiveBackground).color().name()); mComplianceLbl->setText(i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "%1 communication possible.", Formatting::deVsString())); } else { mOkButton->setIcon(QIcon::fromTheme(QStringLiteral("security-medium"))); mOkButton->setStyleSheet( QStringLiteral("background-color: ") + QStringLiteral( "#FAE9EB")); // KColorScheme(QPalette::Active, KColorScheme::View).background(KColorScheme::NegativeBackground).color().name())); mComplianceLbl->setText(i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "%1 communication not possible.", Formatting::deVsString())); } mComplianceLbl->setVisible(true); } GpgME::Protocol mForcedProtocol; QList mSigningCombos; QList mEncCombos; QList mAllCombos; QScrollArea *mScrollArea; QVBoxLayout *mScrollLayout; QPushButton *mOkButton; QVBoxLayout *mMainLay; QButtonGroup *mFormatBtns; QString mSender; bool mSign; bool mEncrypt; bool mAllowMixed; NewKeyApprovalDialog *q; QList mRunningJobs; GpgME::Error mLastError; QLabel *mComplianceLbl; KeyResolver::Solution mAcceptedResult; QString mGenerateTooltip; }; NewKeyApprovalDialog::NewKeyApprovalDialog(bool encrypt, bool sign, const QString &sender, KeyResolver::Solution preferredSolution, KeyResolver::Solution alternativeSolution, bool allowMixed, GpgME::Protocol forcedProtocol, QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f) , d{std::make_unique(this, encrypt, sign, forcedProtocol, preferredSolution.protocol, sender, allowMixed)} { if (sign) { d->setSigningKeys(std::move(preferredSolution.signingKeys), std::move(alternativeSolution.signingKeys)); } if (encrypt) { d->setEncryptionKeys(allowMixed ? UnknownProtocol : preferredSolution.protocol, std::move(preferredSolution.encryptionKeys), allowMixed ? UnknownProtocol : alternativeSolution.protocol, std::move(alternativeSolution.encryptionKeys)); } d->updateWidgets(); d->updateOkButton(); const auto size = sizeHint(); const auto desk = screen()->size(); resize(QSize(desk.width() / 3, qMin(size.height(), desk.height() / 2))); } Kleo::NewKeyApprovalDialog::~NewKeyApprovalDialog() = default; KeyResolver::Solution NewKeyApprovalDialog::result() { return d->mAcceptedResult; } #include "newkeyapprovaldialog.moc" diff --git a/src/ui/progressbar.cpp b/src/ui/progressbar.cpp index 4bfba0f6d..b6d481b99 100644 --- a/src/ui/progressbar.cpp +++ b/src/ui/progressbar.cpp @@ -1,98 +1,100 @@ /* progressbar.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "progressbar.h" #include #include static const int busyTimerTickInterval = 100; static const int busyTimerTickIncrement = 5; Kleo::ProgressBar::ProgressBar(QWidget *parent) : QProgressBar(parent) , mRealProgress(-1) { mBusyTimer = new QTimer(this); connect(mBusyTimer, &QTimer::timeout, this, &ProgressBar::slotBusyTimerTick); fixup(true); } void Kleo::ProgressBar::slotProgress(const QString &, int cur, int tot) { setRange(cur, tot); } void Kleo::ProgressBar::slotProgress(const QString &, int, int cur, int tot) { setRange(cur, tot); } void Kleo::ProgressBar::setMaximum(int total) { qCDebug(KLEO_UI_LOG) << "Kleo::ProgressBar::setMaximum(" << total << " )"; if (total == maximum()) { return; } QProgressBar::setMaximum(0); fixup(false); } void Kleo::ProgressBar::setValue(int p) { qCDebug(KLEO_UI_LOG) << "Kleo::ProgressBar::setValue(" << p << " )"; mRealProgress = p; fixup(true); } void Kleo::ProgressBar::reset() { mRealProgress = -1; fixup(true); } void Kleo::ProgressBar::slotBusyTimerTick() { fixup(false); if (mBusyTimer->isActive()) { QProgressBar::setValue(QProgressBar::value() + busyTimerTickIncrement); } } void Kleo::ProgressBar::fixup(bool newValue) { const int cur = QProgressBar::value(); const int tot = QProgressBar::maximum(); qCDebug(KLEO_UI_LOG) << "Kleo::ProgressBar::startStopBusyTimer() cur =" << cur << "; tot =" << tot << "; real =" << mRealProgress; if ((newValue && mRealProgress < 0) || (!newValue && cur < 0)) { qCDebug(KLEO_UI_LOG) << "(new value) switch to reset"; mBusyTimer->stop(); if (newValue) { QProgressBar::reset(); } mRealProgress = -1; } else if (tot == 0) { qCDebug(KLEO_UI_LOG) << "(new value) switch or stay in busy"; if (!mBusyTimer->isActive()) { mBusyTimer->start(busyTimerTickInterval); if (newValue) { QProgressBar::setValue(mRealProgress); } } } else { qCDebug(KLEO_UI_LOG) << "(new value) normal progress"; mBusyTimer->stop(); if (QProgressBar::value() != mRealProgress) { QProgressBar::setValue(mRealProgress); } } } diff --git a/src/ui/progressdialog.cpp b/src/ui/progressdialog.cpp index 65f9ef4c6..c10026ca4 100644 --- a/src/ui/progressdialog.cpp +++ b/src/ui/progressdialog.cpp @@ -1,75 +1,77 @@ /* progressdialog.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "progressdialog.h" #ifndef QT_NO_PROGRESSDIALOG #include "progressbar.h" #include #include #include Kleo::ProgressDialog::ProgressDialog(QGpgME::Job *job, const QString &baseText, QWidget *creator, Qt::WindowFlags f) : QProgressDialog(creator, f) , mBaseText(baseText) { Q_ASSERT(job); setBar(new ProgressBar(this /*, "replacement progressbar in Kleo::ProgressDialog"*/)); setMinimumDuration(2000 /*ms*/); setAutoReset(false); setAutoClose(false); setLabelText(baseText); setModal(false); setRange(0, 0); // activate busy indicator connect(job, &QGpgME::Job::progress, this, &ProgressDialog::slotProgress); connect(job, &QGpgME::Job::done, this, &ProgressDialog::slotDone); connect(this, &QProgressDialog::canceled, job, &QGpgME::Job::slotCancel); QTimer::singleShot(minimumDuration(), this, &ProgressDialog::forceShow); } Kleo::ProgressDialog::~ProgressDialog() { } void Kleo::ProgressDialog::setMinimumDuration(int ms) { if (0 < ms && ms < minimumDuration()) { QTimer::singleShot(ms, this, &ProgressDialog::forceShow); } QProgressDialog::setMinimumDuration(ms); } void Kleo::ProgressDialog::slotProgress(const QString &what, int current, int total) { qCDebug(KLEO_UI_LOG) << "Kleo::ProgressDialog::slotProgress( \"" << what << "\"," << current << "," << total << ")"; if (mBaseText.isEmpty()) { setLabelText(what); } else if (what.isEmpty()) { setLabelText(mBaseText); } else { setLabelText(i18n("%1: %2", mBaseText, what)); } setRange(current, total); } void Kleo::ProgressDialog::slotDone() { qCDebug(KLEO_UI_LOG) << "Kleo::ProgressDialog::slotDone()"; hide(); deleteLater(); } #endif // QT_NO_PROGRESSDIALOG diff --git a/src/utils/classify.cpp b/src/utils/classify.cpp index a08f04db0..918be5313 100644 --- a/src/utils/classify.cpp +++ b/src/utils/classify.cpp @@ -1,422 +1,424 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/classify.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "classify.h" #include "kleo/checksumdefinition.h" #include "utils/algorithm.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo::Class; namespace { const unsigned int ExamineContentHint = 0x8000; static const struct _classification { char extension[4]; unsigned int classification; } classifications[] = { // ordered by extension {"arl", Kleo::Class::CMS | Binary | CertificateRevocationList}, {"asc", Kleo::Class::OpenPGP | Ascii | OpaqueSignature | DetachedSignature | CipherText | AnyCertStoreType | ExamineContentHint}, {"cer", Kleo::Class::CMS | Binary | Certificate}, {"crl", Kleo::Class::CMS | Binary | CertificateRevocationList}, {"crt", Kleo::Class::CMS | Binary | Certificate}, {"der", Kleo::Class::CMS | Binary | Certificate | CertificateRevocationList}, {"gpg", Kleo::Class::OpenPGP | Binary | OpaqueSignature | CipherText | AnyCertStoreType | ExamineContentHint}, {"p10", Kleo::Class::CMS | Ascii | CertificateRequest}, {"p12", Kleo::Class::CMS | Binary | ExportedPSM}, {"p7c", Kleo::Class::CMS | Binary | Certificate}, {"p7m", Kleo::Class::CMS | AnyFormat | CipherText}, {"p7s", Kleo::Class::CMS | AnyFormat | AnySignature}, {"pem", Kleo::Class::CMS | Ascii | AnyType | ExamineContentHint}, {"pfx", Kleo::Class::CMS | Binary | Certificate}, {"pgp", Kleo::Class::OpenPGP | Binary | OpaqueSignature | CipherText | AnyCertStoreType | ExamineContentHint}, {"sig", Kleo::Class::OpenPGP | AnyFormat | DetachedSignature}, }; static const QMap gpgmeTypeMap{ // clang-format off {GpgME::Data::PGPSigned, Kleo::Class::OpenPGP | OpaqueSignature }, /* PGPOther might be just an unencrypted unsigned pgp message. Decrypt * would yield the plaintext anyway so for us this is CipherText. */ {GpgME::Data::PGPOther, Kleo::Class::OpenPGP | CipherText }, {GpgME::Data::PGPKey, Kleo::Class::OpenPGP | Certificate }, {GpgME::Data::CMSSigned, Kleo::Class::CMS | AnySignature }, {GpgME::Data::CMSEncrypted, Kleo::Class::CMS | CipherText }, /* See PGPOther */ {GpgME::Data::CMSOther, Kleo::Class::CMS | CipherText }, {GpgME::Data::X509Cert, Kleo::Class::CMS | Certificate }, {GpgME::Data::PKCS12, Kleo::Class::CMS | Binary | ExportedPSM }, {GpgME::Data::PGPEncrypted, Kleo::Class::OpenPGP | CipherText }, {GpgME::Data::PGPSignature, Kleo::Class::OpenPGP | DetachedSignature}, // clang-format on }; static const unsigned int defaultClassification = NoClass; template class Op> struct ByExtension { using result_type = bool; template bool operator()(const T &lhs, const T &rhs) const { return Op()(qstricmp(lhs.extension, rhs.extension), 0); } template bool operator()(const T &lhs, const char *rhs) const { return Op()(qstricmp(lhs.extension, rhs), 0); } template bool operator()(const char *lhs, const T &rhs) const { return Op()(qstricmp(lhs, rhs.extension), 0); } bool operator()(const char *lhs, const char *rhs) const { return Op()(qstricmp(lhs, rhs), 0); } }; static const struct _content_classification { char content[28]; unsigned int classification; } content_classifications[] = { // clang-format off {"CERTIFICATE", Certificate }, {"ENCRYPTED MESSAGE", CipherText }, {"MESSAGE", OpaqueSignature | CipherText }, {"PKCS12", ExportedPSM }, {"PRIVATE KEY BLOCK", ExportedPSM }, {"PUBLIC KEY BLOCK", Certificate }, {"SIGNATURE", DetachedSignature }, {"SIGNED MESSAGE", ClearsignedMessage | DetachedSignature}, // clang-format on }; template class Op> struct ByContent { using result_type = bool; const unsigned int N; explicit ByContent(unsigned int n) : N(n) { } template bool operator()(const T &lhs, const T &rhs) const { return Op()(qstrncmp(lhs.content, rhs.content, N), 0); } template bool operator()(const T &lhs, const char *rhs) const { return Op()(qstrncmp(lhs.content, rhs, N), 0); } template bool operator()(const char *lhs, const T &rhs) const { return Op()(qstrncmp(lhs, rhs.content, N), 0); } bool operator()(const char *lhs, const char *rhs) const { return Op()(qstrncmp(lhs, rhs, N), 0); } }; } unsigned int Kleo::classify(const QStringList &fileNames) { if (fileNames.empty()) { return 0; } unsigned int result = classify(fileNames.front()); for (const QString &fileName : fileNames) { result &= classify(fileName); } return result; } static unsigned int classifyExtension(const QFileInfo &fi) { const _classification *const it = Kleo::binary_find(std::begin(classifications), std::end(classifications), fi.suffix().toLatin1().constData(), ByExtension()); if (it != std::end(classifications)) { if (!(it->classification & ExamineContentHint)) { return it->classification; } } return it == std::end(classifications) ? defaultClassification : it->classification; } unsigned int Kleo::classify(const QString &filename) { Q_ASSERT(std::is_sorted(std::begin(classifications), std::end(classifications), ByExtension())); const QFileInfo fi(filename); if (!fi.exists()) { return 0; } QFile file(filename); /* The least reliable but always available classification */ const unsigned int extClass = classifyExtension(fi); if (!GpgME::hasFeature(0, GpgME::BinaryAndFineGrainedIdentify) && !(extClass & ExamineContentHint)) { /* GpgME's identify and our internal Classify were so incomplete * before BinaryAndFineGrainedIdentify that we are better of * to just use the file extension if ExamineContentHint is not set. */ qCDebug(LIBKLEO_LOG) << "Classified based only on extension."; return extClass; } if (!file.open(QIODevice::ReadOnly)) { qCDebug(LIBKLEO_LOG) << "Failed to open file: " << filename << " for classification."; return extClass; } /* More reliable */ const unsigned int contentClass = classifyContent(file.read(4096)); if (contentClass != defaultClassification) { qCDebug(LIBKLEO_LOG) << "Classified based on content as:" << contentClass; return contentClass; } /* Probably some X509 Stuff that GpgME in it's wisdom does not handle. Again * file extension is probably more reliable as the last resort. */ qCDebug(LIBKLEO_LOG) << "No classification based on content."; return extClass; } static unsigned int classifyContentInteral(const QByteArray &data) { Q_ASSERT(std::is_sorted(std::begin(content_classifications), std::end(content_classifications), ByContent(100))); static const char beginString[] = "-----BEGIN "; static const QByteArrayMatcher beginMatcher(beginString); int pos = beginMatcher.indexIn(data); if (pos < 0) { return defaultClassification; } pos += sizeof beginString - 1; const bool pgp = qstrncmp(data.data() + pos, "PGP ", 4) == 0; if (pgp) { pos += 4; } const int epos = data.indexOf("-----\n", pos); if (epos < 0) { return defaultClassification; } const _content_classification *const cit = Kleo::binary_find(std::begin(content_classifications), std::end(content_classifications), data.data() + pos, ByContent(epos - pos)); if (cit != std::end(content_classifications)) { return cit->classification | (pgp ? Kleo::Class::OpenPGP : Kleo::Class::CMS); } return defaultClassification; } unsigned int Kleo::classifyContent(const QByteArray &data) { /* As of Version 1.6.0 GpgME does not distinguish between detached * signatures and signatures. So we prefer kleo's classification and * only use gpgme as fallback. * With newer versions we have a better identify that really inspects * the PGP Packages. Which is by far the most reliable classification. * So this is already used for the default classification. File extensions * and our classifyinternal is only used as a fallback. */ if (!GpgME::hasFeature(0, GpgME::BinaryAndFineGrainedIdentify)) { unsigned int ourClassification = classifyContentInteral(data); if (ourClassification != defaultClassification) { return ourClassification; } } QGpgME::QByteArrayDataProvider dp(data); GpgME::Data gpgmeData(&dp); GpgME::Data::Type type = gpgmeData.type(); return gpgmeTypeMap.value(type, defaultClassification); } QString Kleo::printableClassification(unsigned int classification) { QStringList parts; if (classification & Kleo::Class::CMS) { parts.push_back(QStringLiteral("CMS")); } if (classification & Kleo::Class::OpenPGP) { parts.push_back(QStringLiteral("OpenPGP")); } if (classification & Kleo::Class::Binary) { parts.push_back(QStringLiteral("Binary")); } if (classification & Kleo::Class::Ascii) { parts.push_back(QStringLiteral("Ascii")); } if (classification & Kleo::Class::DetachedSignature) { parts.push_back(QStringLiteral("DetachedSignature")); } if (classification & Kleo::Class::OpaqueSignature) { parts.push_back(QStringLiteral("OpaqueSignature")); } if (classification & Kleo::Class::ClearsignedMessage) { parts.push_back(QStringLiteral("ClearsignedMessage")); } if (classification & Kleo::Class::CipherText) { parts.push_back(QStringLiteral("CipherText")); } if (classification & Kleo::Class::Certificate) { parts.push_back(QStringLiteral("Certificate")); } if (classification & Kleo::Class::ExportedPSM) { parts.push_back(QStringLiteral("ExportedPSM")); } if (classification & Kleo::Class::CertificateRequest) { parts.push_back(QStringLiteral("CertificateRequest")); } return parts.join(QLatin1String(", ")); } static QString chopped(QString s, unsigned int n) { s.chop(n); return s; } /*! \return the data file that corresponds to the signature file \a signatureFileName, or QString(), if no such file can be found. */ QString Kleo::findSignedData(const QString &signatureFileName) { if (!mayBeDetachedSignature(signatureFileName)) { return QString(); } const QString baseName = chopped(signatureFileName, 4); return QFile::exists(baseName) ? baseName : QString(); } /*! \return all (existing) candidate signature files for \a signedDataFileName Note that there can very well be more than one such file, e.g. if the same data file was signed by both CMS and OpenPGP certificates. */ QStringList Kleo::findSignatures(const QString &signedDataFileName) { QStringList result; for (unsigned int i = 0, end = sizeof(classifications) / sizeof(_classification); i < end; ++i) { if (classifications[i].classification & DetachedSignature) { const QString candidate = signedDataFileName + QLatin1Char('.') + QLatin1String(classifications[i].extension); if (QFile::exists(candidate)) { result.push_back(candidate); } } } return result; } /*! \return the (likely) output filename for \a inputFileName, or "inputFileName.out" if none can be determined. */ QString Kleo::outputFileName(const QString &inputFileName) { const QFileInfo fi(inputFileName); if (!std::binary_search(std::begin(classifications), std::end(classifications), fi.suffix().toLatin1().constData(), ByExtension())) { return inputFileName + QLatin1String(".out"); } else { return chopped(inputFileName, 4); } } /*! \return the commonly used extension for files of type \a classification, or NULL if none such exists. */ const char *Kleo::outputFileExtension(unsigned int classification, bool usePGPFileExt) { if (usePGPFileExt && (classification & Class::OpenPGP) && (classification & Class::Binary)) { return "pgp"; } for (unsigned int i = 0; i < sizeof classifications / sizeof *classifications; ++i) { if ((classifications[i].classification & classification) == classification) { return classifications[i].extension; } } return nullptr; } bool Kleo::isFingerprint(const QString &fpr) { static QRegularExpression fprRegex(QStringLiteral("[0-9a-fA-F]{40}")); return fprRegex.match(fpr).hasMatch(); } bool Kleo::isChecksumFile(const QString &file) { static bool initialized; static QList patterns; const QFileInfo fi(file); if (!fi.exists()) { return false; } if (!initialized) { const auto getChecksumDefinitions = ChecksumDefinition::getChecksumDefinitions(); for (const std::shared_ptr &cd : getChecksumDefinitions) { if (cd) { const auto patternsList = cd->patterns(); for (const QString &pattern : patternsList) { #ifdef Q_OS_WIN patterns << QRegExp(pattern, Qt::CaseInsensitive); #else patterns << QRegExp(pattern, Qt::CaseSensitive); #endif } } } initialized = true; } const QString fileName = fi.fileName(); for (const QRegExp &pattern : std::as_const(patterns)) { if (pattern.exactMatch(fileName)) { return true; } } return false; } diff --git a/src/utils/compat.cpp b/src/utils/compat.cpp index ba62e5d2e..05bb89786 100644 --- a/src/utils/compat.cpp +++ b/src/utils/compat.cpp @@ -1,43 +1,45 @@ /* utils/compat.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "compat.h" #include #include #if QGPGME_VERSION >= 0x11000 // 1.16.0 #define CRYPTOCONFIG_HAS_GROUPLESS_ENTRY_OVERLOAD #endif using namespace QGpgME; QGpgME::CryptoConfigEntry *Kleo::getCryptoConfigEntry(const CryptoConfig *config, const char *componentName, const char *entryName) { if (!config) { return nullptr; } #ifdef CRYPTOCONFIG_HAS_GROUPLESS_ENTRY_OVERLOAD return config->entry(QString::fromLatin1(componentName), QString::fromLatin1(entryName)); #else const CryptoConfigComponent *const comp = config->component(QString::fromLatin1(componentName)); if (!comp) { return nullptr; } const QStringList groupNames = comp->groupList(); for (const auto &groupName : groupNames) { const CryptoConfigGroup *const group = comp->group(groupName); if (CryptoConfigEntry *const entry = group->entry(QString::fromLatin1(entryName))) { return entry; } } return nullptr; #endif } diff --git a/src/utils/cryptoconfig.cpp b/src/utils/cryptoconfig.cpp index 79104d787..4b190afff 100644 --- a/src/utils/cryptoconfig.cpp +++ b/src/utils/cryptoconfig.cpp @@ -1,111 +1,113 @@ /* utils/cryptoconfig.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "cryptoconfig.h" #include "cryptoconfig_p.h" #include "utils/compat.h" #include #include #include using namespace QGpgME; static std::unordered_map> fakeCryptoConfigIntValues; static std::unordered_map> fakeCryptoConfigStringValues; int Kleo::getCryptoConfigIntValue(const char *componentName, const char *entryName, int defaultValue) { if (!fakeCryptoConfigIntValues.empty()) { const auto componentIt = fakeCryptoConfigIntValues.find(componentName); if (componentIt != std::end(fakeCryptoConfigIntValues)) { const auto entryIt = componentIt->second.find(entryName); if (entryIt != std::end(componentIt->second)) { return entryIt->second; } } } const CryptoConfig *const config = cryptoConfig(); if (!config) { return defaultValue; } const CryptoConfigEntry *const entry = getCryptoConfigEntry(config, componentName, entryName); if (entry && entry->argType() == CryptoConfigEntry::ArgType_Int) { return entry->intValue(); } return defaultValue; } QString Kleo::getCryptoConfigStringValue(const char *componentName, const char *entryName) { if (!fakeCryptoConfigStringValues.empty()) { const auto componentIt = fakeCryptoConfigStringValues.find(componentName); if (componentIt != std::end(fakeCryptoConfigStringValues)) { const auto entryIt = componentIt->second.find(entryName); if (entryIt != std::end(componentIt->second)) { return entryIt->second; } } } const CryptoConfig *const config = cryptoConfig(); if (!config) { return {}; } const CryptoConfigEntry *const entry = getCryptoConfigEntry(config, componentName, entryName); if (entry && entry->argType() == CryptoConfigEntry::ArgType_String) { return entry->stringValue(); } return {}; } QList Kleo::getCryptoConfigUrlList(const char *componentName, const char *entryName) { const CryptoConfig *const config = cryptoConfig(); if (!config) { return {}; } const CryptoConfigEntry *const entry = getCryptoConfigEntry(config, componentName, entryName); if (entry && entry->isList() && (entry->argType() == CryptoConfigEntry::ArgType_LDAPURL || entry->argType() == CryptoConfigEntry::ArgType_Path)) { return entry->urlValueList(); } return {}; } void Kleo::Private::setFakeCryptoConfigIntValue(const std::string &componentName, const std::string &entryName, int fakeValue) { fakeCryptoConfigIntValues[componentName][entryName] = fakeValue; } void Kleo::Private::clearFakeCryptoConfigIntValue(const std::string &componentName, const std::string &entryName) { auto &entryMap = fakeCryptoConfigIntValues[componentName]; entryMap.erase(entryName); if (entryMap.empty()) { fakeCryptoConfigIntValues.erase(componentName); } } void Kleo::Private::setFakeCryptoConfigStringValue(const std::string &componentName, const std::string &entryName, const QString &fakeValue) { fakeCryptoConfigStringValues[componentName][entryName] = fakeValue; } void Kleo::Private::clearFakeCryptoConfigStringValue(const std::string &componentName, const std::string &entryName) { auto &entryMap = fakeCryptoConfigStringValues[componentName]; entryMap.erase(entryName); if (entryMap.empty()) { fakeCryptoConfigStringValues.erase(componentName); } } diff --git a/src/utils/filesystemwatcher.cpp b/src/utils/filesystemwatcher.cpp index 11c36ff57..7102add73 100644 --- a/src/utils/filesystemwatcher.cpp +++ b/src/utils/filesystemwatcher.cpp @@ -1,326 +1,328 @@ /* -*- mode: c++; c-basic-offset:4 -*- filesystemwatcher.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "filesystemwatcher.h" #include "kleo/stl_util.h" #include #include #include #include #include #include #include using namespace Kleo; class FileSystemWatcher::Private { FileSystemWatcher *const q; public: explicit Private(FileSystemWatcher *qq, const QStringList &paths = QStringList()); ~Private() { delete m_watcher; } void onFileChanged(const QString &path); void onDirectoryChanged(const QString &path); void handleTimer(); void onTimeout(); void connectWatcher(); QFileSystemWatcher *m_watcher = nullptr; QTimer m_timer; std::set m_seenPaths; std::set m_cachedDirectories; std::set m_cachedFiles; QStringList m_paths, m_blacklist, m_whitelist; }; FileSystemWatcher::Private::Private(FileSystemWatcher *qq, const QStringList &paths) : q(qq) , m_watcher(nullptr) , m_paths(paths) { m_timer.setSingleShot(true); connect(&m_timer, &QTimer::timeout, q, [this]() { onTimeout(); }); } static bool is_matching(const QString &file, const QStringList &list) { for (const QString &entry : list) { if (QRegExp(entry, Qt::CaseInsensitive, QRegExp::Wildcard).exactMatch(file)) { return true; } } return false; } static bool is_blacklisted(const QString &file, const QStringList &blacklist) { return is_matching(file, blacklist); } static bool is_whitelisted(const QString &file, const QStringList &whitelist) { if (whitelist.empty()) { return true; // special case } return is_matching(file, whitelist); } void FileSystemWatcher::Private::onFileChanged(const QString &path) { const QFileInfo fi(path); if (is_blacklisted(fi.fileName(), m_blacklist)) { return; } if (!is_whitelisted(fi.fileName(), m_whitelist)) { return; } qCDebug(LIBKLEO_LOG) << path; if (fi.exists()) { m_seenPaths.insert(path); } else { m_seenPaths.erase(path); } m_cachedFiles.insert(path); handleTimer(); } static QStringList list_dir_absolute(const QString &path, const QStringList &blacklist, const QStringList &whitelist) { QDir dir(path); QStringList entries = dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot); QStringList::iterator end = std::remove_if(entries.begin(), entries.end(), [&blacklist](const QString &entry) { return is_blacklisted(entry, blacklist); }); if (!whitelist.empty()) { end = std::remove_if(entries.begin(), end, [&whitelist](const QString &entry) { return !is_whitelisted(entry, whitelist); }); } entries.erase(end, entries.end()); std::sort(entries.begin(), entries.end()); std::transform(entries.begin(), entries.end(), entries.begin(), [&dir](const QString &entry) { return dir.absoluteFilePath(entry); }); return entries; } static QStringList find_new_files(const QStringList ¤t, const std::set &seen) { QStringList result; std::set_difference(current.begin(), current.end(), seen.begin(), seen.end(), std::back_inserter(result)); return result; } void FileSystemWatcher::Private::onDirectoryChanged(const QString &path) { const QStringList newFiles = find_new_files(list_dir_absolute(path, m_blacklist, m_whitelist), m_seenPaths); if (newFiles.empty()) { return; } qCDebug(LIBKLEO_LOG) << "newFiles" << newFiles; m_cachedFiles.insert(newFiles.begin(), newFiles.end()); q->addPaths(newFiles); m_cachedDirectories.insert(path); handleTimer(); } void FileSystemWatcher::Private::onTimeout() { std::set dirs; std::set files; dirs.swap(m_cachedDirectories); files.swap(m_cachedFiles); if (dirs.empty() && files.empty()) { return; } Q_EMIT q->triggered(); for (const QString &i : std::as_const(dirs)) { Q_EMIT q->directoryChanged(i); } for (const QString &i : std::as_const(files)) { Q_EMIT q->fileChanged(i); } } void FileSystemWatcher::Private::handleTimer() { if (m_timer.interval() == 0) { onTimeout(); return; } m_timer.start(); } void FileSystemWatcher::Private::connectWatcher() { if (!m_watcher) { return; } connect(m_watcher, &QFileSystemWatcher::directoryChanged, q, [this](const QString &str) { onDirectoryChanged(str); }); connect(m_watcher, &QFileSystemWatcher::fileChanged, q, [this](const QString &str) { onFileChanged(str); }); } FileSystemWatcher::FileSystemWatcher(QObject *p) : QObject(p) , d(new Private(this)) { setEnabled(true); } FileSystemWatcher::FileSystemWatcher(const QStringList &paths, QObject *p) : QObject(p) , d(new Private(this, paths)) { setEnabled(true); } void FileSystemWatcher::setEnabled(bool enable) { if (isEnabled() == enable) { return; } if (enable) { Q_ASSERT(!d->m_watcher); d->m_watcher = new QFileSystemWatcher; if (!d->m_paths.empty()) { d->m_watcher->addPaths(d->m_paths); } d->connectWatcher(); } else { Q_ASSERT(d->m_watcher); delete d->m_watcher; d->m_watcher = nullptr; } } bool FileSystemWatcher::isEnabled() const { return d->m_watcher != nullptr; } FileSystemWatcher::~FileSystemWatcher() { } void FileSystemWatcher::setDelay(int ms) { Q_ASSERT(ms >= 0); d->m_timer.setInterval(ms); } int FileSystemWatcher::delay() const { return d->m_timer.interval(); } void FileSystemWatcher::blacklistFiles(const QStringList &paths) { d->m_blacklist += paths; QStringList blacklisted; d->m_paths.erase(kdtools::separate_if(d->m_paths.begin(), d->m_paths.end(), std::back_inserter(blacklisted), d->m_paths.begin(), [this](const QString &path) { return is_blacklisted(path, d->m_blacklist); }) .second, d->m_paths.end()); if (d->m_watcher && !blacklisted.empty()) { d->m_watcher->removePaths(blacklisted); } } void FileSystemWatcher::whitelistFiles(const QStringList &patterns) { d->m_whitelist += patterns; // ### would be nice to add newly-matching paths here right away, // ### but it's not as simple as blacklisting above, esp. since we // ### don't want to subject addPath()'ed paths to whitelisting. } static QStringList resolve(const QStringList &paths, const QStringList &blacklist, const QStringList &whitelist) { if (paths.empty()) { return QStringList(); } QStringList result; for (const QString &path : paths) { if (QDir(path).exists()) { result += list_dir_absolute(path, blacklist, whitelist); } } return result + resolve(result, blacklist, whitelist); } void FileSystemWatcher::addPaths(const QStringList &paths) { if (paths.empty()) { return; } const QStringList newPaths = paths + resolve(paths, d->m_blacklist, d->m_whitelist); if (!newPaths.empty()) { qCDebug(LIBKLEO_LOG) << "adding\n " << newPaths.join(QLatin1String("\n ")) << "\n/end"; } d->m_paths += newPaths; d->m_seenPaths.insert(newPaths.begin(), newPaths.end()); if (d->m_watcher && !newPaths.empty()) { d->m_watcher->addPaths(newPaths); } } void FileSystemWatcher::addPath(const QString &path) { addPaths(QStringList(path)); } void FileSystemWatcher::removePaths(const QStringList &paths) { if (paths.empty()) { return; } for (const QString &i : paths) { d->m_paths.removeAll(i); } if (d->m_watcher) { d->m_watcher->removePaths(paths); } } void FileSystemWatcher::removePath(const QString &path) { removePaths(QStringList(path)); } #include "moc_filesystemwatcher.cpp" diff --git a/src/utils/gnupg.cpp b/src/utils/gnupg.cpp index 060397164..d1597afb6 100644 --- a/src/utils/gnupg.cpp +++ b/src/utils/gnupg.cpp @@ -1,684 +1,686 @@ /* -*- 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 "utils/assuan.h" #include "utils/compat.h" #include "utils/cryptoconfig.h" #include "utils/hex.h" #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 #include using namespace GpgME; QString Kleo::gnupgHomeDirectory() { static const QString homeDir = QString::fromUtf8(GpgME::dirInfo("homedir")); return homeDir; } 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(), gnupgHome.filePath(QStringLiteral("private-keys-v1.d")), }; } 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; 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); QString versVersion; QString versDescription; QString versDescLong; // Open the file first to avoid a verify and then read issue where // "auditors" might say its an issue,... if (!versionFile.open(QIODevice::ReadOnly)) { // No need to translate this should only be the case in development // builds. return; } else { // Expect a three line format of three HTML strings. versVersion = QString::fromUtf8(versionFile.readLine()).trimmed(); versDescription = QString::fromUtf8(versionFile.readLine()).trimmed(); versDescLong = 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; 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; } }; } // 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::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 getCryptoConfigStringValue("gpg", "compliance") == QLatin1String{"de-vs"}; } bool Kleo::gnupgIsDeVsCompliant() { if (!gnupgUsesDeVsCompliance()) { return false; } // The pseudo option compliance_de_vs was fully added in 2.2.34; // For versions between 2.2.28 and 2.2.33 there was a broken config // value with a wrong type. So for them we add an extra check. This // can be removed in future versions because. For GnuPG we could assume // non-compliance for older versions as versions of Kleopatra for // which this matters are bundled with new enough versions of GnuPG anyway if (engineIsVersion(2, 2, 28) && !engineIsVersion(2, 2, 34)) { return true; } return getCryptoConfigIntValue("gpg", "compliance_de_vs", 0) != 0; } enum GpgME::UserID::Validity Kleo::keyValidity(const GpgME::Key &key) { enum UserID::Validity validity = UserID::Validity::Unknown; for (const auto &uid : key.userIDs()) { if (validity == UserID::Validity::Unknown || validity > uid.validity()) { validity = uid.validity(); } } return validity; } #ifdef Q_OS_WIN static QString fromEncoding(unsigned int src_encoding, const char *data) { int n = MultiByteToWideChar(src_encoding, 0, data, -1, NULL, 0); if (n < 0) { 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); return QString(); } const auto ret = QString::fromWCharArray(result, n); free(result); return ret; } #endif QString Kleo::stringFromGpgOutput(const QByteArray &ba) { #ifdef Q_OS_WIN /* 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(); } if (!cpno) { cpno = GetACP(); } if (!cpno) { qCDebug(LIBKLEO_LOG) << "Failed to find native codepage"; return QString(); } return fromEncoding(cpno, ba.constData()); #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")}); } diff --git a/src/utils/hex.cpp b/src/utils/hex.cpp index 1a96bfa02..6a2734c38 100644 --- a/src/utils/hex.cpp +++ b/src/utils/hex.cpp @@ -1,130 +1,132 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/hex.cpp This file is part of libkleopatra SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "hex.h" #include "kleo/kleoexception.h" #include #include #include using namespace Kleo; static unsigned char unhex(unsigned char ch) { if (ch >= '0' && ch <= '9') { return ch - '0'; } if (ch >= 'A' && ch <= 'F') { return ch - 'A' + 10; } if (ch >= 'a' && ch <= 'f') { return ch - 'a' + 10; } const char cch = ch; throw Kleo::Exception(gpg_error(GPG_ERR_ASS_SYNTAX), i18n("Invalid hex char '%1' in input stream.", QString::fromLatin1(&cch, 1))); } std::string Kleo::hexdecode(const std::string &in) { std::string result; result.reserve(in.size()); for (std::string::const_iterator it = in.begin(), end = in.end(); it != end; ++it) { if (*it == '%') { ++it; unsigned char ch = '\0'; if (it == end) { throw Exception(gpg_error(GPG_ERR_ASS_SYNTAX), i18n("Premature end of hex-encoded char in input stream")); } ch |= unhex(*it) << 4; ++it; if (it == end) { throw Exception(gpg_error(GPG_ERR_ASS_SYNTAX), i18n("Premature end of hex-encoded char in input stream")); } ch |= unhex(*it); result.push_back(ch); } else if (*it == '+') { result += ' '; } else { result.push_back(*it); } } return result; } std::string Kleo::hexencode(const std::string &in) { std::string result; result.reserve(3 * in.size()); static const char hex[] = "0123456789ABCDEF"; for (std::string::const_iterator it = in.begin(), end = in.end(); it != end; ++it) { switch (const unsigned char ch = *it) { default: if ((ch >= '!' && ch <= '~') || ch > 0xA0) { result += ch; break; } // else fall through case ' ': result += '+'; break; case '"': case '#': case '$': case '%': case '\'': case '+': case '=': result += '%'; result += hex[(ch & 0xF0) >> 4]; result += hex[(ch & 0x0F)]; break; } } return result; } std::string Kleo::hexdecode(const char *in) { if (!in) { return std::string(); } return hexdecode(std::string(in)); } std::string Kleo::hexencode(const char *in) { if (!in) { return std::string(); } return hexencode(std::string(in)); } QByteArray Kleo::hexdecode(const QByteArray &in) { if (in.isNull()) { return QByteArray(); } const std::string result = hexdecode(std::string(in.constData())); return QByteArray(result.data(), result.size()); } QByteArray Kleo::hexencode(const QByteArray &in) { if (in.isNull()) { return QByteArray(); } const std::string result = hexencode(std::string(in.constData())); return QByteArray(result.data(), result.size()); } diff --git a/src/utils/keyhelpers.cpp b/src/utils/keyhelpers.cpp index 199286217..043b6d3b0 100644 --- a/src/utils/keyhelpers.cpp +++ b/src/utils/keyhelpers.cpp @@ -1,60 +1,62 @@ /* utils/keyhelpers.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "keyhelpers.h" #include "models/keycache.h" #include using namespace Kleo; namespace { bool havePublicKeyForSignature(const GpgME::UserID::Signature &signature) { // GnuPG returns status "NoPublicKey" for missing signing keys, but also // for expired or revoked signing keys. return (signature.status() != GpgME::UserID::Signature::NoPublicKey) // || !KeyCache::instance()->findByKeyIDOrFingerprint(signature.signerKeyID()).isNull(); } auto _getMissingSignerKeyIds(const std::vector &signatures) { return std::accumulate(std::begin(signatures), std::end(signatures), std::set{}, [](auto &keyIds, const auto &signature) { if (!havePublicKeyForSignature(signature)) { keyIds.insert(QLatin1String{signature.signerKeyID()}); } return keyIds; }); } } std::set Kleo::getMissingSignerKeyIds(const std::vector &userIds) { return std::accumulate(std::begin(userIds), std::end(userIds), std::set{}, [](auto &keyIds, const auto &userID) { if (!userID.isBad()) { const auto newKeyIds = _getMissingSignerKeyIds(userID.signatures()); std::copy(std::begin(newKeyIds), std::end(newKeyIds), std::inserter(keyIds, std::end(keyIds))); } return keyIds; }); } std::set Kleo::getMissingSignerKeyIds(const std::vector &keys) { return std::accumulate(std::begin(keys), std::end(keys), std::set{}, [](auto &keyIds, const auto &key) { if (!key.isBad()) { const auto newKeyIds = getMissingSignerKeyIds(key.userIDs()); std::copy(std::begin(newKeyIds), std::end(newKeyIds), std::inserter(keyIds, std::end(keyIds))); } return keyIds; }); } diff --git a/src/utils/test.cpp b/src/utils/test.cpp index cd6aeb3f8..527011855 100644 --- a/src/utils/test.cpp +++ b/src/utils/test.cpp @@ -1,41 +1,43 @@ /* utils/test.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ +#include + #include "test.h" #include "cryptoconfig_p.h" #include using namespace Kleo::Tests; FakeCryptoConfigIntValue::FakeCryptoConfigIntValue(const char *componentName, const char *entryName, int fakeValue) : mComponentName(componentName) , mEntryName(entryName) { Kleo::Private::setFakeCryptoConfigIntValue(mComponentName, mEntryName, fakeValue); } FakeCryptoConfigIntValue::~FakeCryptoConfigIntValue() { Kleo::Private::clearFakeCryptoConfigIntValue(mComponentName, mEntryName); } FakeCryptoConfigStringValue::FakeCryptoConfigStringValue(const char *componentName, const char *entryName, const QString &fakeValue) : mComponentName(componentName) , mEntryName(entryName) { Kleo::Private::setFakeCryptoConfigStringValue(mComponentName, mEntryName, fakeValue); } FakeCryptoConfigStringValue::~FakeCryptoConfigStringValue() { Kleo::Private::clearFakeCryptoConfigStringValue(mComponentName, mEntryName); }