diff --git a/autotests/keyselectioncombotest.cpp b/autotests/keyselectioncombotest.cpp index 2d1775b6f..557486698 100644 --- a/autotests/keyselectioncombotest.cpp +++ b/autotests/keyselectioncombotest.cpp @@ -1,216 +1,216 @@ /* autotests/keyselectioncombotest.cpp This file is part of libkleopatra's test suite. SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include #include #include #include #include #include #include #include using namespace Kleo; namespace { auto mapValidity(GpgME::UserID::Validity validity) { switch (validity) { default: case GpgME::UserID::Unknown: return GPGME_VALIDITY_UNKNOWN; case GpgME::UserID::Undefined: return GPGME_VALIDITY_UNDEFINED; case GpgME::UserID::Never: return GPGME_VALIDITY_NEVER; case GpgME::UserID::Marginal: return GPGME_VALIDITY_MARGINAL; case GpgME::UserID::Full: return GPGME_VALIDITY_FULL; case GpgME::UserID::Ultimate: return GPGME_VALIDITY_ULTIMATE; } } GpgME::Key createTestKey(const char *uid, GpgME::Protocol protocol = GpgME::UnknownProtocol, - KeyUsage usage = KeyUsage::AnyUsage, + KeyCache::KeyUsage usage = KeyCache::KeyUsage::AnyUsage, GpgME::UserID::Validity validity = GpgME::UserID::Full) { static int count = 0; count++; gpgme_key_t key; gpgme_key_from_uid(&key, uid); Q_ASSERT(key); Q_ASSERT(key->uids); if (protocol != GpgME::UnknownProtocol) { key->protocol = protocol == GpgME::OpenPGP ? GPGME_PROTOCOL_OpenPGP : GPGME_PROTOCOL_CMS; } const QByteArray fingerprint = QByteArray::number(count, 16).rightJustified(40, '0'); key->fpr = strdup(fingerprint.constData()); key->revoked = 0; key->expired = 0; key->disabled = 0; - key->can_encrypt = int(usage == KeyUsage::AnyUsage || usage == KeyUsage::Encrypt); - key->can_sign = int(usage == KeyUsage::AnyUsage || usage == KeyUsage::Sign); + key->can_encrypt = int(usage == KeyCache::KeyUsage::AnyUsage || usage == KeyCache::KeyUsage::Encrypt); + key->can_sign = int(usage == KeyCache::KeyUsage::AnyUsage || usage == KeyCache::KeyUsage::Sign); key->secret = 1; key->uids->validity = mapValidity(validity); return GpgME::Key(key, false); } auto testKey(const char *address, GpgME::Protocol protocol = GpgME::UnknownProtocol) { const auto email = GpgME::UserID::addrSpecFromString(address); const auto keys = KeyCache::instance()->findByEMailAddress(email); for (const auto &key : keys) { if (protocol == GpgME::UnknownProtocol || key.protocol() == protocol) { return key; } } return GpgME::Key(); } void waitForKeySelectionComboBeingInitialized(const KeySelectionCombo *combo) { QVERIFY(combo); const auto spy = std::make_unique(combo, &KeySelectionCombo::keyListingFinished); QVERIFY(spy->isValid()); QVERIFY(spy->wait(10)); } } class KeySelectionComboTest : public QObject { Q_OBJECT private Q_SLOTS: void init() { // hold a reference to the key cache to avoid rebuilding while the test is running mKeyCache = KeyCache::instance(); KeyCache::mutableInstance()->setKeys({ - createTestKey("sender@example.net", GpgME::OpenPGP, KeyUsage::AnyUsage), - createTestKey("sender@example.net", GpgME::CMS, KeyUsage::AnyUsage), - createTestKey("Full Trust ", GpgME::OpenPGP, KeyUsage::Encrypt), - createTestKey("Trusted S/MIME ", GpgME::CMS, KeyUsage::Encrypt), - createTestKey("Marginal Validity ", GpgME::OpenPGP, KeyUsage::Encrypt, GpgME::UserID::Marginal), + createTestKey("sender@example.net", GpgME::OpenPGP, KeyCache::KeyCache::KeyUsage::AnyUsage), + createTestKey("sender@example.net", GpgME::CMS, KeyCache::KeyCache::KeyUsage::AnyUsage), + createTestKey("Full Trust ", GpgME::OpenPGP, KeyCache::KeyCache::KeyUsage::Encrypt), + createTestKey("Trusted S/MIME ", GpgME::CMS, KeyCache::KeyCache::KeyUsage::Encrypt), + createTestKey("Marginal Validity ", GpgME::OpenPGP, KeyCache::KeyCache::KeyUsage::Encrypt, GpgME::UserID::Marginal), }); } void cleanup() { // verify that nobody else holds a reference to the key cache QVERIFY(mKeyCache.use_count() == 1); mKeyCache.reset(); } void test__verify_test_keys() { QVERIFY(!testKey("sender@example.net", GpgME::OpenPGP).isNull()); QVERIFY(!testKey("sender@example.net", GpgME::CMS).isNull()); QVERIFY(!testKey("Full Trust ", GpgME::OpenPGP).isNull()); QVERIFY(!testKey("Trusted S/MIME ", GpgME::CMS).isNull()); QVERIFY(!testKey("Marginal Validity ", GpgME::OpenPGP).isNull()); } void test__after_initialization_default_key_is_current_key() { const auto combo = std::make_unique(); combo->setDefaultKey(QString::fromLatin1(testKey("Full Trust ", GpgME::OpenPGP).primaryFingerprint())); waitForKeySelectionComboBeingInitialized(combo.get()); QCOMPARE(combo->currentKey().primaryFingerprint(), testKey("Full Trust ", GpgME::OpenPGP).primaryFingerprint()); } void test__currently_selected_key_is_retained_if_cache_is_updated() { const auto combo = std::make_unique(); combo->setDefaultKey(QString::fromLatin1(testKey("Full Trust ", GpgME::OpenPGP).primaryFingerprint())); waitForKeySelectionComboBeingInitialized(combo.get()); combo->setCurrentIndex(3); QCOMPARE(combo->currentKey().primaryFingerprint(), testKey("Trusted S/MIME ", GpgME::CMS).primaryFingerprint()); Q_EMIT KeyCache::mutableInstance()->keyListingDone(GpgME::KeyListResult{}); QCOMPARE(combo->currentKey().primaryFingerprint(), testKey("Trusted S/MIME ", GpgME::CMS).primaryFingerprint()); } void test__default_key_is_selected_if_currently_selected_key_is_gone_after_model_update() { const auto combo = std::make_unique(); combo->setDefaultKey(QString::fromLatin1(testKey("Full Trust ", GpgME::OpenPGP).primaryFingerprint())); waitForKeySelectionComboBeingInitialized(combo.get()); combo->setCurrentIndex(3); QCOMPARE(combo->currentKey().primaryFingerprint(), testKey("Trusted S/MIME ", GpgME::CMS).primaryFingerprint()); KeyCache::mutableInstance()->setKeys({ testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS), testKey("Full Trust ", GpgME::OpenPGP), testKey("Marginal Validity ", GpgME::OpenPGP), }); QCOMPARE(combo->currentKey().primaryFingerprint(), testKey("Full Trust ", GpgME::OpenPGP).primaryFingerprint()); } void test__currently_selected_custom_item_is_retained_if_cache_is_updated() { const auto combo = std::make_unique(); combo->prependCustomItem({}, {}, QStringLiteral("custom1")); combo->appendCustomItem({}, {}, QStringLiteral("custom2")); combo->setDefaultKey(QString::fromLatin1(testKey("Full Trust ", GpgME::OpenPGP).primaryFingerprint())); waitForKeySelectionComboBeingInitialized(combo.get()); combo->setCurrentIndex(combo->count() - 1); QCOMPARE(combo->currentData(), QStringLiteral("custom2")); Q_EMIT KeyCache::mutableInstance()->keyListingDone(GpgME::KeyListResult{}); QCOMPARE(combo->currentData(), QStringLiteral("custom2")); } void test__default_key_is_selected_if_currently_selected_custom_item_is_gone_after_model_update() { const auto combo = std::make_unique(); combo->prependCustomItem({}, {}, QStringLiteral("custom1")); combo->appendCustomItem({}, {}, QStringLiteral("custom2")); combo->setDefaultKey(QString::fromLatin1(testKey("Full Trust ", GpgME::OpenPGP).primaryFingerprint())); waitForKeySelectionComboBeingInitialized(combo.get()); combo->setCurrentIndex(combo->count() - 1); QCOMPARE(combo->currentData(), QStringLiteral("custom2")); combo->removeCustomItem(QStringLiteral("custom2")); QCOMPARE(combo->currentKey().primaryFingerprint(), testKey("Full Trust ", GpgME::OpenPGP).primaryFingerprint()); } private: std::shared_ptr mKeyCache; }; QTEST_MAIN(KeySelectionComboTest) #include "keyselectioncombotest.moc" diff --git a/autotests/newkeyapprovaldialogtest.cpp b/autotests/newkeyapprovaldialogtest.cpp index 5eb51fc29..669b957e5 100644 --- a/autotests/newkeyapprovaldialogtest.cpp +++ b/autotests/newkeyapprovaldialogtest.cpp @@ -1,1088 +1,1088 @@ /* autotests/newkeyapprovaldialogtest.cpp This file is part of libkleopatra's test suite. SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; namespace QTest { template<> inline char *toString(const bool &t) { return t ? qstrdup("true") : qstrdup("false"); } template<> inline bool qCompare(bool const &t1, bool const &t2, const char *actual, const char *expected, const char *file, int line) { return compare_helper(t1 == t2, "Compared values are not the same", toString(t1), toString(t2), actual, expected, file, line); } template<> inline char *toString(const GpgME::Protocol &t) { return qstrdup(Formatting::displayName(t).toLocal8Bit().constData()); } template<> inline bool qCompare(GpgME::Protocol const &t1, GpgME::Protocol const &t2, const char *actual, const char *expected, const char *file, int line) { return compare_helper(t1 == t2, "Compared values are not the same", toString(t1), toString(t2), actual, expected, file, line); } } namespace { // copied from NewKeyApprovalDialog::Private enum Action { Unset, GenerateKey, IgnoreKey, }; auto mapValidity(GpgME::UserID::Validity validity) { switch (validity) { default: case GpgME::UserID::Unknown: return GPGME_VALIDITY_UNKNOWN; case GpgME::UserID::Undefined: return GPGME_VALIDITY_UNDEFINED; case GpgME::UserID::Never: return GPGME_VALIDITY_NEVER; case GpgME::UserID::Marginal: return GPGME_VALIDITY_MARGINAL; case GpgME::UserID::Full: return GPGME_VALIDITY_FULL; case GpgME::UserID::Ultimate: return GPGME_VALIDITY_ULTIMATE; } } GpgME::Key createTestKey(const char *uid, GpgME::Protocol protocol = GpgME::UnknownProtocol, - KeyUsage usage = KeyUsage::AnyUsage, + KeyCache::KeyUsage usage = KeyCache::KeyUsage::AnyUsage, GpgME::UserID::Validity validity = GpgME::UserID::Full) { static int count = 0; count++; gpgme_key_t key; gpgme_key_from_uid(&key, uid); Q_ASSERT(key); Q_ASSERT(key->uids); if (protocol != GpgME::UnknownProtocol) { key->protocol = protocol == GpgME::OpenPGP ? GPGME_PROTOCOL_OpenPGP : GPGME_PROTOCOL_CMS; } const QByteArray fingerprint = QByteArray::number(count, 16).rightJustified(40, '0'); key->fpr = strdup(fingerprint.constData()); key->revoked = 0; key->expired = 0; key->disabled = 0; - key->can_encrypt = int(usage == KeyUsage::AnyUsage || usage == KeyUsage::Encrypt); - key->can_sign = int(usage == KeyUsage::AnyUsage || usage == KeyUsage::Sign); + key->can_encrypt = int(usage == KeyCache::KeyUsage::AnyUsage || usage == KeyCache::KeyUsage::Encrypt); + key->can_sign = int(usage == KeyCache::KeyUsage::AnyUsage || usage == KeyCache::KeyUsage::Sign); key->secret = 1; key->uids->validity = mapValidity(validity); return GpgME::Key(key, false); } auto testKey(const char *address, GpgME::Protocol protocol = GpgME::UnknownProtocol) { const auto email = GpgME::UserID::addrSpecFromString(address); const auto keys = KeyCache::instance()->findByEMailAddress(email); for (const auto &key : keys) { if (protocol == GpgME::UnknownProtocol || key.protocol() == protocol) { return key; } } return GpgME::Key(); } void waitForKeySelectionCombosBeingInitialized(const QDialog *dialog) { QVERIFY(dialog); auto combo = dialog->findChild(); QVERIFY(combo); const auto spy = std::make_unique(combo, &KeySelectionCombo::keyListingFinished); QVERIFY(spy->isValid()); QVERIFY(spy->wait(10)); } template struct Widgets { std::vector visible; std::vector hidden; }; template Widgets visibleAndHiddenWidgets(const QList &widgets) { Widgets result; std::partition_copy(std::begin(widgets), std::end(widgets), std::back_inserter(result.visible), std::back_inserter(result.hidden), std::mem_fn(&QWidget::isVisible)); return result; } enum Visibility { IsHidden, IsVisible, }; enum CheckedState { IsUnchecked, IsChecked, }; template void verifyProtocolButton(const T *button, Visibility expectedVisibility, CheckedState expectedCheckedState) { QVERIFY(button); QCOMPARE(button->isVisible(), expectedVisibility == IsVisible); QCOMPARE(button->isChecked(), expectedCheckedState == IsChecked); } template void verifyWidgetVisibility(const T *widget, Visibility expectedVisibility) { QVERIFY(widget); QCOMPARE(widget->isVisible(), expectedVisibility == IsVisible); } template void verifyWidgetsVisibility(const QList &widgets, Visibility expectedVisibility) { for (auto w : widgets) { verifyWidgetVisibility(w, expectedVisibility); } } void verifyProtocolLabels(const QList &labels, int expectedNumber, Visibility expectedVisibility) { QCOMPARE(labels.size(), expectedNumber); verifyWidgetsVisibility(labels, expectedVisibility); } bool listsOfKeysAreEqual(const std::vector &l1, const std::vector &l2) { return std::equal(std::begin(l1), std::end(l1), std::begin(l2), std::end(l2), ByFingerprint()); } void verifySolution(const KeyResolver::Solution &actual, const KeyResolver::Solution &expected) { QCOMPARE(actual.protocol, expected.protocol); QVERIFY(listsOfKeysAreEqual(actual.signingKeys, expected.signingKeys)); QVERIFY(std::equal(actual.encryptionKeys.constKeyValueBegin(), actual.encryptionKeys.constKeyValueEnd(), expected.encryptionKeys.constKeyValueBegin(), expected.encryptionKeys.constKeyValueEnd(), [](const auto &kv1, const auto &kv2) { return kv1.first == kv2.first && listsOfKeysAreEqual(kv1.second, kv2.second); })); } void switchKeySelectionCombosFromGenerateKeyToIgnoreKey(const QList &combos) { for (auto combo : combos) { if (combo->currentData(Qt::UserRole).toInt() == GenerateKey) { const auto ignoreIndex = combo->findData(IgnoreKey); QVERIFY(ignoreIndex != -1); combo->setCurrentIndex(ignoreIndex); } } } } class NewKeyApprovalDialogTest : public QObject { Q_OBJECT private Q_SLOTS: void init() { // hold a reference to the key cache to avoid rebuilding while the test is running mKeyCache = KeyCache::instance(); KeyCache::mutableInstance()->setKeys({ - createTestKey("sender@example.net", GpgME::OpenPGP, KeyUsage::AnyUsage), - createTestKey("sender@example.net", GpgME::CMS, KeyUsage::AnyUsage), - createTestKey("Full Trust ", GpgME::OpenPGP, KeyUsage::Encrypt), - createTestKey("Trusted S/MIME ", GpgME::CMS, KeyUsage::Encrypt), - createTestKey("Marginal Validity ", GpgME::OpenPGP, KeyUsage::Encrypt, GpgME::UserID::Marginal), + createTestKey("sender@example.net", GpgME::OpenPGP, KeyCache::KeyCache::KeyUsage::AnyUsage), + createTestKey("sender@example.net", GpgME::CMS, KeyCache::KeyCache::KeyUsage::AnyUsage), + createTestKey("Full Trust ", GpgME::OpenPGP, KeyCache::KeyCache::KeyUsage::Encrypt), + createTestKey("Trusted S/MIME ", GpgME::CMS, KeyCache::KeyCache::KeyUsage::Encrypt), + createTestKey("Marginal Validity ", GpgME::OpenPGP, KeyCache::KeyCache::KeyUsage::Encrypt, GpgME::UserID::Marginal), }); } void cleanup() { // verify that nobody else holds a reference to the key cache QVERIFY(mKeyCache.use_count() == 1); mKeyCache.reset(); } void test__verify_test_keys() { QVERIFY(!testKey("sender@example.net", GpgME::OpenPGP).isNull()); QVERIFY(!testKey("sender@example.net", GpgME::CMS).isNull()); QVERIFY(!testKey("Full Trust ", GpgME::OpenPGP).isNull()); QVERIFY(!testKey("Trusted S/MIME ", GpgME::CMS).isNull()); QVERIFY(!testKey("Marginal Validity ", GpgME::OpenPGP).isNull()); } void test__both_protocols_allowed__mixed_not_allowed__openpgp_preferred() { const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; const bool allowMixed = false; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::OpenPGP, {testKey("sender@example.net", GpgME::OpenPGP)}, { {QStringLiteral("prefer-openpgp@example.net"), {testKey("Full Trust ", GpgME::OpenPGP)}}, {QStringLiteral("prefer-smime@example.net"), {}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::OpenPGP)}}, }, }; const KeyResolver::Solution alternativeSolution = { GpgME::CMS, {testKey("sender@example.net", GpgME::CMS)}, { {QStringLiteral("prefer-openpgp@example.net"), {}}, {QStringLiteral("prefer-smime@example.net"), {testKey("Trusted S/MIME ", GpgME::CMS)}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::CMS)}}, }, }; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); verifyProtocolButton(dialog->findChild(QStringLiteral("openpgp button")), IsVisible, IsChecked); verifyProtocolButton(dialog->findChild(QStringLiteral("smime button")), IsVisible, IsUnchecked); const auto signingKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("signing key"))); QCOMPARE(signingKeyWidgets.visible.size(), 1); QCOMPARE(signingKeyWidgets.hidden.size(), 1); QCOMPARE(signingKeyWidgets.visible[0]->defaultKey(GpgME::OpenPGP), preferredSolution.signingKeys[0].primaryFingerprint()); QCOMPARE(signingKeyWidgets.hidden[0]->defaultKey(GpgME::CMS), alternativeSolution.signingKeys[0].primaryFingerprint()); const auto encryptionKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("encryption key"))); QCOMPARE(encryptionKeyWidgets.visible.size(), 3); QCOMPARE(encryptionKeyWidgets.hidden.size(), 3); // encryption key widgets for sender come first (visible for OpenPGP, hidden for S/MIME) QCOMPARE(encryptionKeyWidgets.visible[0]->property("address").toString(), sender); QCOMPARE(encryptionKeyWidgets.visible[0]->defaultKey(GpgME::OpenPGP), preferredSolution.encryptionKeys.value(sender)[0].primaryFingerprint()); QCOMPARE(encryptionKeyWidgets.hidden[0]->property("address").toString(), sender); QCOMPARE(encryptionKeyWidgets.hidden[0]->defaultKey(GpgME::CMS), alternativeSolution.encryptionKeys.value(sender)[0].primaryFingerprint()); // encryption key widgets for other recipients follow (visible for OpenPGP, hidden for S/MIME) QCOMPARE(encryptionKeyWidgets.visible[1]->property("address").toString(), "prefer-openpgp@example.net"); QCOMPARE(encryptionKeyWidgets.visible[1]->defaultKey(GpgME::OpenPGP), preferredSolution.encryptionKeys.value("prefer-openpgp@example.net")[0].primaryFingerprint()); QCOMPARE(encryptionKeyWidgets.hidden[1]->property("address").toString(), "prefer-openpgp@example.net"); QVERIFY(encryptionKeyWidgets.hidden[1]->defaultKey(GpgME::CMS).isEmpty()); QCOMPARE(encryptionKeyWidgets.visible[2]->property("address").toString(), "prefer-smime@example.net"); QVERIFY(encryptionKeyWidgets.visible[2]->defaultKey(GpgME::OpenPGP).isEmpty()); QCOMPARE(encryptionKeyWidgets.hidden[2]->property("address").toString(), "prefer-smime@example.net"); QCOMPARE(encryptionKeyWidgets.hidden[2]->defaultKey(GpgME::CMS), alternativeSolution.encryptionKeys.value("prefer-smime@example.net")[0].primaryFingerprint()); } void test__both_protocols_allowed__mixed_not_allowed__smime_preferred() { const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; const bool allowMixed = false; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::CMS, {testKey("sender@example.net", GpgME::CMS)}, { {QStringLiteral("prefer-openpgp@example.net"), {}}, {QStringLiteral("prefer-smime@example.net"), {testKey("Trusted S/MIME ", GpgME::CMS)}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::CMS)}}, }, }; const KeyResolver::Solution alternativeSolution = { GpgME::OpenPGP, {testKey("sender@example.net", GpgME::OpenPGP)}, { {QStringLiteral("prefer-openpgp@example.net"), {testKey("Full Trust ", GpgME::OpenPGP)}}, {QStringLiteral("prefer-smime@example.net"), {}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::OpenPGP)}}, }, }; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); verifyProtocolButton(dialog->findChild(QStringLiteral("openpgp button")), IsVisible, IsUnchecked); verifyProtocolButton(dialog->findChild(QStringLiteral("smime button")), IsVisible, IsChecked); const auto signingKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("signing key"))); QCOMPARE(signingKeyWidgets.visible.size(), 1); QCOMPARE(signingKeyWidgets.hidden.size(), 1); QCOMPARE(signingKeyWidgets.visible[0]->defaultKey(GpgME::CMS), preferredSolution.signingKeys[0].primaryFingerprint()); QCOMPARE(signingKeyWidgets.hidden[0]->defaultKey(GpgME::OpenPGP), alternativeSolution.signingKeys[0].primaryFingerprint()); const auto encryptionKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("encryption key"))); QCOMPARE(encryptionKeyWidgets.visible.size(), 3); QCOMPARE(encryptionKeyWidgets.hidden.size(), 3); // encryption key widgets for sender come first (visible for S/MIME, hidden for OpenPGP) QCOMPARE(encryptionKeyWidgets.visible[0]->property("address").toString(), sender); QCOMPARE(encryptionKeyWidgets.visible[0]->defaultKey(GpgME::CMS), preferredSolution.encryptionKeys.value(sender)[0].primaryFingerprint()); QCOMPARE(encryptionKeyWidgets.hidden[0]->property("address").toString(), sender); QCOMPARE(encryptionKeyWidgets.hidden[0]->defaultKey(GpgME::OpenPGP), alternativeSolution.encryptionKeys.value(sender)[0].primaryFingerprint()); // encryption key widgets for other recipients follow (visible for OpenPGP, hidden for S/MIME) QCOMPARE(encryptionKeyWidgets.visible[1]->property("address").toString(), "prefer-openpgp@example.net"); QVERIFY(encryptionKeyWidgets.visible[1]->defaultKey(GpgME::CMS).isEmpty()); QCOMPARE(encryptionKeyWidgets.hidden[1]->property("address").toString(), "prefer-openpgp@example.net"); QCOMPARE(encryptionKeyWidgets.hidden[1]->defaultKey(GpgME::OpenPGP), alternativeSolution.encryptionKeys.value("prefer-openpgp@example.net")[0].primaryFingerprint()); QCOMPARE(encryptionKeyWidgets.visible[2]->property("address").toString(), "prefer-smime@example.net"); QCOMPARE(encryptionKeyWidgets.visible[2]->defaultKey(GpgME::CMS), preferredSolution.encryptionKeys.value("prefer-smime@example.net")[0].primaryFingerprint()); QCOMPARE(encryptionKeyWidgets.hidden[2]->property("address").toString(), "prefer-smime@example.net"); QVERIFY(encryptionKeyWidgets.hidden[2]->defaultKey(GpgME::OpenPGP).isEmpty()); } void test__openpgp_only() { const GpgME::Protocol forcedProtocol = GpgME::OpenPGP; const bool allowMixed = false; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::OpenPGP, {testKey("sender@example.net", GpgME::OpenPGP)}, { {QStringLiteral("prefer-openpgp@example.net"), {testKey("Full Trust ", GpgME::OpenPGP)}}, {QStringLiteral("prefer-smime@example.net"), {}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::OpenPGP)}}, }, }; const KeyResolver::Solution alternativeSolution = {}; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); verifyProtocolButton(dialog->findChild(QStringLiteral("openpgp button")), IsHidden, IsChecked); verifyProtocolButton(dialog->findChild(QStringLiteral("smime button")), IsHidden, IsUnchecked); const auto signingKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("signing key"))); QCOMPARE(signingKeyWidgets.visible.size(), 1); QCOMPARE(signingKeyWidgets.hidden.size(), 0); QCOMPARE(signingKeyWidgets.visible[0]->defaultKey(GpgME::OpenPGP), preferredSolution.signingKeys[0].primaryFingerprint()); const auto encryptionKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("encryption key"))); QCOMPARE(encryptionKeyWidgets.visible.size(), 3); QCOMPARE(encryptionKeyWidgets.hidden.size(), 0); // encryption key widget for sender comes first QCOMPARE(encryptionKeyWidgets.visible[0]->property("address").toString(), sender); QCOMPARE(encryptionKeyWidgets.visible[0]->defaultKey(GpgME::OpenPGP), preferredSolution.encryptionKeys.value(sender)[0].primaryFingerprint()); // encryption key widgets for other recipients follow QCOMPARE(encryptionKeyWidgets.visible[1]->property("address").toString(), "prefer-openpgp@example.net"); QCOMPARE(encryptionKeyWidgets.visible[1]->defaultKey(GpgME::OpenPGP), preferredSolution.encryptionKeys.value("prefer-openpgp@example.net")[0].primaryFingerprint()); QCOMPARE(encryptionKeyWidgets.visible[2]->property("address").toString(), "prefer-smime@example.net"); QVERIFY(encryptionKeyWidgets.visible[2]->defaultKey(GpgME::OpenPGP).isEmpty()); } void test__smime_only() { const GpgME::Protocol forcedProtocol = GpgME::CMS; const bool allowMixed = false; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::CMS, {testKey("sender@example.net", GpgME::CMS)}, { {QStringLiteral("prefer-openpgp@example.net"), {}}, {QStringLiteral("prefer-smime@example.net"), {testKey("Trusted S/MIME ", GpgME::CMS)}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::CMS)}}, }, }; const KeyResolver::Solution alternativeSolution = {}; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); verifyProtocolButton(dialog->findChild(QStringLiteral("openpgp button")), IsHidden, IsUnchecked); verifyProtocolButton(dialog->findChild(QStringLiteral("smime button")), IsHidden, IsChecked); const auto signingKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("signing key"))); QCOMPARE(signingKeyWidgets.visible.size(), 1); QCOMPARE(signingKeyWidgets.hidden.size(), 0); QCOMPARE(signingKeyWidgets.visible[0]->defaultKey(GpgME::CMS), preferredSolution.signingKeys[0].primaryFingerprint()); const auto encryptionKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("encryption key"))); QCOMPARE(encryptionKeyWidgets.visible.size(), 3); QCOMPARE(encryptionKeyWidgets.hidden.size(), 0); // encryption key widget for sender comes first QCOMPARE(encryptionKeyWidgets.visible[0]->property("address").toString(), sender); QCOMPARE(encryptionKeyWidgets.visible[0]->defaultKey(GpgME::CMS), preferredSolution.encryptionKeys.value(sender)[0].primaryFingerprint()); // encryption key widgets for other recipients follow QCOMPARE(encryptionKeyWidgets.visible[1]->property("address").toString(), "prefer-openpgp@example.net"); QVERIFY(encryptionKeyWidgets.visible[1]->defaultKey(GpgME::CMS).isEmpty()); QCOMPARE(encryptionKeyWidgets.visible[2]->property("address").toString(), "prefer-smime@example.net"); QCOMPARE(encryptionKeyWidgets.visible[2]->defaultKey(GpgME::CMS), preferredSolution.encryptionKeys.value("prefer-smime@example.net")[0].primaryFingerprint()); } void test__both_protocols_allowed__mixed_allowed() { const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; const bool allowMixed = true; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::UnknownProtocol, {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}, { {QStringLiteral("prefer-openpgp@example.net"), {testKey("Full Trust ", GpgME::OpenPGP)}}, {QStringLiteral("prefer-smime@example.net"), {testKey("Trusted S/MIME ", GpgME::CMS)}}, {QStringLiteral("unknown@example.net"), {}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}}, }, }; const KeyResolver::Solution alternativeSolution = {}; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); verifyProtocolButton(dialog->findChild(QStringLiteral("openpgp button")), IsVisible, IsChecked); verifyProtocolButton(dialog->findChild(QStringLiteral("smime button")), IsVisible, IsChecked); verifyProtocolLabels(dialog->findChildren(QStringLiteral("protocol label")), 4, IsVisible); const auto signingKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("signing key"))); QCOMPARE(signingKeyWidgets.visible.size(), 2); QCOMPARE(signingKeyWidgets.hidden.size(), 0); QCOMPARE(signingKeyWidgets.visible[0]->defaultKey(GpgME::OpenPGP), preferredSolution.signingKeys[0].primaryFingerprint()); QCOMPARE(signingKeyWidgets.visible[1]->defaultKey(GpgME::CMS), preferredSolution.signingKeys[1].primaryFingerprint()); const auto encryptionKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("encryption key"))); QCOMPARE(encryptionKeyWidgets.visible.size(), 5); QCOMPARE(encryptionKeyWidgets.hidden.size(), 0); // encryption key widgets for sender come first QCOMPARE(encryptionKeyWidgets.visible[0]->property("address").toString(), sender); QCOMPARE(encryptionKeyWidgets.visible[0]->defaultKey(GpgME::OpenPGP), preferredSolution.encryptionKeys.value(sender)[0].primaryFingerprint()); QCOMPARE(encryptionKeyWidgets.visible[1]->property("address").toString(), sender); QCOMPARE(encryptionKeyWidgets.visible[1]->defaultKey(GpgME::CMS), preferredSolution.encryptionKeys.value(sender)[1].primaryFingerprint()); // encryption key widgets for other recipients follow QCOMPARE(encryptionKeyWidgets.visible[2]->property("address").toString(), "prefer-openpgp@example.net"); QCOMPARE(encryptionKeyWidgets.visible[2]->defaultKey(GpgME::UnknownProtocol), preferredSolution.encryptionKeys.value("prefer-openpgp@example.net")[0].primaryFingerprint()); QCOMPARE(encryptionKeyWidgets.visible[3]->property("address").toString(), "prefer-smime@example.net"); QCOMPARE(encryptionKeyWidgets.visible[3]->defaultKey(GpgME::UnknownProtocol), preferredSolution.encryptionKeys.value("prefer-smime@example.net")[0].primaryFingerprint()); QCOMPARE(encryptionKeyWidgets.visible[4]->property("address").toString(), "unknown@example.net"); QVERIFY(encryptionKeyWidgets.visible[4]->defaultKey(GpgME::UnknownProtocol).isEmpty()); } void test__both_protocols_allowed__mixed_allowed__openpgp_only_preferred_solution() { const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; const bool allowMixed = true; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::OpenPGP, {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}, { {QStringLiteral("prefer-openpgp@example.net"), {testKey("Full Trust ", GpgME::OpenPGP)}}, {QStringLiteral("unknown@example.net"), {}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}}, }, }; const KeyResolver::Solution alternativeSolution = {}; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); verifyProtocolButton(dialog->findChild(QStringLiteral("openpgp button")), IsVisible, IsChecked); verifyProtocolButton(dialog->findChild(QStringLiteral("smime button")), IsVisible, IsUnchecked); verifyProtocolLabels(dialog->findChildren(QStringLiteral("protocol label")), 4, IsHidden); const auto signingKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("signing key"))); QCOMPARE(signingKeyWidgets.visible.size(), 1); QCOMPARE(signingKeyWidgets.hidden.size(), 1); QCOMPARE(signingKeyWidgets.visible[0]->defaultKey(GpgME::OpenPGP), preferredSolution.signingKeys[0].primaryFingerprint()); QCOMPARE(signingKeyWidgets.hidden[0]->defaultKey(GpgME::CMS), preferredSolution.signingKeys[1].primaryFingerprint()); const auto encryptionKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("encryption key"))); QCOMPARE(encryptionKeyWidgets.visible.size(), 3); QCOMPARE(encryptionKeyWidgets.hidden.size(), 1); // encryption key widgets for sender come first QCOMPARE(encryptionKeyWidgets.visible[0]->property("address").toString(), sender); QCOMPARE(encryptionKeyWidgets.visible[0]->defaultKey(GpgME::OpenPGP), preferredSolution.encryptionKeys.value(sender)[0].primaryFingerprint()); QCOMPARE(encryptionKeyWidgets.hidden[0]->property("address").toString(), sender); QCOMPARE(encryptionKeyWidgets.hidden[0]->defaultKey(GpgME::CMS), preferredSolution.encryptionKeys.value(sender)[1].primaryFingerprint()); // encryption key widgets for other recipients follow QCOMPARE(encryptionKeyWidgets.visible[1]->property("address").toString(), "prefer-openpgp@example.net"); QCOMPARE(encryptionKeyWidgets.visible[1]->defaultKey(GpgME::UnknownProtocol), preferredSolution.encryptionKeys.value("prefer-openpgp@example.net")[0].primaryFingerprint()); QCOMPARE(encryptionKeyWidgets.visible[2]->property("address").toString(), "unknown@example.net"); QVERIFY(encryptionKeyWidgets.visible[2]->defaultKey(GpgME::UnknownProtocol).isEmpty()); } void test__both_protocols_allowed__mixed_allowed__smime_only_preferred_solution() { const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; const bool allowMixed = true; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::CMS, {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}, { {QStringLiteral("prefer-smime@example.net"), {testKey("Trusted S/MIME ", GpgME::CMS)}}, {QStringLiteral("unknown@example.net"), {}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}}, }, }; const KeyResolver::Solution alternativeSolution = {}; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); verifyProtocolButton(dialog->findChild(QStringLiteral("openpgp button")), IsVisible, IsUnchecked); verifyProtocolButton(dialog->findChild(QStringLiteral("smime button")), IsVisible, IsChecked); verifyProtocolLabels(dialog->findChildren(QStringLiteral("protocol label")), 4, IsHidden); const auto signingKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("signing key"))); QCOMPARE(signingKeyWidgets.visible.size(), 1); QCOMPARE(signingKeyWidgets.hidden.size(), 1); QCOMPARE(signingKeyWidgets.visible[0]->defaultKey(GpgME::CMS), preferredSolution.signingKeys[1].primaryFingerprint()); QCOMPARE(signingKeyWidgets.hidden[0]->defaultKey(GpgME::OpenPGP), preferredSolution.signingKeys[0].primaryFingerprint()); const auto encryptionKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("encryption key"))); QCOMPARE(encryptionKeyWidgets.visible.size(), 3); QCOMPARE(encryptionKeyWidgets.hidden.size(), 1); // encryption key widgets for sender come first QCOMPARE(encryptionKeyWidgets.visible[0]->property("address").toString(), sender); QCOMPARE(encryptionKeyWidgets.visible[0]->defaultKey(GpgME::CMS), preferredSolution.encryptionKeys.value(sender)[1].primaryFingerprint()); QCOMPARE(encryptionKeyWidgets.hidden[0]->property("address").toString(), sender); QCOMPARE(encryptionKeyWidgets.hidden[0]->defaultKey(GpgME::OpenPGP), preferredSolution.encryptionKeys.value(sender)[0].primaryFingerprint()); // encryption key widgets for other recipients follow QCOMPARE(encryptionKeyWidgets.visible[1]->property("address").toString(), "prefer-smime@example.net"); QCOMPARE(encryptionKeyWidgets.visible[1]->defaultKey(GpgME::UnknownProtocol), preferredSolution.encryptionKeys.value("prefer-smime@example.net")[0].primaryFingerprint()); QCOMPARE(encryptionKeyWidgets.visible[2]->property("address").toString(), "unknown@example.net"); QVERIFY(encryptionKeyWidgets.visible[2]->defaultKey(GpgME::UnknownProtocol).isEmpty()); } void test__both_protocols_allowed__mixed_allowed__no_sender_keys() { const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; const bool allowMixed = true; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::UnknownProtocol, {}, { {QStringLiteral("prefer-openpgp@example.net"), {testKey("Full Trust ", GpgME::OpenPGP)}}, {QStringLiteral("prefer-smime@example.net"), {testKey("Trusted S/MIME ", GpgME::CMS)}}, {QStringLiteral("unknown@example.net"), {}}, {QStringLiteral("sender@example.net"), {}}, }, }; const KeyResolver::Solution alternativeSolution = {}; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); const auto signingKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("signing key"))); QCOMPARE(signingKeyWidgets.visible.size(), 2); QCOMPARE(signingKeyWidgets.hidden.size(), 0); const auto encryptionKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("encryption key"))); QCOMPARE(encryptionKeyWidgets.visible.size(), 5); QCOMPARE(encryptionKeyWidgets.hidden.size(), 0); // encryption key widgets for sender come first QCOMPARE(encryptionKeyWidgets.visible[0]->property("address").toString(), sender); QCOMPARE(encryptionKeyWidgets.visible[1]->property("address").toString(), sender); // encryption key widgets for other recipients follow QCOMPARE(encryptionKeyWidgets.visible[2]->property("address").toString(), "prefer-openpgp@example.net"); QCOMPARE(encryptionKeyWidgets.visible[2]->defaultKey(GpgME::UnknownProtocol), preferredSolution.encryptionKeys.value("prefer-openpgp@example.net")[0].primaryFingerprint()); QCOMPARE(encryptionKeyWidgets.visible[3]->property("address").toString(), "prefer-smime@example.net"); QCOMPARE(encryptionKeyWidgets.visible[3]->defaultKey(GpgME::UnknownProtocol), preferredSolution.encryptionKeys.value("prefer-smime@example.net")[0].primaryFingerprint()); QCOMPARE(encryptionKeyWidgets.visible[4]->property("address").toString(), "unknown@example.net"); QVERIFY(encryptionKeyWidgets.visible[4]->defaultKey(GpgME::UnknownProtocol).isEmpty()); } void test__both_protocols_allowed__mixed_allowed__encrypt_only() { const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; const bool allowMixed = true; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::UnknownProtocol, {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}, { {QStringLiteral("prefer-openpgp@example.net"), {testKey("Full Trust ", GpgME::OpenPGP)}}, {QStringLiteral("prefer-smime@example.net"), {testKey("Trusted S/MIME ", GpgME::CMS)}}, {QStringLiteral("unknown@example.net"), {}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}}, }, }; const KeyResolver::Solution alternativeSolution = {}; const auto dialog = std::make_unique(true, false, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); const auto signingKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("signing key"))); QCOMPARE(signingKeyWidgets.visible.size(), 0); QCOMPARE(signingKeyWidgets.hidden.size(), 0); const auto encryptionKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("encryption key"))); QCOMPARE(encryptionKeyWidgets.visible.size(), 5); QCOMPARE(encryptionKeyWidgets.hidden.size(), 0); } void test__ok_button_shows_generate_if_generate_is_selected() { const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; const bool allowMixed = true; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::OpenPGP, {}, // no signing keys to get "Generate key" choice in OpenPGP combo {{QStringLiteral("sender@example.net"), {}}} // no encryption keys to get "Generate key" choice in OpenPGP combo }; const KeyResolver::Solution alternativeSolution = {}; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); waitForKeySelectionCombosBeingInitialized(dialog.get()); const auto okButton = dialog->findChild("ok button"); QVERIFY(okButton); QVERIFY(okButton->text() != "Generate"); { // get the first signing key combo which is the OpenPGP one const auto signingKeyCombo = dialog->findChild("signing key"); verifyWidgetVisibility(signingKeyCombo, IsVisible); const auto originalIndex = signingKeyCombo->currentIndex(); const auto generateIndex = signingKeyCombo->findData(GenerateKey); QVERIFY(generateIndex != -1); signingKeyCombo->setCurrentIndex(generateIndex); QCOMPARE(okButton->text(), "Generate"); signingKeyCombo->setCurrentIndex(originalIndex); QVERIFY(okButton->text() != "Generate"); } { // get the first encryption key combo which is the OpenPGP one for the sender const auto encryptionKeyCombo = dialog->findChild("encryption key"); verifyWidgetVisibility(encryptionKeyCombo, IsVisible); const auto originalIndex = encryptionKeyCombo->currentIndex(); const auto generateIndex = encryptionKeyCombo->findData(GenerateKey); QVERIFY(generateIndex != -1); encryptionKeyCombo->setCurrentIndex(generateIndex); QCOMPARE(okButton->text(), QStringLiteral("Generate")); encryptionKeyCombo->setCurrentIndex(originalIndex); QVERIFY(okButton->text() != QStringLiteral("Generate")); } } void test__ok_button_does_not_show_generate_if_generate_is_selected_in_hidden_combos() { const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; const bool allowMixed = true; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::CMS, // enables S/MIME as default protocol, hides OpenPGP combos {}, // no signing keys to get "Generate key" choice in OpenPGP combo {{QStringLiteral("sender@example.net"), {}}} // no encryption keys to get "Generate key" choice in OpenPGP combo }; const KeyResolver::Solution alternativeSolution = {}; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); waitForKeySelectionCombosBeingInitialized(dialog.get()); const auto okButton = dialog->findChild("ok button"); QVERIFY(okButton); QVERIFY(okButton->text() != "Generate"); { // get the first signing key combo which is the OpenPGP one const auto signingKeyCombo = dialog->findChild("signing key"); verifyWidgetVisibility(signingKeyCombo, IsHidden); const auto originalIndex = signingKeyCombo->currentIndex(); const auto generateIndex = signingKeyCombo->findData(GenerateKey); QVERIFY(generateIndex != -1); signingKeyCombo->setCurrentIndex(generateIndex); QVERIFY(okButton->text() != QStringLiteral("Generate")); signingKeyCombo->setCurrentIndex(originalIndex); QVERIFY(okButton->text() != QStringLiteral("Generate")); } { // get the first encryption key combo which is the OpenPGP one for the sender const auto encryptionKeyCombo = dialog->findChild("encryption key"); verifyWidgetVisibility(encryptionKeyCombo, IsHidden); const auto originalIndex = encryptionKeyCombo->currentIndex(); const auto generateIndex = encryptionKeyCombo->findData(GenerateKey); QVERIFY(generateIndex != -1); encryptionKeyCombo->setCurrentIndex(generateIndex); QVERIFY(okButton->text() != QStringLiteral("Generate")); encryptionKeyCombo->setCurrentIndex(originalIndex); QVERIFY(okButton->text() != QStringLiteral("Generate")); } } void test__ok_button_is_disabled_if_ignore_is_selected_in_all_visible_encryption_combos() { const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; const bool allowMixed = true; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::OpenPGP, {}, // no signing keys to get "Generate key" choice in OpenPGP combo {{QStringLiteral("sender@example.net"), {}}} // no encryption keys to get "Generate key" choice in OpenPGP combo }; const KeyResolver::Solution alternativeSolution = {}; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); waitForKeySelectionCombosBeingInitialized(dialog.get()); const auto okButton = dialog->findChild(QStringLiteral("ok button")); QVERIFY(okButton); QVERIFY(okButton->isEnabled()); const auto encryptionKeyWidgets = visibleAndHiddenWidgets(dialog->findChildren(QStringLiteral("encryption key"))); for (auto combo : encryptionKeyWidgets.visible) { const auto ignoreIndex = combo->findData(IgnoreKey); QVERIFY(ignoreIndex != -1); combo->setCurrentIndex(ignoreIndex); } QVERIFY(!okButton->isEnabled()); } void test__vs_de_compliance__all_keys_fully_valid() { const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; const bool allowMixed = true; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::UnknownProtocol, {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}, { {QStringLiteral("prefer-openpgp@example.net"), {testKey("Full Trust ", GpgME::OpenPGP)}}, {QStringLiteral("prefer-smime@example.net"), {testKey("Trusted S/MIME ", GpgME::CMS)}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}}, }, }; const KeyResolver::Solution alternativeSolution = {}; Tests::FakeCryptoConfigStringValue fakeCompliance{"gpg", "compliance", QStringLiteral("de-vs")}; Tests::FakeCryptoConfigIntValue fakeDeVsCompliance{"gpg", "compliance_de_vs", 1}; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); waitForKeySelectionCombosBeingInitialized(dialog.get()); const auto complianceLabel = dialog->findChild(QStringLiteral("compliance label")); verifyWidgetVisibility(complianceLabel, IsVisible); QVERIFY(!complianceLabel->text().contains(" not ")); } void test__vs_de_compliance__not_all_keys_fully_valid() { const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; const bool allowMixed = true; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::UnknownProtocol, {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}, { {QStringLiteral("marginal-openpgp@example.net"), {testKey("Marginal Validity ", GpgME::OpenPGP)}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}}, }, }; const KeyResolver::Solution alternativeSolution = {}; Tests::FakeCryptoConfigStringValue fakeCompliance{"gpg", "compliance", QStringLiteral("de-vs")}; Tests::FakeCryptoConfigIntValue fakeDeVsCompliance{"gpg", "compliance_de_vs", 1}; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); waitForKeySelectionCombosBeingInitialized(dialog.get()); const auto complianceLabel = dialog->findChild(QStringLiteral("compliance label")); verifyWidgetVisibility(complianceLabel, IsVisible); QVERIFY(complianceLabel->text().contains(" not ")); } void test__vs_de_compliance__null_keys_are_ignored() { const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; const bool allowMixed = true; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::UnknownProtocol, {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}, { {QStringLiteral("unknown@example.net"), {}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}}, }, }; const KeyResolver::Solution alternativeSolution = {}; Tests::FakeCryptoConfigStringValue fakeCompliance{"gpg", "compliance", QStringLiteral("de-vs")}; Tests::FakeCryptoConfigIntValue fakeDeVsCompliance{"gpg", "compliance_de_vs", 1}; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); waitForKeySelectionCombosBeingInitialized(dialog.get()); const auto complianceLabel = dialog->findChild(QStringLiteral("compliance label")); verifyWidgetVisibility(complianceLabel, IsVisible); QVERIFY(!complianceLabel->text().contains(" not ")); } void test__sign_and_encrypt_to_self_only() { const GpgME::Protocol forcedProtocol = GpgME::OpenPGP; const bool allowMixed = false; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::OpenPGP, {testKey("sender@example.net", GpgME::OpenPGP)}, { {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::OpenPGP)}}, }, }; const KeyResolver::Solution alternativeSolution = {}; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); QVERIFY(!dialog->findChild(QStringLiteral("encrypt-to-others box"))); } void test__sign_and_encrypt_to_self_and_others() { const GpgME::Protocol forcedProtocol = GpgME::OpenPGP; const bool allowMixed = false; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::OpenPGP, {testKey("sender@example.net", GpgME::OpenPGP)}, { {QStringLiteral("prefer-openpgp@example.net"), {testKey("Full Trust ", GpgME::OpenPGP)}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::OpenPGP)}}, }, }; const KeyResolver::Solution alternativeSolution = {}; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); QVERIFY(dialog->findChild(QStringLiteral("encrypt-to-others box"))); } void test__result_does_not_include_null_keys() { const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; const bool allowMixed = true; const QString sender = QStringLiteral("unknown@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::UnknownProtocol, {}, { {QStringLiteral("prefer-openpgp@example.net"), {testKey("Full Trust ", GpgME::OpenPGP)}}, {QStringLiteral("prefer-smime@example.net"), {testKey("Trusted S/MIME ", GpgME::CMS)}}, {QStringLiteral("unknown@example.net"), {}}, }, }; const KeyResolver::Solution alternativeSolution = {}; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); waitForKeySelectionCombosBeingInitialized(dialog.get()); switchKeySelectionCombosFromGenerateKeyToIgnoreKey(dialog->findChildren()); const QSignalSpy dialogAcceptedSpy{dialog.get(), &QDialog::accepted}; QVERIFY(dialogAcceptedSpy.isValid()); const auto okButton = dialog->findChild(QStringLiteral("ok button")); QVERIFY(okButton); QVERIFY(okButton->isEnabled()); okButton->click(); QCOMPARE(dialogAcceptedSpy.count(), 1); verifySolution(dialog->result(), {GpgME::UnknownProtocol, {}, { {QStringLiteral("prefer-openpgp@example.net"), {testKey("Full Trust ", GpgME::OpenPGP)}}, {QStringLiteral("prefer-smime@example.net"), {testKey("Trusted S/MIME ", GpgME::CMS)}}, }}); } void test__result_has_keys_for_both_protocols_if_both_are_needed() { const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; const bool allowMixed = true; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::UnknownProtocol, {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}, { {QStringLiteral("prefer-openpgp@example.net"), {testKey("Full Trust ", GpgME::OpenPGP)}}, {QStringLiteral("prefer-smime@example.net"), {testKey("Trusted S/MIME ", GpgME::CMS)}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}}, }, }; const KeyResolver::Solution alternativeSolution = {}; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); waitForKeySelectionCombosBeingInitialized(dialog.get()); switchKeySelectionCombosFromGenerateKeyToIgnoreKey(dialog->findChildren()); const QSignalSpy dialogAcceptedSpy{dialog.get(), &QDialog::accepted}; QVERIFY(dialogAcceptedSpy.isValid()); const auto okButton = dialog->findChild(QStringLiteral("ok button")); QVERIFY(okButton); QVERIFY(okButton->isEnabled()); okButton->click(); QCOMPARE(dialogAcceptedSpy.count(), 1); verifySolution(dialog->result(), preferredSolution); } void test__result_has_only_openpgp_keys_if_openpgp_protocol_selected() { const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; const bool allowMixed = true; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::UnknownProtocol, {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}, { {QStringLiteral("prefer-openpgp@example.net"), {testKey("Full Trust ", GpgME::OpenPGP)}}, {QStringLiteral("prefer-smime@example.net"), {testKey("Trusted S/MIME ", GpgME::CMS)}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}}, }, }; const KeyResolver::Solution alternativeSolution = {}; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); waitForKeySelectionCombosBeingInitialized(dialog.get()); switchKeySelectionCombosFromGenerateKeyToIgnoreKey(dialog->findChildren()); const auto smimeButton = dialog->findChild(QStringLiteral("smime button")); QVERIFY(smimeButton); smimeButton->click(); QVERIFY(!smimeButton->isChecked()); const QSignalSpy dialogAcceptedSpy{dialog.get(), &QDialog::accepted}; QVERIFY(dialogAcceptedSpy.isValid()); const auto okButton = dialog->findChild(QStringLiteral("ok button")); QVERIFY(okButton); QVERIFY(okButton->isEnabled()); okButton->click(); QCOMPARE(dialogAcceptedSpy.count(), 1); verifySolution(dialog->result(), {GpgME::OpenPGP, {testKey("sender@example.net", GpgME::OpenPGP)}, { {QStringLiteral("prefer-openpgp@example.net"), {testKey("Full Trust ", GpgME::OpenPGP)}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::OpenPGP)}}, }}); } void test__result_has_only_smime_keys_if_smime_protocol_selected() { const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; const bool allowMixed = true; const QString sender = QStringLiteral("sender@example.net"); const KeyResolver::Solution preferredSolution = { GpgME::UnknownProtocol, {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}, { {QStringLiteral("prefer-openpgp@example.net"), {testKey("Full Trust ", GpgME::OpenPGP)}}, {QStringLiteral("prefer-smime@example.net"), {testKey("Trusted S/MIME ", GpgME::CMS)}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::OpenPGP), testKey("sender@example.net", GpgME::CMS)}}, }, }; const KeyResolver::Solution alternativeSolution = {}; const auto dialog = std::make_unique(true, true, sender, preferredSolution, alternativeSolution, allowMixed, forcedProtocol); dialog->show(); waitForKeySelectionCombosBeingInitialized(dialog.get()); switchKeySelectionCombosFromGenerateKeyToIgnoreKey(dialog->findChildren()); const auto openPGPButton = dialog->findChild(QStringLiteral("openpgp button")); QVERIFY(openPGPButton); openPGPButton->click(); QVERIFY(!openPGPButton->isChecked()); const QSignalSpy dialogAcceptedSpy{dialog.get(), &QDialog::accepted}; QVERIFY(dialogAcceptedSpy.isValid()); const auto okButton = dialog->findChild(QStringLiteral("ok button")); QVERIFY(okButton); QVERIFY(okButton->isEnabled()); okButton->click(); QCOMPARE(dialogAcceptedSpy.count(), 1); verifySolution(dialog->result(), {GpgME::CMS, {testKey("sender@example.net", GpgME::CMS)}, { {QStringLiteral("prefer-smime@example.net"), {testKey("Trusted S/MIME ", GpgME::CMS)}}, {QStringLiteral("sender@example.net"), {testKey("sender@example.net", GpgME::CMS)}}, }}); } private: std::shared_ptr mKeyCache; }; QTEST_MAIN(NewKeyApprovalDialogTest) #include "newkeyapprovaldialogtest.moc" diff --git a/src/kleo/enum.h b/src/kleo/enum.h index d74dbf4e7..80fa53d4b 100644 --- a/src/kleo/enum.h +++ b/src/kleo/enum.h @@ -1,92 +1,84 @@ /* kleo/enum.h 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 */ #pragma once #include "kleo_export.h" class QString; #include namespace GpgME { class Key; class UserID; } namespace Kleo { -enum class KeyUsage : char { - AnyUsage, - Sign, - Encrypt, - Certify, - Authenticate, -}; - enum CryptoMessageFormat { // clang-format off InlineOpenPGPFormat = 1, OpenPGPMIMEFormat = 2, SMIMEFormat = 4, SMIMEOpaqueFormat = 8, AnyOpenPGP = InlineOpenPGPFormat | OpenPGPMIMEFormat, AnySMIME = SMIMEOpaqueFormat | SMIMEFormat, AutoFormat = AnyOpenPGP | AnySMIME // clang-format on }; KLEO_EXPORT QString cryptoMessageFormatToLabel(CryptoMessageFormat f); KLEO_EXPORT const char *cryptoMessageFormatToString(CryptoMessageFormat f); KLEO_EXPORT QStringList cryptoMessageFormatsToStringList(unsigned int f); KLEO_EXPORT CryptoMessageFormat stringToCryptoMessageFormat(const QString &s); KLEO_EXPORT unsigned int stringListToCryptoMessageFormats(const QStringList &sl); enum Action { Conflict, DoIt, DontDoIt, Ask, AskOpportunistic, Impossible }; enum EncryptionPreference { // clang-format off UnknownPreference = 0, NeverEncrypt = 1, AlwaysEncrypt = 2, AlwaysEncryptIfPossible = 3, AlwaysAskForEncryption = 4, AskWheneverPossible = 5, MaxEncryptionPreference = AskWheneverPossible // clang-format on }; KLEO_EXPORT QString encryptionPreferenceToLabel(EncryptionPreference pref); KLEO_EXPORT const char *encryptionPreferenceToString(EncryptionPreference pref); KLEO_EXPORT EncryptionPreference stringToEncryptionPreference(const QString &str); enum SigningPreference { // clang-format off UnknownSigningPreference = 0, NeverSign = 1, AlwaysSign = 2, AlwaysSignIfPossible = 3, AlwaysAskForSigning = 4, AskSigningWheneverPossible = 5, MaxSigningPreference = AskSigningWheneverPossible // clang-format on }; KLEO_EXPORT QString signingPreferenceToLabel(SigningPreference pref); KLEO_EXPORT const char *signingPreferenceToString(SigningPreference pref); KLEO_EXPORT SigningPreference stringToSigningPreference(const QString &str); enum TrustLevel { Level0, Level1, Level2, Level3, Level4 }; KLEO_EXPORT TrustLevel trustLevel(const GpgME::Key &key); KLEO_EXPORT TrustLevel trustLevel(const GpgME::UserID &uid); } diff --git a/src/kleo/keyresolvercore.cpp b/src/kleo/keyresolvercore.cpp index f5384a569..e698273f7 100644 --- a/src/kleo/keyresolvercore.cpp +++ b/src/kleo/keyresolvercore.cpp @@ -1,785 +1,785 @@ /* -*- 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 "keyresolvercore.h" #include "kleo/enum.h" #include "kleo/keygroup.h" #include "models/keycache.h" #include "utils/formatting.h" #include "utils/gnupg.h" #include #include "libkleo_debug.h" 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, KeyUsage::Sign); + auto group = mCache->findGroup(address, protocol, KeyCache::KeyUsage::Sign); if (group.isNull()) { - group = mCache->findGroup(address, UnknownProtocol, KeyUsage::Sign); + 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, KeyUsage::Sign); + 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, KeyUsage::Encrypt); + 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, KeyUsage::Encrypt); + 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/models/keycache.cpp b/src/models/keycache.cpp index 822eaac25..e5a233af3 100644 --- a/src/models/keycache.cpp +++ b/src/models/keycache.cpp @@ -1,1738 +1,1738 @@ /* -*- 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 "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 "kleo/debug.h" #include "libkleo_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 : std::unary_function { 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 } }; struct ready_for_encryption : std::unary_function { #define DO(op, meth, meth2) \ if (op key.meth()) { \ } else { \ qDebug("rejecting for encrypting: %s: %s", #meth2, key.primaryFingerprint()); \ return false; \ } 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, KeyUsage usage) +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 == KeyUsage::Sign && !s.canSign()) { + if (usage == KeyCache::KeyUsage::Sign && !s.canSign()) { continue; } - if (usage == KeyUsage::Encrypt && !s.canEncrypt()) { + 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, KeyUsage usage) +bool allKeysAllowUsage(const T &keys, KeyCache::KeyUsage usage) { switch (usage) { - case KeyUsage::AnyUsage: + case KeyCache::KeyUsage::AnyUsage: return true; - case KeyUsage::Sign: + case KeyCache::KeyUsage::Sign: return std::all_of(std::begin(keys), std::end(keys), std::mem_fn(&Key::canSign)); - case KeyUsage::Encrypt: + case KeyCache::KeyUsage::Encrypt: return std::all_of(std::begin(keys), std::end(keys), std::mem_fn(&Key::canEncrypt)); - case KeyUsage::Certify: + case KeyCache::KeyUsage::Certify: return std::all_of(std::begin(keys), std::end(keys), std::mem_fn(&Key::canCertify)); - case KeyUsage::Authenticate: + 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/keycache.h b/src/models/keycache.h index 4de62daf1..cf002cdd9 100644 --- a/src/models/keycache.h +++ b/src/models/keycache.h @@ -1,213 +1,220 @@ /* -*- mode: c++; c-basic-offset:4 -*- models/keycache.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include #include "kleo_export.h" #include #include #include #include namespace GpgME { class Key; class DecryptionResult; class VerificationResult; class KeyListResult; class Subkey; } namespace KMime { namespace Types { class Mailbox; } } namespace Kleo { class FileSystemWatcher; class KeyGroup; class KeyGroupConfig; -enum class KeyUsage : char; class KeyCacheAutoRefreshSuspension; class KLEO_EXPORT KeyCache : public QObject { Q_OBJECT protected: explicit KeyCache(); public: + enum class KeyUsage { + AnyUsage, + Sign, + Encrypt, + Certify, + Authenticate, + }; + static std::shared_ptr instance(); static std::shared_ptr mutableInstance(); ~KeyCache() override; void setGroupsEnabled(bool enabled); void setGroupConfig(const std::shared_ptr &groupConfig); void insert(const GpgME::Key &key); void insert(const std::vector &keys); bool insert(const KeyGroup &group); void refresh(const std::vector &keys); bool update(const KeyGroup &group); void remove(const GpgME::Key &key); void remove(const std::vector &keys); bool remove(const KeyGroup &group); void addFileSystemWatcher(const std::shared_ptr &watcher); void enableFileSystemWatcher(bool enable); void setRefreshInterval(int hours); int refreshInterval() const; std::shared_ptr suspendAutoRefresh(); void enableRemarks(bool enable); bool remarksEnabled() const; const std::vector &keys() const; std::vector secretKeys() const; KeyGroup group(const QString &id) const; std::vector groups() const; std::vector configurableGroups() const; void saveConfigurableGroups(const std::vector &groups); const GpgME::Key &findByFingerprint(const char *fpr) const; const GpgME::Key &findByFingerprint(const std::string &fpr) const; std::vector findByFingerprint(const std::vector &fprs) const; std::vector findByEMailAddress(const char *email) const; std::vector findByEMailAddress(const std::string &email) const; /** Look through the cache and search for the best key for a mailbox. * * The best key is the key with a UID for the provided mailbox that * has the highest validity and a subkey that is capable for the given * usage. * If more then one key have a UID with the same validity * the most recently created key is taken. * * @returns the "best" key for the mailbox. */ GpgME::Key findBestByMailBox(const char *addr, GpgME::Protocol proto, KeyUsage usage) const; /** * Looks for a group named @a name which contains keys with protocol @a protocol * that are suitable for the usage @a usage. * * If @a protocol is GpgME::OpenPGP or GpgME::CMS, then only groups consisting of keys * matching this protocol are considered. Use @a protocol GpgME::UnknownProtocol to consider * any groups regardless of the protocol including mixed-protocol groups. * * If @a usage is not KeyUsage::AnyUsage, then only groups consisting of keys supporting this usage * are considered. * The validity of keys and the presence of a private key (necessary for signing, certification, and * authentication) is not taken into account. * * The first group that fulfills all conditions is returned. * * @returns a matching group or a null group if no matching group is found. */ KeyGroup findGroup(const QString &name, GpgME::Protocol protocol, KeyUsage usage) const; const GpgME::Key &findByShortKeyID(const char *id) const; const GpgME::Key &findByShortKeyID(const std::string &id) const; const GpgME::Key &findByKeyIDOrFingerprint(const char *id) const; const GpgME::Key &findByKeyIDOrFingerprint(const std::string &id) const; std::vector findByKeyIDOrFingerprint(const std::vector &ids) const; const GpgME::Subkey &findSubkeyByKeyGrip(const char *grip, GpgME::Protocol protocol = GpgME::UnknownProtocol) const; const GpgME::Subkey &findSubkeyByKeyGrip(const std::string &grip, GpgME::Protocol protocol = GpgME::UnknownProtocol) const; std::vector findSubkeysByKeyID(const std::vector &ids) const; std::vector findRecipients(const GpgME::DecryptionResult &result) const; std::vector findSigners(const GpgME::VerificationResult &result) const; std::vector findSigningKeysByMailbox(const QString &mb) const; std::vector findEncryptionKeysByMailbox(const QString &mb) const; /** Check for group keys. * * @returns A list of keys configured for groupName. Empty if no group cached.*/ std::vector getGroupKeys(const QString &groupName) const; enum Option { // clang-format off NoOption = 0, RecursiveSearch = 1, IncludeSubject = 2, // clang-format on }; Q_DECLARE_FLAGS(Options, Option) std::vector findSubjects(const GpgME::Key &key, Options option = RecursiveSearch) const; std::vector findSubjects(const std::vector &keys, Options options = RecursiveSearch) const; std::vector findSubjects(std::vector::const_iterator first, std::vector::const_iterator last, Options options = RecursiveSearch) const; std::vector findIssuers(const GpgME::Key &key, Options options = RecursiveSearch) const; /** Check if at least one keylisting was finished. */ bool initialized() const; /** Check if all keys have OpenPGP Protocol. */ bool pgpOnly() const; /** Set the keys the cache shall contain. Marks cache as initialized. Use for tests only. */ void setKeys(const std::vector &keys); void setGroups(const std::vector &groups); public Q_SLOTS: void clear(); void startKeyListing(GpgME::Protocol proto = GpgME::UnknownProtocol) { reload(proto); } void reload(GpgME::Protocol proto = GpgME::UnknownProtocol); void cancelKeyListing(); Q_SIGNALS: // void changed( const GpgME::Key & key ); void aboutToRemove(const GpgME::Key &key); void added(const GpgME::Key &key); void keyListingDone(const GpgME::KeyListResult &result); void keysMayHaveChanged(); void groupAdded(const Kleo::KeyGroup &group); void groupUpdated(const Kleo::KeyGroup &group); void groupRemoved(const Kleo::KeyGroup &group); private: class RefreshKeysJob; class Private; QScopedPointer const d; }; } Q_DECLARE_OPERATORS_FOR_FLAGS(Kleo::KeyCache::Options)