diff --git a/src/kleo/keyresolver.cpp b/src/kleo/keyresolver.cpp index 4ef8af941..357a305af 100644 --- a/src/kleo/keyresolver.cpp +++ b/src/kleo/keyresolver.cpp @@ -1,219 +1,245 @@ /* -*- 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 #include #include #include #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) + , mCore(std::make_unique(enc, sig, fmt)) , mFormat(fmt) , mEncrypt(enc) , mSign(sig) , mAllowMixed(allowMixed) , mCache(KeyCache::instance()) , mDialogWindowFlags(Qt::WindowFlags()) , mPreferredProtocol(UnknownProtocol) { - mCore.setAllowMixedProtocols(allowMixed); + Q_ASSERT(mCore); + mCore->setAllowMixedProtocols(allowMixed); + } + + Private(KeyResolver *qq, std::unique_ptr core, bool allowMixed) + : q(qq) + , mCore(std::move(core)) + , mFormat(mCore->format()) + , mEncrypt(mCore->encrypt()) + , mSign(mCore->sign()) + , mAllowMixed(allowMixed) + , mCache(KeyCache::instance()) + , mDialogWindowFlags(Qt::WindowFlags()) + , mPreferredProtocol(UnknownProtocol) + { + Q_ASSERT(mCore); + mCore->setAllowMixedProtocols(allowMixed); } ~Private() = default; KeyResolver::Solution expandUnresolvedGroups(KeyResolver::Solution solution); void showApprovalDialog(KeyResolverCore::Result result, QWidget *parent); void dialogAccepted(); KeyResolver *const q; - KeyResolverCore mCore; + std::unique_ptr const 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; }; static bool lessThan(const Key &leftKey, const Key &rightKey) { // shouldn't happen, but still put null keys at the end if (leftKey.isNull()) { return false; } if (rightKey.isNull()) { return true; } // first sort by the displayed name and/or email address const auto leftNameAndOrEmail = Formatting::nameAndEmailForSummaryLine(leftKey); const auto rightNameAndOrEmail = Formatting::nameAndEmailForSummaryLine(rightKey); const int cmp = QString::localeAwareCompare(leftNameAndOrEmail, rightNameAndOrEmail); if (cmp) { return cmp < 0; } // sort certificates with identical name/email address by their fingerprints return strcmp(leftKey.primaryFingerprint(), rightKey.primaryFingerprint()) < 0; } KeyResolver::Solution KeyResolver::Private::expandUnresolvedGroups(KeyResolver::Solution solution) { for (auto it = solution.encryptionKeys.begin(); it != solution.encryptionKeys.end(); ++it) { const auto &address = it.key(); if (!it.value().empty()) { continue; } const auto keyMatchingAddress = mCache->findBestByMailBox(address.toUtf8().constData(), solution.protocol, KeyCache::KeyUsage::Encrypt); if (!keyMatchingAddress.isNull()) { continue; } const auto groupMatchingAddress = mCache->findGroup(address, solution.protocol, KeyCache::KeyUsage::Encrypt); if (!groupMatchingAddress.isNull()) { qCDebug(LIBKLEO_LOG) << __func__ << "Expanding unresolved" << address << "with matching group"; const auto &groupKeys = groupMatchingAddress.keys(); std::vector keys; keys.reserve(groupKeys.size()); std::copy(groupKeys.begin(), groupKeys.end(), std::back_inserter(keys)); std::sort(keys.begin(), keys.end(), lessThan); it.value() = keys; } } return solution; } void KeyResolver::Private::showApprovalDialog(KeyResolverCore::Result result, QWidget *parent) { const auto preferredSolution = expandUnresolvedGroups(std::move(result.solution)); const auto alternativeSolution = expandUnresolvedGroups(std::move(result.alternative)); - const QString sender = mCore.normalizedSender(); + const QString sender = mCore->normalizedSender(); mDialog = std::make_unique(mEncrypt, mSign, sender, std::move(preferredSolution), std::move(alternativeSolution), 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 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)) { } +KeyResolver::KeyResolver(std::unique_ptr keyResolverCore, bool allowMixed) + : d(new Private(this, std::move(keyResolverCore), allowMixed)) +{ +} + Kleo::KeyResolver::~KeyResolver() = default; void KeyResolver::setRecipients(const QStringList &addresses) { - d->mCore.setRecipients(addresses); + d->mCore->setRecipients(addresses); } void KeyResolver::setSender(const QString &address) { - d->mCore.setSender(address); + d->mCore->setSender(address); +} + +QString KeyResolver::normalizedSender() const +{ + return d->mCore->normalizedSender(); } void KeyResolver::setOverrideKeys(const QMap> &overrides) { - d->mCore.setOverrideKeys(overrides); + d->mCore->setOverrideKeys(overrides); } void KeyResolver::setSigningKeys(const QStringList &fingerprints) { - d->mCore.setSigningKeys(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); + d->mCore->setPreferredProtocol(proto); } void KeyResolver::setMinimumValidity(int validity) { - d->mCore.setMinimumValidity(validity); + d->mCore->setMinimumValidity(validity); } #include "moc_keyresolver.cpp" diff --git a/src/kleo/keyresolver.h b/src/kleo/keyresolver.h index ae76eaf0d..191dfc058 100644 --- a/src/kleo/keyresolver.h +++ b/src/kleo/keyresolver.h @@ -1,197 +1,211 @@ /* -*- c++ -*- keyresolver.h 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 */ #pragma once #include "kleo_export.h" #include #include #include #include #include #include #include namespace GpgME { class Key; } namespace Kleo { +class KeyResolverCore; /** * Class to find Keys for E-Mail signing and encryption. * * The KeyResolver uses the Keycache to find keys for signing * or encryption. * * Overrides can be provided for address book integration. * * If no override key(s) are provided for an address and no * KeyGroup for this address is found, then the key * with a uid that matches the address and has the highest * validity is used. If both keys have the same validity, * then the key with the newest subkey is used. * * The KeyResolver also supports groups so the number of * encryption keys does not necessarily * need to match the amount of sender addresses. For this reason * maps are used to map addresses to lists of keys. * * The keys can be OpenPGP keys and S/MIME (CMS) keys. * As a caller you need to partition the keys by their protocol and * send one message for each protocol for the recipients and signed * by the signing keys. */ class KLEO_EXPORT KeyResolver : public QObject { Q_OBJECT public: /** * Solution represents the solution found by the KeyResolver. */ struct Solution { /** * This property holds a hint at the protocol of the signing and encryption * keys, i.e. if @p protocol is either @c GpgME::OpenPGP or @c GpgME::CMS, * then all keys have the corresponding protocol. Otherwise, the keys have * mixed protocols. */ GpgME::Protocol protocol = GpgME::UnknownProtocol; /** * This property contains the signing keys to use. It contains zero or one * OpenPGP key and zero or one S/MIME key. */ std::vector signingKeys; /** * This property contains the encryption keys to use for the different recipients. * * The list of keys will contain for regular users either one S/MIME key * or one OpenPGP key. For a group address, the list of keys will instead contain * the keys required to encrypt for every member of the group. * * The keys of the map represent the normalized email addresses of the recipients. * * @see Kleo::KeyGroup */ QMap> encryptionKeys; }; /** Creates a new key resolver object. * * @param encrypt: Should encryption keys be selected. * @param sign: Should signing keys be selected. * @param protocol: A specific key protocol (OpenPGP, S/MIME) for selection. Default: Both protocols. * @param allowMixed: Specify if multiple message formats may be resolved. **/ explicit KeyResolver(bool encrypt, bool sign, GpgME::Protocol protocol = GpgME::UnknownProtocol, bool allowMixed = true); + /** + * Creates a new key resolver object using a custom key resolver implementation. + * + * @param keyResolverCore: A custom key resolver implementation + * @param allowMixed: Specify if multiple message formats may be resolved. + **/ + explicit KeyResolver(std::unique_ptr keyResolverCore, bool allowMixed = true); + ~KeyResolver() override; /** * Set the list of recipient addresses. * * @param addresses: A list of (not necessarily normalized) email addresses */ void setRecipients(const QStringList &addresses); /** * Set the sender's address. * * This address is added to the list of recipients (for encryption to self) * and it is used for signing key resolution, if the signing keys are not * explicitly set through setSigningKeys. * * @param sender: The sender of this message. */ void setSender(const QString &sender); + /** + * Get the normalized sender email address + */ + QString normalizedSender() const; + /** * Set up possible override keys for recipients addresses. * The keys for the fingerprints are looked * up and used when found. * * Overrides for @c GpgME::UnknownProtocol are used regardless of the * protocol. Overrides for a specific protocol are only used for this * protocol. Overrides for @c GpgME::UnknownProtocol takes precedence over * overrides for a specific protocol. * * @param overrides: A map of \ -> (\ \) */ void setOverrideKeys(const QMap> &overrides); /** * Set explicit signing keys to use. */ void setSigningKeys(const QStringList &fingerprints); /** * Set the minimum user id validity for autoresolution. * * The default value is marginal * * @param validity int representation of a GpgME::UserID::Validity. */ void setMinimumValidity(int validity); /** * Get the result of the resolution. * * @return the resolved keys for signing and encryption. */ Solution result() const; /** * Starts the key resolving procedure. Emits keysResolved on success or * error. * * @param showApproval: If set to true a dialog listing the keys * will always be shown. * @param parentWidget: Optional, a Widget to use as parent for dialogs. */ void start(bool showApproval, QWidget *parentWidget = nullptr); /** * Set window flags for a possible dialog. */ void setDialogWindowFlags(Qt::WindowFlags flags); /** * Set the protocol that is preferred to be displayed first when * it is not clear from the keys. E.g. if both OpenPGP and S/MIME * can be resolved. */ void setPreferredProtocol(GpgME::Protocol proto); Q_SIGNALS: /** * Emitted when key resolution finished. * * @param success: The general result. If true continue sending, * if false abort. * @param sendUnencrypted: If there could be no key found for one of * the recipients the user was queried if the * mail should be sent out unencrypted. * sendUnencrypted is true if the user agreed * to this.*/ void keysResolved(bool success, bool sendUnencrypted); private: class Private; std::unique_ptr d; }; } // namespace Kleo diff --git a/src/kleo/keyresolvercore.cpp b/src/kleo/keyresolvercore.cpp index 0fc37304e..b5b51d033 100644 --- a/src/kleo/keyresolvercore.cpp +++ b/src/kleo/keyresolvercore.cpp @@ -1,769 +1,784 @@ /* -*- 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 "enum.h" #include "keygroup.h" #include #include #include #include #include #include #include "kleo/debug.h" #include #include using namespace Kleo; using namespace GpgME; namespace { static inline bool ValidEncryptionKey(const Key &key) { if (key.isNull() || key.isRevoked() || key.isExpired() || key.isDisabled() || !Kleo::keyHasEncrypt(key)) { return false; } return true; } static inline bool ValidSigningKey(const Key &key) { if (key.isNull() || key.isRevoked() || key.isExpired() || key.isDisabled() || !Kleo::keyHasSign(key) || !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; } } // 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 (DeVSCompliance::isCompliant() && !DeVSCompliance::keyIsCompliant(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 (DeVSCompliance::isCompliant() && !DeVSCompliance::keyIsCompliant(key)) { qCDebug(LIBKLEO_LOG) << "Rejected enc key" << key.primaryFingerprint() << "because it is not de-vs compliant."; return false; } if (address.isEmpty()) { // group key must satisfy minimum validity for all user IDs return Kleo::minimalValidityOfNotRevokedUserIDs(key) >= mMinimumValidity; } 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 { if (protocolKeysMap[OpenPGP].empty()) { protocolKeysMap[OpenPGP] = resolveSenderWithGroup(mSender, OpenPGP); } if (protocolKeysMap[CMS].empty()) { 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; +bool KeyResolverCore::encrypt() const +{ + return d->mEncrypt; +} + +bool KeyResolverCore::sign() const +{ + return d->mSign; +} + +GpgME::Protocol KeyResolverCore::format() const +{ + return d->mFormat; +} + 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/keyresolvercore.h b/src/kleo/keyresolvercore.h index 9f2321f47..ffedd4a62 100644 --- a/src/kleo/keyresolvercore.h +++ b/src/kleo/keyresolvercore.h @@ -1,85 +1,94 @@ /* -*- c++ -*- kleo/keyresolvercore.h 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 */ #pragma once #include "kleo_export.h" #include #include #include #include #include #include class QString; namespace GpgME { class Key; } namespace Kleo { +/** + * Key resolver + */ class KLEO_EXPORT KeyResolverCore { public: enum SolutionFlags { // clang-format off SomeUnresolved = 0, AllResolved = 1, OpenPGPOnly = 2, CMSOnly = 4, MixedProtocols = OpenPGPOnly | CMSOnly, Error = 0x1000, ResolvedMask = AllResolved | Error, ProtocolsMask = OpenPGPOnly | CMSOnly | Error, // clang-format on }; struct Result { SolutionFlags flags; KeyResolver::Solution solution; KeyResolver::Solution alternative; }; explicit KeyResolverCore(bool encrypt, bool sign, GpgME::Protocol format = GpgME::UnknownProtocol); ~KeyResolverCore(); + bool encrypt() const; + + bool sign() const; + + GpgME::Protocol format() const; + void setSender(const QString &sender); QString normalizedSender() const; void setRecipients(const QStringList &addresses); void setSigningKeys(const QStringList &fingerprints); void setOverrideKeys(const QMap> &overrides); void setAllowMixedProtocols(bool allowMixed); void setPreferredProtocol(GpgME::Protocol proto); void setMinimumValidity(int validity); Result resolve(); private: class Private; std::unique_ptr d; }; } // namespace Kleo