diff --git a/autotests/keyresolvercoretest.cpp b/autotests/keyresolvercoretest.cpp index fb5e4868f..a92f4ed24 100644 --- a/autotests/keyresolvercoretest.cpp +++ b/autotests/keyresolvercoretest.cpp @@ -1,516 +1,620 @@ /* autotests/keyresolvercoretest.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 using namespace Kleo; using namespace GpgME; namespace QTest { template <> inline bool qCompare(GpgME::UserID::Validity const &t1, GpgME::UserID::Validity const &t2, const char *actual, const char *expected, const char *file, int line) { return qCompare(int(t1), int(t2), actual, expected, file, line); } + +template <> +inline bool qCompare(int const &t1, KeyResolverCore::SolutionFlags const &t2, const char *actual, const char *expected, + const char *file, int line) +{ + return qCompare(int(t1), int(t2), actual, expected, file, line); +} } class KeyResolverCoreTest: public QObject { Q_OBJECT private Q_SLOTS: void init() { mGnupgHome = QTest::qExtractTestData("/fixtures/keyresolvercoretest"); qputenv("GNUPGHOME", mGnupgHome->path().toLocal8Bit()); // hold a reference to the key cache to avoid rebuilding while the test is running mKeyCache = KeyCache::instance(); } void cleanup() { // verify that nobody else holds a reference to the key cache QVERIFY(mKeyCache.use_count() == 1); mKeyCache.reset(); mGnupgHome.reset(); } void test_verify_test_keys() { { const Key openpgp = testKey("sender-mixed@example.net", OpenPGP); QVERIFY(openpgp.hasSecret() && openpgp.canEncrypt() && openpgp.canSign()); QCOMPARE(openpgp.userID(0).validity(), UserID::Ultimate); const Key smime = testKey("sender-mixed@example.net", CMS); QVERIFY(smime.hasSecret() && smime.canEncrypt() && smime.canSign()); QCOMPARE(smime.userID(0).validity(), UserID::Full); } { const Key openpgp = testKey("sender-openpgp@example.net", OpenPGP); QVERIFY(openpgp.hasSecret() && openpgp.canEncrypt() && openpgp.canSign()); QCOMPARE(openpgp.userID(0).validity(), UserID::Ultimate); } { const Key smime = testKey("sender-smime@example.net", CMS); QVERIFY(smime.hasSecret() && smime.canEncrypt() && smime.canSign()); QCOMPARE(smime.userID(0).validity(), UserID::Full); } { const Key openpgp = testKey("prefer-openpgp@example.net", OpenPGP); QVERIFY(openpgp.canEncrypt()); QCOMPARE(openpgp.userID(0).validity(), UserID::Ultimate); const Key smime = testKey("prefer-openpgp@example.net", CMS); QVERIFY(smime.canEncrypt()); QCOMPARE(smime.userID(0).validity(), UserID::Full); } { const Key openpgp = testKey("full-validity@example.net", OpenPGP); QVERIFY(openpgp.canEncrypt()); QCOMPARE(openpgp.userID(0).validity(), UserID::Full); const Key smime = testKey("full-validity@example.net", CMS); QVERIFY(smime.canEncrypt()); QCOMPARE(smime.userID(0).validity(), UserID::Full); } { const Key openpgp = testKey("prefer-smime@example.net", OpenPGP); QVERIFY(openpgp.canEncrypt()); QCOMPARE(openpgp.userID(0).validity(), UserID::Marginal); const Key smime = testKey("prefer-smime@example.net", CMS); QVERIFY(smime.canEncrypt()); QCOMPARE(smime.userID(0).validity(), UserID::Full); } } void test_openpgp_is_used_if_openpgp_only_and_smime_only_are_both_possible() { KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ true); + resolver.setAllowMixedProtocols(false); resolver.setSender(QStringLiteral("sender-mixed@example.net")); - const bool success = resolver.resolve(); + const auto result = resolver.resolve(); - QVERIFY(success); - QCOMPARE(resolver.signingKeys().value(OpenPGP).size(), 1); - QCOMPARE(resolver.signingKeys().value(OpenPGP)[0].primaryFingerprint(), + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::AllResolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::OpenPGPOnly); + QCOMPARE(result.solution.protocol, OpenPGP); + QCOMPARE(result.solution.signingKeys.size(), 1); + QCOMPARE(result.solution.signingKeys[0].primaryFingerprint(), testKey("sender-mixed@example.net", OpenPGP).primaryFingerprint()); - QCOMPARE(resolver.signingKeys().value(CMS).size(), 0); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).size(), 1); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).value("sender-mixed@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).value("sender-mixed@example.net")[0].primaryFingerprint(), + QCOMPARE(result.solution.encryptionKeys.size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-mixed@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-mixed@example.net")[0].primaryFingerprint(), testKey("sender-mixed@example.net", OpenPGP).primaryFingerprint()); - QCOMPARE(resolver.encryptionKeys().value(CMS).size(), 0); + QCOMPARE(result.alternative.protocol, CMS); + QCOMPARE(result.alternative.signingKeys.size(), 1); + QCOMPARE(result.alternative.signingKeys[0].primaryFingerprint(), + testKey("sender-mixed@example.net", CMS).primaryFingerprint()); + QCOMPARE(result.alternative.encryptionKeys.size(), 1); + QCOMPARE(result.alternative.encryptionKeys.value("sender-mixed@example.net").size(), 1); + QCOMPARE(result.alternative.encryptionKeys.value("sender-mixed@example.net")[0].primaryFingerprint(), + testKey("sender-mixed@example.net", CMS).primaryFingerprint()); } void test_openpgp_is_used_if_openpgp_only_and_smime_only_are_both_possible_with_preference_for_openpgp() { KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ true); + resolver.setAllowMixedProtocols(false); resolver.setPreferredProtocol(OpenPGP); resolver.setSender(QStringLiteral("sender-mixed@example.net")); - const bool success = resolver.resolve(); + const auto result = resolver.resolve(); - QVERIFY(success); - QCOMPARE(resolver.signingKeys().value(OpenPGP).size(), 1); - QCOMPARE(resolver.signingKeys().value(OpenPGP)[0].primaryFingerprint(), + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::AllResolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::OpenPGPOnly); + QCOMPARE(result.solution.protocol, OpenPGP); + QCOMPARE(result.solution.signingKeys.size(), 1); + QCOMPARE(result.solution.signingKeys[0].primaryFingerprint(), testKey("sender-mixed@example.net", OpenPGP).primaryFingerprint()); - QCOMPARE(resolver.signingKeys().value(CMS).size(), 0); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).size(), 1); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).value("sender-mixed@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).value("sender-mixed@example.net")[0].primaryFingerprint(), + QCOMPARE(result.solution.encryptionKeys.size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-mixed@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-mixed@example.net")[0].primaryFingerprint(), testKey("sender-mixed@example.net", OpenPGP).primaryFingerprint()); - QCOMPARE(resolver.encryptionKeys().value(CMS).size(), 0); + QCOMPARE(result.alternative.protocol, CMS); + QCOMPARE(result.alternative.signingKeys.size(), 1); + QCOMPARE(result.alternative.signingKeys[0].primaryFingerprint(), + testKey("sender-mixed@example.net", CMS).primaryFingerprint()); + QCOMPARE(result.alternative.encryptionKeys.size(), 1); + QCOMPARE(result.alternative.encryptionKeys.value("sender-mixed@example.net").size(), 1); + QCOMPARE(result.alternative.encryptionKeys.value("sender-mixed@example.net")[0].primaryFingerprint(), + testKey("sender-mixed@example.net", CMS).primaryFingerprint()); } void test_smime_is_used_if_openpgp_only_and_smime_only_are_both_possible_with_preference_for_smime() { KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ true); + resolver.setAllowMixedProtocols(false); resolver.setPreferredProtocol(CMS); resolver.setSender(QStringLiteral("sender-mixed@example.net")); - const bool success = resolver.resolve(); + const auto result = resolver.resolve(); - QVERIFY(success); - QCOMPARE(resolver.signingKeys().value(OpenPGP).size(), 0); - QCOMPARE(resolver.signingKeys().value(CMS).size(), 1); - QCOMPARE(resolver.signingKeys().value(CMS)[0].primaryFingerprint(), + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::AllResolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::CMSOnly); + QCOMPARE(result.solution.protocol, CMS); + QCOMPARE(result.solution.signingKeys.size(), 1); + QCOMPARE(result.solution.signingKeys[0].primaryFingerprint(), testKey("sender-mixed@example.net", CMS).primaryFingerprint()); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).size(), 0); - QCOMPARE(resolver.encryptionKeys().value(CMS).size(), 1); - QCOMPARE(resolver.encryptionKeys().value(CMS).value("sender-mixed@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(CMS).value("sender-mixed@example.net")[0].primaryFingerprint(), + QCOMPARE(result.solution.encryptionKeys.size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-mixed@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-mixed@example.net")[0].primaryFingerprint(), testKey("sender-mixed@example.net", CMS).primaryFingerprint()); + QCOMPARE(result.alternative.protocol, OpenPGP); + QCOMPARE(result.alternative.signingKeys.size(), 1); + QCOMPARE(result.alternative.signingKeys[0].primaryFingerprint(), + testKey("sender-mixed@example.net", OpenPGP).primaryFingerprint()); + QCOMPARE(result.alternative.encryptionKeys.size(), 1); + QCOMPARE(result.alternative.encryptionKeys.value("sender-mixed@example.net").size(), 1); + QCOMPARE(result.alternative.encryptionKeys.value("sender-mixed@example.net")[0].primaryFingerprint(), + testKey("sender-mixed@example.net", OpenPGP).primaryFingerprint()); } - void test_in_mixed_mode_keys_with_higher_validity_are_preferred() + void test_in_mixed_mode_openpgp_is_used_if_openpgp_only_and_smime_only_are_both_possible() + { + KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ true); + resolver.setSender(QStringLiteral("sender-mixed@example.net")); + + const auto result = resolver.resolve(); + + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::AllResolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::OpenPGPOnly); + QCOMPARE(result.solution.protocol, UnknownProtocol); + QCOMPARE(result.solution.signingKeys.size(), 1); + QCOMPARE(result.solution.signingKeys[0].primaryFingerprint(), + testKey("sender-mixed@example.net", OpenPGP).primaryFingerprint()); + QCOMPARE(result.solution.encryptionKeys.size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-mixed@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-mixed@example.net")[0].primaryFingerprint(), + testKey("sender-mixed@example.net", OpenPGP).primaryFingerprint()); + // no alternative solution is proposed + QCOMPARE(result.alternative.protocol, UnknownProtocol); + QCOMPARE(result.alternative.encryptionKeys.size(), 0); + } + + void test_in_mixed_mode_openpgp_is_used_if_openpgp_only_and_smime_only_are_both_possible_with_preference_for_openpgp() + { + KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ true); + resolver.setPreferredProtocol(OpenPGP); + resolver.setSender(QStringLiteral("sender-mixed@example.net")); + + const auto result = resolver.resolve(); + + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::AllResolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::OpenPGPOnly); + QCOMPARE(result.solution.protocol, UnknownProtocol); + QCOMPARE(result.solution.signingKeys.size(), 1); + QCOMPARE(result.solution.signingKeys[0].primaryFingerprint(), + testKey("sender-mixed@example.net", OpenPGP).primaryFingerprint()); + QCOMPARE(result.solution.encryptionKeys.size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-mixed@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-mixed@example.net")[0].primaryFingerprint(), + testKey("sender-mixed@example.net", OpenPGP).primaryFingerprint()); + // no alternative solution is proposed + QCOMPARE(result.alternative.protocol, UnknownProtocol); + QCOMPARE(result.alternative.encryptionKeys.size(), 0); + } + + void test_in_mixed_mode_smime_is_used_if_openpgp_only_and_smime_only_are_both_possible_with_preference_for_smime() + { + KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ true); + resolver.setPreferredProtocol(CMS); + resolver.setSender(QStringLiteral("sender-mixed@example.net")); + + const auto result = resolver.resolve(); + + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::AllResolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::CMSOnly); + QCOMPARE(result.solution.protocol, UnknownProtocol); + QCOMPARE(result.solution.signingKeys.size(), 1); + QCOMPARE(result.solution.signingKeys[0].primaryFingerprint(), + testKey("sender-mixed@example.net", CMS).primaryFingerprint()); + QCOMPARE(result.solution.encryptionKeys.size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-mixed@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-mixed@example.net")[0].primaryFingerprint(), + testKey("sender-mixed@example.net", CMS).primaryFingerprint()); + // no alternative solution is proposed + QCOMPARE(result.alternative.protocol, UnknownProtocol); + QCOMPARE(result.alternative.encryptionKeys.size(), 0); + } + + void test_in_mixed_mode_keys_with_higher_validity_are_preferred_if_both_protocols_are_needed() { KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ false); resolver.setRecipients({"sender-openpgp@example.net", "sender-smime@example.net", "prefer-openpgp@example.net", "prefer-smime@example.net"}); - const bool success = resolver.resolve(); + const auto result = resolver.resolve(); - QVERIFY(success); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).size(), 4); - QVERIFY(resolver.encryptionKeys().value(UnknownProtocol).contains("sender-openpgp@example.net")); - QVERIFY(resolver.encryptionKeys().value(UnknownProtocol).contains("sender-smime@example.net")); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("prefer-openpgp@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("prefer-openpgp@example.net")[0].primaryFingerprint(), + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::AllResolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::MixedProtocols); + QCOMPARE(result.solution.protocol, UnknownProtocol); + QCOMPARE(result.solution.encryptionKeys.size(), 4); + QVERIFY(result.solution.encryptionKeys.contains("sender-openpgp@example.net")); + QVERIFY(result.solution.encryptionKeys.contains("sender-smime@example.net")); + QCOMPARE(result.solution.encryptionKeys.value("prefer-openpgp@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("prefer-openpgp@example.net")[0].primaryFingerprint(), testKey("prefer-openpgp@example.net", OpenPGP).primaryFingerprint()); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("prefer-smime@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("prefer-smime@example.net")[0].primaryFingerprint(), + QCOMPARE(result.solution.encryptionKeys.value("prefer-smime@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("prefer-smime@example.net")[0].primaryFingerprint(), testKey("prefer-smime@example.net", CMS).primaryFingerprint()); + // no alternative solution is proposed + QCOMPARE(result.alternative.protocol, UnknownProtocol); + QCOMPARE(result.alternative.encryptionKeys.size(), 0); } - void test_reports_failure_if_both_protocols_are_allowed_but_no_keys_are_found_for_an_address() + void test_reports_unresolved_addresses_if_both_protocols_are_allowed_but_no_keys_are_found_for_an_address() { KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ false); resolver.setRecipients({"unknown@example.net"}); - const bool success = resolver.resolve(); + const auto result = resolver.resolve(); - QVERIFY(!success); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).size(), 0); - QCOMPARE(resolver.encryptionKeys().value(CMS).size(), 0); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).size(), 0); + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::SomeUnresolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::OpenPGPOnly); + QCOMPARE(result.solution.encryptionKeys.value("unknown@example.net").size(), 0); } - void test_reports_failure_if_openpgp_is_requested_and_no_openpgp_keys_are_found_for_an_adddress() + void test_reports_unresolved_addresses_if_openpgp_is_requested_and_no_openpgp_keys_are_found_for_an_address() { KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ false, OpenPGP); resolver.setRecipients({"sender-openpgp@example.net", "sender-smime@example.net"}); - const bool success = resolver.resolve(); - QVERIFY(!success); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).size(), 1); - QVERIFY(resolver.encryptionKeys().value(OpenPGP).contains("sender-openpgp@example.net")); - QCOMPARE(resolver.encryptionKeys().value(CMS).size(), 0); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).size(), 0); + const auto result = resolver.resolve(); + + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::SomeUnresolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::OpenPGPOnly); + QCOMPARE(result.solution.encryptionKeys.size(), 2); + QCOMPARE(result.solution.encryptionKeys.value("sender-openpgp@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-smime@example.net").size(), 0); + QCOMPARE(result.alternative.encryptionKeys.size(), 0); } - void test_reports_failure_if_smime_is_requested_and_no_smime_keys_are_found_for_an_adddress() + void test_reports_unresolved_addresses_if_smime_is_requested_and_no_smime_keys_are_found_for_an_address() { KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ false, CMS); resolver.setRecipients({"sender-openpgp@example.net", "sender-smime@example.net"}); - const bool success = resolver.resolve(); - QVERIFY(!success); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).size(), 0); - QCOMPARE(resolver.encryptionKeys().value(CMS).size(), 1); - QVERIFY(resolver.encryptionKeys().value(CMS).contains("sender-smime@example.net")); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).size(), 0); + const auto result = resolver.resolve(); + + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::SomeUnresolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::CMSOnly); + QCOMPARE(result.solution.encryptionKeys.size(), 2); + QCOMPARE(result.solution.encryptionKeys.value("sender-openpgp@example.net").size(), 0); + QCOMPARE(result.solution.encryptionKeys.value("sender-smime@example.net").size(), 1); + QCOMPARE(result.alternative.encryptionKeys.size(), 0); } - void test_reports_failure_if_mixed_protocols_are_not_allowed_but_needed() + void test_reports_unresolved_addresses_if_mixed_protocols_are_not_allowed_but_needed() { KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ false); resolver.setAllowMixedProtocols(false); resolver.setRecipients({"sender-openpgp@example.net", "sender-smime@example.net"}); - const bool success = resolver.resolve(); - QVERIFY(!success); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).size(), 1); - QVERIFY(resolver.encryptionKeys().value(OpenPGP).contains("sender-openpgp@example.net")); - QCOMPARE(resolver.encryptionKeys().value(CMS).size(), 1); - QVERIFY(resolver.encryptionKeys().value(CMS).contains("sender-smime@example.net")); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).size(), 0); + const auto result = resolver.resolve(); + + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::SomeUnresolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::OpenPGPOnly); + QCOMPARE(result.solution.encryptionKeys.size(), 2); + QCOMPARE(result.solution.encryptionKeys.value("sender-openpgp@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-smime@example.net").size(), 0); + QCOMPARE(result.alternative.encryptionKeys.size(), 2); + QCOMPARE(result.alternative.encryptionKeys.value("sender-openpgp@example.net").size(), 0); + QCOMPARE(result.alternative.encryptionKeys.value("sender-smime@example.net").size(), 1); } void test_openpgp_overrides_are_used_if_both_protocols_are_allowed() { const QString override = testKey("prefer-openpgp@example.net", OpenPGP).primaryFingerprint(); KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ true); + resolver.setAllowMixedProtocols(false); resolver.setSender(QStringLiteral("sender-mixed@example.net")); resolver.setRecipients({"full-validity@example.net"}); resolver.setOverrideKeys({{OpenPGP, {{QStringLiteral("Needs to be normalized "), {override}}}}}); - const bool success = resolver.resolve(); + const auto result = resolver.resolve(); - QVERIFY(success); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).value("full-validity@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).value("full-validity@example.net")[0].primaryFingerprint(), override); - QCOMPARE(resolver.encryptionKeys().value(CMS).size(), 0); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("full-validity@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("full-validity@example.net")[0].primaryFingerprint(), override); + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::AllResolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::OpenPGPOnly); + QCOMPARE(result.solution.encryptionKeys.value("full-validity@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("full-validity@example.net")[0].primaryFingerprint(), override); + QCOMPARE(result.alternative.encryptionKeys.value("full-validity@example.net").size(), 1); + QCOMPARE(result.alternative.encryptionKeys.value("full-validity@example.net")[0].primaryFingerprint(), + testKey("full-validity@example.net", CMS).primaryFingerprint()); } void test_openpgp_overrides_are_used_if_openpgp_only_is_requested() { const QString override = testKey("prefer-openpgp@example.net", OpenPGP).primaryFingerprint(); KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ true, OpenPGP); resolver.setSender(QStringLiteral("sender-mixed@example.net")); resolver.setRecipients({"full-validity@example.net"}); resolver.setOverrideKeys({{OpenPGP, {{QStringLiteral("Needs to be normalized "), {override}}}}}); - const bool success = resolver.resolve(); + const auto result = resolver.resolve(); - QVERIFY(success); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).value("full-validity@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).value("full-validity@example.net")[0].primaryFingerprint(), override); - QCOMPARE(resolver.encryptionKeys().value(CMS).size(), 0); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).size(), 0); + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::AllResolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::OpenPGPOnly); + QCOMPARE(result.solution.encryptionKeys.value("full-validity@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("full-validity@example.net")[0].primaryFingerprint(), override); + QCOMPARE(result.alternative.encryptionKeys.size(), 0); } void test_openpgp_overrides_are_ignored_if_smime_only_is_requested() { const QString override = testKey("prefer-openpgp@example.net", OpenPGP).primaryFingerprint(); KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ true, CMS); resolver.setSender(QStringLiteral("sender-mixed@example.net")); resolver.setRecipients({"full-validity@example.net"}); resolver.setOverrideKeys({{OpenPGP, {{QStringLiteral("Needs to be normalized "), {override}}}}}); - const bool success = resolver.resolve(); + const auto result = resolver.resolve(); - QVERIFY(success); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).size(), 0); - QCOMPARE(resolver.encryptionKeys().value(CMS).value("full-validity@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(CMS).value("full-validity@example.net")[0].primaryFingerprint(), + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::AllResolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::CMSOnly); + QCOMPARE(result.solution.encryptionKeys.value("full-validity@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("full-validity@example.net")[0].primaryFingerprint(), testKey("full-validity@example.net", CMS).primaryFingerprint()); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).size(), 0); + QCOMPARE(result.alternative.encryptionKeys.size(), 0); } - void test_smime_overrides_are_used_if_both_protocols_are_allowed() + void test_smime_overrides_are_used_if_both_protocols_are_allowed_and_smime_is_preferred() { const QString override = testKey("prefer-smime@example.net", CMS).primaryFingerprint(); KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ true); + resolver.setAllowMixedProtocols(false); resolver.setPreferredProtocol(CMS); resolver.setSender(QStringLiteral("sender-mixed@example.net")); resolver.setRecipients({"full-validity@example.net"}); resolver.setOverrideKeys({{CMS, {{QStringLiteral("Needs to be normalized "), {override}}}}}); - const bool success = resolver.resolve(); + const auto result = resolver.resolve(); - QVERIFY(success); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).size(), 0); - QCOMPARE(resolver.encryptionKeys().value(CMS).value("full-validity@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(CMS).value("full-validity@example.net")[0].primaryFingerprint(), override); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("full-validity@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("full-validity@example.net")[0].primaryFingerprint(), override); + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::AllResolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::CMSOnly); + QCOMPARE(result.solution.encryptionKeys.value("full-validity@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("full-validity@example.net")[0].primaryFingerprint(), override); + QCOMPARE(result.alternative.encryptionKeys.value("full-validity@example.net").size(), 1); + QCOMPARE(result.alternative.encryptionKeys.value("full-validity@example.net")[0].primaryFingerprint(), + testKey("full-validity@example.net", OpenPGP).primaryFingerprint()); } void test_smime_overrides_are_used_if_smime_only_is_requested() { const QString override = testKey("prefer-smime@example.net", CMS).primaryFingerprint(); KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ true, CMS); resolver.setSender(QStringLiteral("sender-mixed@example.net")); resolver.setRecipients({"full-validity@example.net"}); resolver.setOverrideKeys({{CMS, {{QStringLiteral("Needs to be normalized "), {override}}}}}); - const bool success = resolver.resolve(); + const auto result = resolver.resolve(); - QVERIFY(success); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).size(), 0); - QCOMPARE(resolver.encryptionKeys().value(CMS).value("full-validity@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(CMS).value("full-validity@example.net")[0].primaryFingerprint(), override); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).size(), 0); + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::AllResolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::CMSOnly); + QCOMPARE(result.solution.encryptionKeys.value("full-validity@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("full-validity@example.net")[0].primaryFingerprint(), override); + QCOMPARE(result.alternative.encryptionKeys.size(), 0); } void test_smime_overrides_are_ignored_if_openpgp_only_is_requested() { const QString override = testKey("prefer-smime@example.net", CMS).primaryFingerprint(); KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ true, OpenPGP); resolver.setSender(QStringLiteral("sender-mixed@example.net")); resolver.setRecipients({"full-validity@example.net"}); resolver.setOverrideKeys({{CMS, {{QStringLiteral("Needs to be normalized "), {override}}}}}); - const bool success = resolver.resolve(); + const auto result = resolver.resolve(); - QVERIFY(success); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).value("full-validity@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).value("full-validity@example.net")[0].primaryFingerprint(), + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::AllResolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::OpenPGPOnly); + QCOMPARE(result.solution.encryptionKeys.value("full-validity@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("full-validity@example.net")[0].primaryFingerprint(), testKey("full-validity@example.net", OpenPGP).primaryFingerprint()); - QCOMPARE(resolver.encryptionKeys().value(CMS).size(), 0); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).size(), 0); + QCOMPARE(result.alternative.encryptionKeys.size(), 0); } void test_overrides_for_wrong_protocol_are_ignored() { const QString override1 = testKey("full-validity@example.net", CMS).primaryFingerprint(); const QString override2 = testKey("full-validity@example.net", OpenPGP).primaryFingerprint(); KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ true); resolver.setSender(QStringLiteral("sender-mixed@example.net")); resolver.setRecipients({"sender-openpgp@example.net", "sender-smime@example.net"}); resolver.setOverrideKeys({{OpenPGP, {{QStringLiteral("Needs to be normalized "), {override1}}}}}); resolver.setOverrideKeys({{CMS, {{QStringLiteral("Needs to be normalized "), {override2}}}}}); - const bool success = resolver.resolve(); + const auto result = resolver.resolve(); - QVERIFY(success); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).value("sender-openpgp@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).value("sender-openpgp@example.net")[0].primaryFingerprint(), - testKey("sender-openpgp@example.net", OpenPGP).primaryFingerprint()); - QCOMPARE(resolver.encryptionKeys().value(CMS).value("sender-smime@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(CMS).value("sender-smime@example.net")[0].primaryFingerprint(), - testKey("sender-smime@example.net", CMS).primaryFingerprint()); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("sender-openpgp@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("sender-openpgp@example.net")[0].primaryFingerprint(), + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::AllResolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::MixedProtocols); + QCOMPARE(result.solution.encryptionKeys.value("sender-openpgp@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-openpgp@example.net")[0].primaryFingerprint(), testKey("sender-openpgp@example.net", OpenPGP).primaryFingerprint()); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("sender-smime@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("sender-smime@example.net")[0].primaryFingerprint(), + QCOMPARE(result.solution.encryptionKeys.value("sender-smime@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-smime@example.net")[0].primaryFingerprint(), testKey("sender-smime@example.net", CMS).primaryFingerprint()); } void test_openpgp_only_common_overrides_are_used_for_openpgp() { const QString override = testKey("prefer-openpgp@example.net", OpenPGP).primaryFingerprint(); KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ true); resolver.setSender(QStringLiteral("sender-mixed@example.net")); resolver.setRecipients({"sender-openpgp@example.net"}); resolver.setOverrideKeys({{UnknownProtocol, {{QStringLiteral("Needs to be normalized "), {override}}}}}); - const bool success = resolver.resolve(); + const auto result = resolver.resolve(); - QVERIFY(success); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).value("sender-openpgp@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).value("sender-openpgp@example.net")[0].primaryFingerprint(), override); - QCOMPARE(resolver.encryptionKeys().value(CMS).size(), 0); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("sender-openpgp@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("sender-openpgp@example.net")[0].primaryFingerprint(), override); + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::AllResolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::OpenPGPOnly); + QCOMPARE(result.solution.encryptionKeys.value("sender-openpgp@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-openpgp@example.net")[0].primaryFingerprint(), override); } void test_smime_only_common_overrides_are_used_for_smime() { const QString override = testKey("prefer-smime@example.net", CMS).primaryFingerprint(); KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ true); resolver.setSender(QStringLiteral("sender-mixed@example.net")); resolver.setRecipients({"sender-smime@example.net"}); resolver.setOverrideKeys({{UnknownProtocol, {{QStringLiteral("Needs to be normalized "), {override}}}}}); - const bool success = resolver.resolve(); + const auto result = resolver.resolve(); - QVERIFY(success); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).size(), 0); - QCOMPARE(resolver.encryptionKeys().value(CMS).value("sender-smime@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(CMS).value("sender-smime@example.net")[0].primaryFingerprint(), override); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("sender-smime@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("sender-smime@example.net")[0].primaryFingerprint(), override); + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::AllResolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::CMSOnly); + QCOMPARE(result.solution.encryptionKeys.value("sender-smime@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-smime@example.net")[0].primaryFingerprint(), override); } void test_mixed_protocol_common_overrides_override_protocol_specific_resolution() { const QString override1 = testKey("prefer-openpgp@example.net", OpenPGP).primaryFingerprint(); const QString override2 = testKey("prefer-smime@example.net", CMS).primaryFingerprint(); KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ true); resolver.setSender(QStringLiteral("sender-mixed@example.net")); resolver.setOverrideKeys({{UnknownProtocol, {{QStringLiteral("sender-mixed@example.net"), {override1, override2}}}}}); - const bool success = resolver.resolve(); + const auto result = resolver.resolve(); - QVERIFY(success); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).size(), 0); - QCOMPARE(resolver.encryptionKeys().value(CMS).size(), 0); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("sender-mixed@example.net").size(), 2); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("sender-mixed@example.net")[0].primaryFingerprint(), override1); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("sender-mixed@example.net")[1].primaryFingerprint(), override2); + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::AllResolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::MixedProtocols); + QCOMPARE(result.solution.encryptionKeys.value("sender-mixed@example.net").size(), 2); + QCOMPARE(result.solution.encryptionKeys.value("sender-mixed@example.net")[0].primaryFingerprint(), override1); + QCOMPARE(result.solution.encryptionKeys.value("sender-mixed@example.net")[1].primaryFingerprint(), override2); } void test_common_overrides_override_protocol_specific_overrides() { const QString override1 = testKey("full-validity@example.net", OpenPGP).primaryFingerprint(); const QString override2 = testKey("full-validity@example.net", CMS).primaryFingerprint(); KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ true); resolver.setSender(QStringLiteral("sender-mixed@example.net")); resolver.setRecipients({"sender-openpgp@example.net", "sender-smime@example.net"}); resolver.setOverrideKeys({ {OpenPGP, { {QStringLiteral("sender-openpgp@example.net"), {testKey("prefer-openpgp@example.net", OpenPGP).primaryFingerprint()}} }}, {CMS, { {QStringLiteral("sender-smime@example.net"), {testKey("prefer-smime@example.net", CMS).primaryFingerprint()}} }}, {UnknownProtocol, { {QStringLiteral("sender-openpgp@example.net"), {override1}}, {QStringLiteral("sender-smime@example.net"), {override2}} }} }); - const bool success = resolver.resolve(); - - QVERIFY(success); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).value("sender-openpgp@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(OpenPGP).value("sender-openpgp@example.net")[0].primaryFingerprint(), override1); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("sender-openpgp@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("sender-openpgp@example.net")[0].primaryFingerprint(), override1); - QCOMPARE(resolver.encryptionKeys().value(CMS).value("sender-smime@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(CMS).value("sender-smime@example.net")[0].primaryFingerprint(), override2); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("sender-smime@example.net").size(), 1); - QCOMPARE(resolver.encryptionKeys().value(UnknownProtocol).value("sender-smime@example.net")[0].primaryFingerprint(), override2); + const auto result = resolver.resolve(); + + QCOMPARE(result.flags & KeyResolverCore::ResolvedMask, KeyResolverCore::AllResolved); + QCOMPARE(result.flags & KeyResolverCore::ProtocolsMask, KeyResolverCore::MixedProtocols); + QCOMPARE(result.solution.encryptionKeys.value("sender-openpgp@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-openpgp@example.net")[0].primaryFingerprint(), override1); + QCOMPARE(result.solution.encryptionKeys.value("sender-smime@example.net").size(), 1); + QCOMPARE(result.solution.encryptionKeys.value("sender-smime@example.net")[0].primaryFingerprint(), override2); } void test_reports_failure_if_openpgp_is_requested_but_common_overrides_require_smime() { KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ false, OpenPGP); resolver.setRecipients({"sender-mixed@example.net"}); resolver.setOverrideKeys({{UnknownProtocol, { {QStringLiteral("sender-mixed@example.net"), {testKey("prefer-smime@example.net", CMS).primaryFingerprint()}} }}}); - const bool success = resolver.resolve(); - QVERIFY(!success); - QVERIFY(resolver.encryptionKeys().empty()); + const auto result = resolver.resolve(); + + QVERIFY(result.flags & KeyResolverCore::Error); } void test_reports_failure_if_smime_is_requested_but_common_overrides_require_openpgp() { KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ false, CMS); resolver.setRecipients({"sender-mixed@example.net"}); resolver.setOverrideKeys({{UnknownProtocol, { {QStringLiteral("sender-mixed@example.net"), {testKey("prefer-openpgp@example.net", OpenPGP).primaryFingerprint()}} }}}); - const bool success = resolver.resolve(); - QVERIFY(!success); - QVERIFY(resolver.encryptionKeys().empty()); + const auto result = resolver.resolve(); + + QVERIFY(result.flags & KeyResolverCore::Error); } void test_reports_failure_if_mixed_protocols_are_not_allowed_but_required_by_common_overrides() { KeyResolverCore resolver(/*encrypt=*/ true, /*sign=*/ false); resolver.setAllowMixedProtocols(false); resolver.setRecipients({"sender-mixed@example.net"}); resolver.setOverrideKeys({{UnknownProtocol, { {QStringLiteral("sender-mixed@example.net"), { testKey("prefer-openpgp@example.net", OpenPGP).primaryFingerprint(), testKey("prefer-smime@example.net", CMS).primaryFingerprint() }} }}}); - const bool success = resolver.resolve(); - QVERIFY(!success); - QVERIFY(resolver.encryptionKeys().empty()); + const auto result = resolver.resolve(); + + QVERIFY(result.flags & KeyResolverCore::Error); } private: Key testKey(const char *email, Protocol protocol = UnknownProtocol) { const std::vector keys = KeyCache::instance()->findByEMailAddress(email); for (const auto &key: keys) { if (protocol == UnknownProtocol || key.protocol() == protocol) { return key; } } return Key(); } private: QSharedPointer mGnupgHome; std::shared_ptr mKeyCache; }; QTEST_MAIN(KeyResolverCoreTest) #include "keyresolvercoretest.moc" diff --git a/autotests/newkeyapprovaldialogtest.cpp b/autotests/newkeyapprovaldialogtest.cpp index c4a13c89e..8b8f25f42 100644 --- a/autotests/newkeyapprovaldialogtest.cpp +++ b/autotests/newkeyapprovaldialogtest.cpp @@ -1,225 +1,466 @@ /* 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 using namespace Kleo; namespace { GpgME::Key createTestKey(const char *uid, GpgME::Protocol protocol = GpgME::UnknownProtocol) { static int count = 0; count++; gpgme_key_t key; gpgme_key_from_uid(&key, uid); Q_ASSERT(key); 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()); return GpgME::Key(key, false); } template -QList visibleWidgets(const QList &widgets) +struct Widgets { - QList result; - std::copy_if(widgets.begin(), widgets.end(), - std::back_inserter(result), - std::mem_fn(&QWidget::isVisible)); + 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; } } class NewKeyApprovalDialogTest: public QObject { Q_OBJECT private Q_SLOTS: - void test_all_resolved_exclusive_prefer_OpenPGP() + void test__both_protocols_allowed__mixed_not_allowed__openpgp_preferred() { - const QStringList unresolvedSenders; - const QStringList unresolvedRecipients; + const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; + const bool allowMixed = false; const QString sender = QStringLiteral("sender@example.net"); - bool allowMixed = false; - GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; - GpgME::Protocol presetProtocol = GpgME::OpenPGP; - const auto dialog = std::make_unique(resolved_senders_openpgp_and_smime(), - resolved_recipients_openpgp_and_smime(), - unresolvedSenders, - unresolvedRecipients, + const KeyResolver::Solution preferredSolution = { + GpgME::OpenPGP, + {createTestKey("sender@example.net", GpgME::OpenPGP)}, + { + {QStringLiteral("prefer-openpgp@example.net"), {createTestKey("Full Trust ", GpgME::OpenPGP)}}, + {QStringLiteral("prefer-smime@example.net"), {}}, + {QStringLiteral("sender@example.net"), {createTestKey("sender@example.net", GpgME::OpenPGP)}} + } + }; + const KeyResolver::Solution alternativeSolution = { + GpgME::CMS, + {createTestKey("sender@example.net", GpgME::CMS)}, + { + {QStringLiteral("prefer-openpgp@example.net"), {}}, + {QStringLiteral("prefer-smime@example.net"), {createTestKey("Trusted S/MIME ", GpgME::CMS)}}, + {QStringLiteral("sender@example.net"), {createTestKey("sender@example.net", GpgME::CMS)}} + } + }; + + const auto dialog = std::make_unique(true, + true, sender, + preferredSolution, + alternativeSolution, allowMixed, - forcedProtocol, - presetProtocol); + forcedProtocol); dialog->show(); - const QList signingKeyWidgets = dialog->findChildren(QStringLiteral("signing key")); - QCOMPARE(signingKeyWidgets.size(), 2); - const auto visibleSigningKeyWidgets = visibleWidgets(signingKeyWidgets); - QCOMPARE(visibleSigningKeyWidgets.size(), 1); - for (auto combo: visibleSigningKeyWidgets) { - QVERIFY(combo); - QVERIFY2(!combo->defaultKey(GpgME::OpenPGP).isEmpty(), "visible signing key widget should default to OpenPGP key"); - } - const QList encryptionKeyWidgets = dialog->findChildren(QStringLiteral("encryption key")); - QCOMPARE(encryptionKeyWidgets.size(), 4); - const auto visibleEncryptionKeyWidgets = visibleWidgets(encryptionKeyWidgets); - QCOMPARE(visibleEncryptionKeyWidgets.size(), 3); - QCOMPARE(visibleEncryptionKeyWidgets[0]->property("address").toString(), sender); - QVERIFY2(!visibleEncryptionKeyWidgets[0]->defaultKey(GpgME::OpenPGP).isEmpty(), - "encryption key widget for sender's OpenPGP key is first visible widget"); - for (auto combo: visibleEncryptionKeyWidgets) { - QVERIFY(combo); - QVERIFY2(combo->property("address").toString() != sender || !combo->defaultKey(GpgME::OpenPGP).isEmpty(), - "encryption key widget for sender's CMS key should be hidden"); - } + + 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_all_resolved_exclusive_prefer_SMIME() + void test__both_protocols_allowed__mixed_not_allowed__smime_preferred() { - const QStringList unresolvedSenders; - const QStringList unresolvedRecipients; + const GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; + const bool allowMixed = false; const QString sender = QStringLiteral("sender@example.net"); - bool allowMixed = false; - GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; - GpgME::Protocol presetProtocol = GpgME::CMS; - const auto dialog = std::make_unique(resolved_senders_openpgp_and_smime(), - resolved_recipients_openpgp_and_smime(), - unresolvedSenders, - unresolvedRecipients, + const KeyResolver::Solution preferredSolution = { + GpgME::CMS, + {createTestKey("sender@example.net", GpgME::CMS)}, + { + {QStringLiteral("prefer-openpgp@example.net"), {}}, + {QStringLiteral("prefer-smime@example.net"), {createTestKey("Trusted S/MIME ", GpgME::CMS)}}, + {QStringLiteral("sender@example.net"), {createTestKey("sender@example.net", GpgME::CMS)}} + } + }; + const KeyResolver::Solution alternativeSolution = { + GpgME::OpenPGP, + {createTestKey("sender@example.net", GpgME::OpenPGP)}, + { + {QStringLiteral("prefer-openpgp@example.net"), {createTestKey("Full Trust ", GpgME::OpenPGP)}}, + {QStringLiteral("prefer-smime@example.net"), {}}, + {QStringLiteral("sender@example.net"), {createTestKey("sender@example.net", GpgME::OpenPGP)}} + } + }; + + const auto dialog = std::make_unique(true, + true, sender, + preferredSolution, + alternativeSolution, allowMixed, - forcedProtocol, - presetProtocol); + forcedProtocol); dialog->show(); - const QList signingKeyWidgets = dialog->findChildren(QStringLiteral("signing key")); - QCOMPARE(signingKeyWidgets.size(), 2); - const auto visibleSigningKeyWidgets = visibleWidgets(signingKeyWidgets); - QCOMPARE(visibleSigningKeyWidgets.size(), 1); - for (auto combo: visibleSigningKeyWidgets) { - QVERIFY(combo); - QVERIFY2(!combo->defaultKey(GpgME::CMS).isEmpty(), "visible signing key widget should default to S/MIME key"); - } - const QList encryptionKeyWidgets = dialog->findChildren(QStringLiteral("encryption key")); - QCOMPARE(encryptionKeyWidgets.size(), 4); - const auto visibleEncryptionKeyWidgets = visibleWidgets(encryptionKeyWidgets); - QCOMPARE(visibleEncryptionKeyWidgets.size(), 3); - QCOMPARE(visibleEncryptionKeyWidgets[0]->property("address").toString(), sender); - QVERIFY2(!visibleEncryptionKeyWidgets[0]->defaultKey(GpgME::CMS).isEmpty(), - "encryption key widget for sender's CMS key is first visible widget"); - for (auto combo: visibleEncryptionKeyWidgets) { - QVERIFY(combo); - QVERIFY2(combo->property("address").toString() != sender || !combo->defaultKey(GpgME::CMS).isEmpty(), - "encryption key widget for sender's OpenPGP key should be hidden"); - } + + 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_all_resolved_allow_mixed_no_protocol_preference() + void test__openpgp_only() { - const QStringList unresolvedSenders; - const QStringList unresolvedRecipients; + const GpgME::Protocol forcedProtocol = GpgME::OpenPGP; + const bool allowMixed = false; const QString sender = QStringLiteral("sender@example.net"); - bool allowMixed = true; - GpgME::Protocol forcedProtocol = GpgME::UnknownProtocol; - GpgME::Protocol presetProtocol = GpgME::UnknownProtocol; - const auto resolvedSenders = resolved_senders_openpgp_and_smime(); - auto resolvedRecipients = resolved_recipients_openpgp_and_smime(); - resolvedRecipients["prefer-smime@example.net"].push_back(createTestKey("OpenPGP ", GpgME::OpenPGP)); - resolvedRecipients.insert("smime-only@example.net", {createTestKey("S/MIME Only ", GpgME::CMS)}); - const auto dialog = std::make_unique(resolvedSenders, - resolvedRecipients, - unresolvedSenders, - unresolvedRecipients, + const KeyResolver::Solution preferredSolution = { + GpgME::OpenPGP, + {createTestKey("sender@example.net", GpgME::OpenPGP)}, + { + {QStringLiteral("prefer-openpgp@example.net"), {createTestKey("Full Trust ", GpgME::OpenPGP)}}, + {QStringLiteral("prefer-smime@example.net"), {}}, + {QStringLiteral("sender@example.net"), {createTestKey("sender@example.net", GpgME::OpenPGP)}} + } + }; + 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(), 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, + {createTestKey("sender@example.net", GpgME::CMS)}, + { + {QStringLiteral("prefer-openpgp@example.net"), {}}, + {QStringLiteral("prefer-smime@example.net"), {createTestKey("Trusted S/MIME ", GpgME::CMS)}}, + {QStringLiteral("sender@example.net"), {createTestKey("sender@example.net", GpgME::CMS)}} + } + }; + const KeyResolver::Solution alternativeSolution = {}; + + const auto dialog = std::make_unique(true, + true, sender, + preferredSolution, + alternativeSolution, allowMixed, - forcedProtocol, - presetProtocol); + forcedProtocol); dialog->show(); - const QList signingKeyWidgets = dialog->findChildren(QStringLiteral("signing key")); - QCOMPARE(signingKeyWidgets.size(), 2); - for (auto widget : signingKeyWidgets) { - QVERIFY2(widget->isVisible(), "signing key widget should be visible"); - } - // first signing key widget should default to sender's OpenPGP key, the other to sender's S/MIME key - QCOMPARE(signingKeyWidgets[0]->defaultKey(GpgME::OpenPGP), - QString::fromLatin1(resolvedSenders["sender@example.net"][0].primaryFingerprint())); - QCOMPARE(signingKeyWidgets[1]->defaultKey(GpgME::CMS), - QString::fromLatin1(resolvedSenders["sender@example.net"][1].primaryFingerprint())); - - const QList encryptionKeyWidgets = dialog->findChildren(QStringLiteral("encryption key")); - QCOMPARE(encryptionKeyWidgets.size(), 5); - for (auto widget : encryptionKeyWidgets) { - QVERIFY2(widget->isVisible(), - qPrintable(QString("encryption key widget should be visible for address %1").arg(widget->property("address").toString()))); - } - // first two encryption key widgets shall be widgets for sender's keys - QCOMPARE(encryptionKeyWidgets[0]->property("address").toString(), sender); - QCOMPARE(encryptionKeyWidgets[0]->defaultKey(GpgME::OpenPGP), - QString::fromLatin1(resolvedRecipients["sender@example.net"][0].primaryFingerprint())); - QCOMPARE(encryptionKeyWidgets[1]->property("address").toString(), sender); - QCOMPARE(encryptionKeyWidgets[1]->defaultKey(GpgME::CMS), - QString::fromLatin1(resolvedRecipients["sender@example.net"][1].primaryFingerprint())); - // further encryption key widgets shall be widgets for keys of recipients, where OpenPGP keys are preferred due to no specific preset - QCOMPARE(encryptionKeyWidgets[2]->property("address").toString(), QStringLiteral("prefer-openpgp@example.net")); - QCOMPARE(encryptionKeyWidgets[2]->defaultKey(), - QString::fromLatin1(resolvedRecipients["prefer-openpgp@example.net"][0].primaryFingerprint())); - QCOMPARE(encryptionKeyWidgets[3]->property("address").toString(), QStringLiteral("prefer-smime@example.net")); - QCOMPARE(encryptionKeyWidgets[3]->defaultKey(), - QString::fromLatin1(resolvedRecipients["prefer-smime@example.net"][1].primaryFingerprint())); - QCOMPARE(encryptionKeyWidgets[4]->property("address").toString(), QStringLiteral("smime-only@example.net")); - QCOMPARE(encryptionKeyWidgets[4]->defaultKey(), - QString::fromLatin1(resolvedRecipients["smime-only@example.net"][0].primaryFingerprint())); + 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, + {createTestKey("sender@example.net", GpgME::OpenPGP), createTestKey("sender@example.net", GpgME::CMS)}, + { + {QStringLiteral("prefer-openpgp@example.net"), {createTestKey("Full Trust ", GpgME::OpenPGP)}}, + {QStringLiteral("prefer-smime@example.net"), {createTestKey("Trusted S/MIME ", GpgME::CMS)}}, + {QStringLiteral("unknown@example.net"), {}}, + {QStringLiteral("sender@example.net"), {createTestKey("sender@example.net", GpgME::OpenPGP), createTestKey("sender@example.net", GpgME::CMS)}} + } + }; + 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); + 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__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"), {createTestKey("Full Trust ", GpgME::OpenPGP)}}, + {QStringLiteral("prefer-smime@example.net"), {createTestKey("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, + {createTestKey("sender@example.net", GpgME::OpenPGP), createTestKey("sender@example.net", GpgME::CMS)}, + { + {QStringLiteral("prefer-openpgp@example.net"), {createTestKey("Full Trust ", GpgME::OpenPGP)}}, + {QStringLiteral("prefer-smime@example.net"), {createTestKey("Trusted S/MIME ", GpgME::CMS)}}, + {QStringLiteral("unknown@example.net"), {}}, + {QStringLiteral("sender@example.net"), {createTestKey("sender@example.net", GpgME::OpenPGP), createTestKey("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); } private: QMap > resolved_senders_openpgp_and_smime() { return { {QStringLiteral("sender@example.net"), { createTestKey("sender@example.net", GpgME::OpenPGP), createTestKey("sender@example.net", GpgME::CMS) }} }; } QMap > resolved_recipients_openpgp_and_smime() { return { {QStringLiteral("prefer-openpgp@example.net"), { createTestKey("Full Trust ", GpgME::OpenPGP) }}, {QStringLiteral("prefer-smime@example.net"), { createTestKey("Trusted S/MIME ", GpgME::CMS) }}, {QStringLiteral("sender@example.net"), { createTestKey("sender@example.net", GpgME::OpenPGP), createTestKey("sender@example.net", GpgME::CMS) }} }; } }; QTEST_MAIN(NewKeyApprovalDialogTest) #include "newkeyapprovaldialogtest.moc" diff --git a/src/kleo/keyresolver.cpp b/src/kleo/keyresolver.cpp index 589267504..81dd0820f 100644 --- a/src/kleo/keyresolver.cpp +++ b/src/kleo/keyresolver.cpp @@ -1,288 +1,169 @@ /* -*- c++ -*- keyresolver.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2018 Intevation GmbH SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker Based on kpgp.cpp SPDX-FileCopyrightText: 2001, 2002 the KPGP authors See file libkdenetwork/AUTHORS.kpgp for details SPDX-License-Identifier: GPL-2.0-or-later */ #include "keyresolver.h" #include "keyresolvercore.h" #include "models/keycache.h" #include "ui/newkeyapprovaldialog.h" #include "utils/formatting.h" #include #include "libkleo_debug.h" using namespace Kleo; using namespace GpgME; class KeyResolver::Private { public: Private(KeyResolver* qq, bool enc, bool sig, Protocol fmt, bool allowMixed) : q(qq) , mCore(enc, sig, fmt) , mFormat(fmt) , mEncrypt(enc) , mSign(sig) , mAllowMixed(allowMixed) , mCache(KeyCache::instance()) , mDialogWindowFlags(Qt::WindowFlags()) , mPreferredProtocol(UnknownProtocol) { mCore.setAllowMixedProtocols(allowMixed); } ~Private() = default; - void showApprovalDialog(QWidget *parent); + void showApprovalDialog(KeyResolverCore::Result result, QWidget *parent); void dialogAccepted(); KeyResolver *const q; KeyResolverCore mCore; - QMap> mSigKeys; - QMap>> mEncKeys; + Solution mResult; Protocol mFormat; bool mEncrypt; bool mSign; bool mAllowMixed; // The cache is needed as a member variable to avoid rebuilding // it between calls if we are the only user. std::shared_ptr mCache; - std::shared_ptr mDialog; + std::unique_ptr mDialog; Qt::WindowFlags mDialogWindowFlags; Protocol mPreferredProtocol; }; -void KeyResolver::Private::showApprovalDialog(QWidget *parent) +void KeyResolver::Private::showApprovalDialog(KeyResolverCore::Result result, QWidget *parent) { const QString sender = mCore.normalizedSender(); - const QMap> signingKeys = mCore.signingKeys(); - const QStringList unresolvedPGP = mCore.unresolvedRecipients(OpenPGP); - const QStringList unresolvedCMS = mCore.unresolvedRecipients(CMS); - - QMap > resolvedSig; - QStringList unresolvedSig; - const bool pgpOnly = unresolvedPGP.empty() && (!mSign || signingKeys.contains(OpenPGP)); - const bool cmsOnly = unresolvedCMS.empty() && (!mSign || signingKeys.contains(CMS)); - // First handle the signing keys - if (mSign) { - if (signingKeys.empty()) { - unresolvedSig << sender; - } else { - std::vector resolvedSigKeys; - for (const auto &keys: signingKeys) { - for (const auto &key: keys) { - resolvedSigKeys.push_back(key); - } - } - resolvedSig.insert(sender, resolvedSigKeys); - } - } - - // Now build the encryption keys - QMap > resolvedRecp; - QStringList unresolvedRecp; - - if (mEncrypt) { - // Use all unresolved recipients. - if (!cmsOnly && !pgpOnly) { - if (mFormat == UnknownProtocol) { - // In Auto Format we can now remove recipients that could - // be resolved either through CMS or PGP - for (const auto &addr: qAsConst(unresolvedPGP)) { - if (unresolvedCMS.contains(addr)) { - unresolvedRecp << addr; - } - } - } else if (mFormat == OpenPGP) { - unresolvedRecp = unresolvedPGP; - } else if (mFormat == CMS) { - unresolvedRecp = unresolvedCMS; - } - } - - // Now Map all resolved encryption keys regardless of the format. - const QMap>> encryptionKeys = mCore.encryptionKeys(); - for (const auto &map: encryptionKeys.values()) { - for (auto it = map.cbegin(); it != map.cend(); ++it) { - const QString &addr = it.key(); - const auto &keys = it.value(); - if (!resolvedRecp.contains(addr) || !resolvedRecp[addr].size()) { - resolvedRecp.insert(addr, keys); - } else { - std::vector merged = resolvedRecp[addr]; - // Add without duplication - for (const auto &k: keys) { - const auto it = std::find_if (merged.begin(), merged.end(), [k] (const Key &y) { - return (k.primaryFingerprint() && y.primaryFingerprint() && - !strcmp (k.primaryFingerprint(), y.primaryFingerprint())); - }); - if (it == merged.end()) { - merged.push_back(k); - } - } - resolvedRecp[addr] = merged; - } - } - } - } - - // Do we force the protocol? - Protocol forcedProto = mFormat; - - // Start with the protocol for which every keys could be found. - Protocol presetProtocol; - - if (mPreferredProtocol == CMS && cmsOnly) { - presetProtocol = CMS; - } else { - presetProtocol = pgpOnly ? OpenPGP : - cmsOnly ? CMS : - mPreferredProtocol; - } - - mDialog = std::shared_ptr(new NewKeyApprovalDialog(resolvedSig, - resolvedRecp, - unresolvedSig, - unresolvedRecp, - sender, - mAllowMixed, - forcedProto, - presetProtocol, - parent, - mDialogWindowFlags)); + mDialog = std::make_unique(mEncrypt, + mSign, + sender, + std::move(result.solution), + std::move(result.alternative), + mAllowMixed, + mFormat, + parent, + mDialogWindowFlags); connect (mDialog.get(), &QDialog::accepted, q, [this] () { dialogAccepted(); }); connect (mDialog.get(), &QDialog::rejected, q, [this] () { - Q_EMIT q->keysResolved(false, false);} - ); + Q_EMIT q->keysResolved(false, false); + }); mDialog->open(); } void KeyResolver::Private::dialogAccepted() { - for (const auto &key: mDialog->signingKeys()) { - if (!mSigKeys.contains(key.protocol())) { - mSigKeys.insert(key.protocol(), std::vector()); - } - mSigKeys[key.protocol()].push_back(key); - } - - const auto &encMap = mDialog->encryptionKeys(); - // First we fill the protocol-specific maps with - // the results of the dialog. Then we use the sender - // address to determine if a keys in the specific - // maps need updating. - - bool isUnresolved = false; - for (const auto &addr: encMap.keys()) { - for (const auto &key: encMap[addr]) { - if (key.isNull()) { - isUnresolved = true; - } - if (!mEncKeys.contains(key.protocol())) { - mEncKeys.insert(key.protocol(), QMap >()); - } - if (!mEncKeys[key.protocol()].contains(addr)) { - mEncKeys[key.protocol()].insert(addr, std::vector()); - } - qCDebug (LIBKLEO_LOG) << "Adding" << addr << "for" << Formatting::displayName(key.protocol()) - << "fpr:" << key.primaryFingerprint(); - - mEncKeys[key.protocol()][addr].push_back(key); - } - } - - if (isUnresolved) { - // TODO show warning - } - + mResult = mDialog->result(); Q_EMIT q->keysResolved(true, false); } void KeyResolver::start(bool showApproval, QWidget *parentWidget) { qCDebug(LIBKLEO_LOG) << "Starting "; if (!d->mSign && !d->mEncrypt) { // nothing to do return Q_EMIT keysResolved(true, true); } - const bool success = d->mCore.resolve(); - + const auto result = d->mCore.resolve(); + const bool success = (result.flags & KeyResolverCore::AllResolved); if (success && !showApproval) { + d->mResult = std::move(result.solution); + // update protocol hint of solution if solution is single-protocol solution + const auto protocolsHint = result.flags & KeyResolverCore::ProtocolsMask; + if (protocolsHint == KeyResolverCore::OpenPGPOnly) { + d->mResult.protocol = OpenPGP; + } else if (protocolsHint == KeyResolverCore::CMSOnly) { + d->mResult.protocol = CMS; + } Q_EMIT keysResolved(true, false); return; } else if (success) { qCDebug(LIBKLEO_LOG) << "No need for the user showing approval anyway."; } - d->showApprovalDialog(parentWidget); + d->showApprovalDialog(std::move(result), parentWidget); } KeyResolver::KeyResolver(bool encrypt, bool sign, Protocol fmt, bool allowMixed) : d(new Private(this, encrypt, sign, fmt, allowMixed)) { } Kleo::KeyResolver::~KeyResolver() = default; void KeyResolver::setRecipients(const QStringList &addresses) { d->mCore.setRecipients(addresses); } void KeyResolver::setSender(const QString &address) { d->mCore.setSender(address); } void KeyResolver::setOverrideKeys(const QMap > &overrides) { d->mCore.setOverrideKeys(overrides); } void KeyResolver::setSigningKeys(const QStringList &fingerprints) { d->mCore.setSigningKeys(fingerprints); } -QMap > > KeyResolver::encryptionKeys() const -{ - return d->mCore.encryptionKeys(); -} - -QMap > KeyResolver::signingKeys() const +KeyResolver::Solution KeyResolver::result() const { - return d->mCore.signingKeys(); + return d->mResult; } void KeyResolver::setDialogWindowFlags(Qt::WindowFlags flags) { d->mDialogWindowFlags = flags; } void KeyResolver::setPreferredProtocol(Protocol proto) { d->mCore.setPreferredProtocol(proto); } void KeyResolver::setMinimumValidity(int validity) { d->mCore.setMinimumValidity(validity); } diff --git a/src/kleo/keyresolver.h b/src/kleo/keyresolver.h index 2518f34b9..adfa55f8c 100644 --- a/src/kleo/keyresolver.h +++ b/src/kleo/keyresolver.h @@ -1,200 +1,199 @@ /* -*- c++ -*- keyresolver.h This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2018 Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "kleo_export.h" #include #include #include #include #include #include #include #include namespace GpgME { class Key; } namespace Kleo { /** * Class to find Keys for E-Mail encryption. * * The KeyResolver uses the Keycache to find keys for signing * or encryption. * * Overrides can be provided for address book integration if the * format is not Auto overrides will only be respected if they * match the format provided in the constructor. * * If no override key is provided for an address the key * with a uid that matches the address and has the highest * validity is used. If both keys have the same validity * the newest subkey is used. * * The KeyResolver also supports groups so the number of * encryption keys does not necessarily * need to match the amount of sender addresses. For this reason * maps are used heavily to map: * * CryptoFormat * - Addresses * -- For each Address a List of Keys * * As a caller you should iterate over the CryptoFormats and * send one message for each format for the recipients and signed * by the signing keys. * * If the CryptoMessageFormat is Auto the minimum number * of CryptoMessageFormats is returned that respects all overrides. * * ----- * Planned: * * As the central place to manage mail encryption / signing keys * the Keyresolver will also show various warning / nagging messages * and offer solutions if nagging is not explicitly turned off. * These include: * * - If own keys or subkeys are about to expire: * Offer to extend their expiration date. * * - (S/MIME) If they are about to expire: Offer * to generate a new CSR. * * - If a user has not marked a key as backed up and * uses it for encryption for several mails. * * - If a user has multiple keys for a sender address and they * are not cross signed. Offer to cross sign / publish. */ class KLEO_EXPORT KeyResolver : public QObject { Q_OBJECT public: + struct Solution + { + GpgME::Protocol protocol = GpgME::UnknownProtocol; + std::vector signingKeys; + QMap> encryptionKeys; + }; + /** Creates a new key resolver object. * * @param encrypt: Should encryption keys be selected. * @param sign: Should signing keys be selected. * @param format: A specific key protocol (OpenPGP, S/MIME) for selection. Default: Both protocols. * @param allowMixed: Specify if multiple message formats may be resolved. **/ explicit KeyResolver(bool encrypt, bool sign, GpgME::Protocol format = GpgME::UnknownProtocol, bool allowMixed = true); ~KeyResolver() override; /** * Set the list of recipient addresses. Also looks * up possible keys, but doesn't interact with the user. * * @param addresses: A list of unnormalized addresses */ void setRecipients(const QStringList &addresses); /** * Set the senders address. * * Sender address will be added to encryption keys and used * for signing key resolution if the signing keys are not * explicitly set through setSigningKeys. * * @param sender: The sender of this message. */ void setSender(const QString &sender); /** * Set up possible override keys for recipients / sender * addresses. The keys for the fingerprints are looked * up and used when found. Does not interact with the user. * * @param overrides: A map of \ -> (\ \) */ void setOverrideKeys(const QMap > &overrides); /** * Set explicit signing keys. If this was set for a * protocol the sender address will be only used as an additional encryption * recipient for that protocol. */ void setSigningKeys(const QStringList &fingerprints); /** * Set the minimum user id validity for autoresolution. * * The default value is marginal * * @param validity int representation of a GpgME::UserID::Validity. */ void setMinimumValidity(int validity); /** - * Get the encryption keys after resolution. - * - * @return the resolved sender / key pairs for encryption by format. - */ - QMap > > encryptionKeys() const; - - /** - * Get the signing keys to use after resolution. + * Get the result of the resolution. * - * @return the resolved resolved sender / key pairs for signing - * by format. + * @return the resolved keys for signing and encryption. */ - QMap > signingKeys() const; + Solution result() const; /** * Starts the key resolving procedure. Emits keysResolved on success or * error. * * @param showApproval: If set to true a dialog listing the keys * will always be shown. * @param parentWidget: Optional, a Widget to use as parent for dialogs. */ void start(bool showApproval, QWidget *parentWidget = nullptr); /** * Set window flags for a possible dialog. */ void setDialogWindowFlags(Qt::WindowFlags flags); /** * Set the protocol that is preferred to be displayed first when * it is not clear from the keys. E.g. if both OpenPGP and S/MIME * can be resolved. */ void setPreferredProtocol(GpgME::Protocol proto); Q_SIGNALS: /** * Emitted when key resolution finished. * * @param success: The general result. If true continue sending, * if false abort. * @param sendUnencrypted: If there could be no key found for one of * the recipients the user was queried if the * mail should be sent out unencrypted. * sendUnencrypted is true if the user agreed * to this.*/ void keysResolved(bool success, bool sendUnencrypted); private: class Private; std::unique_ptr d; }; } // namespace Kleo diff --git a/src/kleo/keyresolvercore.cpp b/src/kleo/keyresolvercore.cpp index bdac3b94b..b22b66c18 100644 --- a/src/kleo/keyresolvercore.cpp +++ b/src/kleo/keyresolvercore.cpp @@ -1,633 +1,676 @@ /* -*- 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 "models/keycache.h" #include "utils/formatting.h" #include #include "libkleo_debug.h" using namespace Kleo; using namespace GpgME; namespace { 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) , mCompliance(Formatting::complianceMode()) { } ~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(); void resolveSign(Protocol proto); void setSigningKeys(const QStringList &fingerprints); std::vector resolveRecipient(const QString &address, Protocol protocol); void resolveEnc(Protocol proto); void mergeEncryptionKeys(); QStringList unresolvedRecipients(GpgME::Protocol protocol) const; - bool resolve(); + 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; QString mCompliance; }; bool KeyResolverCore::Private::isAcceptableSigningKey(const Key &key) { if (!ValidSigningKey(key)) { return false; } if (mCompliance == QLatin1String("de-vs")) { 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 (mCompliance == QLatin1String("de-vs")) { 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()); if (mSign) { 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)); } } } } void KeyResolverCore::Private::resolveSign(Protocol proto) { if (mSigKeys.contains(proto)) { // Explicitly set return; } const auto keys = mCache->findBestByMailBox(mSender.toUtf8().constData(), proto, true, false); for (const auto &key: keys) { if (key.isNull()) { continue; } if (!isAcceptableSigningKey(key)) { qCDebug(LIBKLEO_LOG) << "Unacceptable signing key" << key.primaryFingerprint() << "for" << mSender; return; } } if (!keys.empty() && !keys[0].isNull()) { mSigKeys.insert(proto, keys); } } 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; } auto list = mSigKeys.value(key.protocol()); list.push_back(key); mSigKeys.insert(key.protocol(), list); } } } std::vector KeyResolverCore::Private::resolveRecipient(const QString &address, Protocol protocol) { const auto keys = mCache->findBestByMailBox(address.toUtf8().constData(), protocol, false, true); if (keys.empty() || keys[0].isNull()) { qCDebug(LIBKLEO_LOG) << "Failed to find any" << Formatting::displayName(protocol) << "key for: " << address; return {}; } if (keys.size() == 1) { if (!isAcceptableEncryptionKey(keys[0], address)) { qCDebug(LIBKLEO_LOG) << "key for:" << address << keys[0].primaryFingerprint() << "has not enough validity"; return {}; } } else { // 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. bool unacceptable = false; for (const auto &key: keys) { if (!isAcceptableEncryptionKey(key)) { qCDebug(LIBKLEO_LOG) << "group key for:" << address << keys[0].primaryFingerprint() << "has not enough validity"; unacceptable = true; break; } } if (unacceptable) { return {}; } } for (const auto &k: keys) { qCDebug(LIBKLEO_LOG) << "Resolved encrypt to" << address << "with key" << k.primaryFingerprint(); } return keys; } // 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) continue; } const std::vector &commonOverride = protocolKeysMap[UnknownProtocol]; if (!commonOverride.empty()) { // there is a common override; use it for current protocol if possible if (allKeysHaveProtocol(commonOverride, proto)) { protocolKeysMap[proto] = commonOverride; continue; } else { qCDebug(LIBKLEO_LOG) << "Common override for" << address << "is unusable for" << Formatting::displayName(proto); continue; } } protocolKeysMap[proto] = resolveRecipient(address, proto); } } -void KeyResolverCore::Private::mergeEncryptionKeys() +auto getBestEncryptionKeys(const QMap>> &encryptionKeys, Protocol preferredProtocol) { - for (auto it = mEncKeys.begin(); it != mEncKeys.end(); ++it) { + QMap> result; + + for (auto it = encryptionKeys.begin(); it != encryptionKeys.end(); ++it) { const QString &address = it.key(); auto &protocolKeysMap = it.value(); - if (!protocolKeysMap[UnknownProtocol].empty()) { - // override keys are set for address + const std::vector &overrideKeys = protocolKeysMap[UnknownProtocol]; + if (!overrideKeys.empty()) { + result.insert(address, overrideKeys); continue; } - const std::vector &keysOpenPGP = protocolKeysMap.value(OpenPGP); - const std::vector &keysCMS = protocolKeysMap.value(CMS); + const std::vector &keysOpenPGP = protocolKeysMap[OpenPGP]; + const std::vector &keysCMS = protocolKeysMap[CMS]; if (keysOpenPGP.empty() && keysCMS.empty()) { - continue; + result.insert(address, {}); } else if (!keysOpenPGP.empty() && keysCMS.empty()) { - protocolKeysMap[UnknownProtocol] = keysOpenPGP; + result.insert(address, keysOpenPGP); } else if (keysOpenPGP.empty() && !keysCMS.empty()) { - protocolKeysMap[UnknownProtocol] = keysCMS; + 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 ((validityPGP > validityCMS) - || (validityPGP == validityCMS && mPreferredProtocol == OpenPGP)) { - protocolKeysMap[UnknownProtocol] = keysOpenPGP; - } else if ((validityCMS > validityPGP) - || (validityCMS == validityPGP && mPreferredProtocol == CMS)) { - protocolKeysMap[UnknownProtocol] = keysCMS; + if ((validityCMS > validityPGP) || (validityCMS == validityPGP && preferredProtocol == CMS)) { + result.insert(address, keysCMS); } else { - protocolKeysMap[UnknownProtocol] = keysOpenPGP; + result.insert(address, keysOpenPGP); } } } + + return result; } QStringList KeyResolverCore::Private::unresolvedRecipients(GpgME::Protocol protocol) const { QStringList result; result.reserve(mEncKeys.size()); for (auto it = mEncKeys.begin(); it != mEncKeys.end(); ++it) { const auto &protocolKeysMap = it.value(); if (protocolKeysMap.value(protocol).empty()) { result.push_back(it.key()); } } return result; } namespace { 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; } -bool KeyResolverCore::Private::resolve() +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 true; + 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 false; + return {Error, {}, {}}; } // Then look for signing / encryption keys - if (mFormat != CMS) { + if (mFormat == OpenPGP || mFormat == UnknownProtocol) { resolveSign(OpenPGP); resolveEnc(OpenPGP); } - const bool pgpOnly = !hasUnresolvedRecipients(mEncKeys, OpenPGP) && (!mSign || mSigKeys.contains(OpenPGP)); + const bool pgpOnly = (!mEncrypt || !hasUnresolvedRecipients(mEncKeys, OpenPGP)) && (!mSign || mSigKeys.contains(OpenPGP)); + + if (mFormat == OpenPGP) { + return { + SolutionFlags((pgpOnly ? AllResolved : SomeUnresolved) | OpenPGPOnly), + {OpenPGP, mSigKeys.value(OpenPGP), keysForProtocol(mEncKeys, OpenPGP)}, + {} + }; + } - if (mFormat != OpenPGP) { + if (mFormat == CMS || mFormat == UnknownProtocol) { resolveSign(CMS); resolveEnc(CMS); } - const bool cmsOnly = !hasUnresolvedRecipients(mEncKeys, CMS) && (!mSign || mSigKeys.contains(CMS)); + const bool cmsOnly = (!mEncrypt || !hasUnresolvedRecipients(mEncKeys, CMS)) && (!mSign || mSigKeys.contains(CMS)); - if (mAllowMixed && mFormat == UnknownProtocol) { - mergeEncryptionKeys(); + if (mFormat == CMS) { + return { + SolutionFlags((cmsOnly ? AllResolved : SomeUnresolved) | CMSOnly), + {CMS, mSigKeys.value(CMS), keysForProtocol(mEncKeys, CMS)}, + {} + }; } - const bool hasUnresolvedMerged = mAllowMixed && mFormat == UnknownProtocol && hasUnresolvedRecipients(mEncKeys, UnknownProtocol); - // Check if we need the user to select different keys. - bool needsUser = false; - if (!pgpOnly && !cmsOnly) { - if (mAllowMixed && mFormat == UnknownProtocol) { - needsUser = hasUnresolvedMerged; + // 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 { - needsUser = (mFormat == UnknownProtocol) - || (mFormat == OpenPGP && !pgpOnly) - || (mFormat == CMS && !cmsOnly); + // keys marked as mixed (UnknownProtocol) as hint that OpenPGP keys are also allowed in key approval + return { + SolutionFlags(AllResolved | CMSOnly), + {UnknownProtocol, mSigKeys.value(CMS), keysForProtocol(mEncKeys, CMS)}, + {} + }; } - if (mSign) { - // So every recipient could be resolved through - // a combination of PGP and S/MIME do we also - // have signing keys for both? - needsUser |= !(mSigKeys.contains(OpenPGP) && - mSigKeys.contains(CMS)); + } + if (pgpOnly) { + if (!mAllowMixed) { + return { + SolutionFlags(AllResolved | OpenPGPOnly), + {OpenPGP, mSigKeys.value(OpenPGP), keysForProtocol(mEncKeys, OpenPGP)}, + {CMS, mSigKeys.value(CMS), keysForProtocol(mEncKeys, CMS)} + }; + } else { + // keys marked as mixed (UnknownProtocol) as hint that S/MIME keys are also allowed in key approval + return { + SolutionFlags(AllResolved | OpenPGPOnly), + {UnknownProtocol, mSigKeys.value(OpenPGP), keysForProtocol(mEncKeys, OpenPGP)}, + {} + }; } } - if (!needsUser) { - if (pgpOnly && cmsOnly) { - if (mPreferredProtocol == CMS) { - mSigKeys.remove(OpenPGP); - for (auto &protocolKeysMap: mEncKeys) { - protocolKeysMap.remove(OpenPGP); - } - } else { - mSigKeys.remove(CMS); - for (auto &protocolKeysMap: mEncKeys) { - protocolKeysMap.remove(CMS); - } - } - } else if (pgpOnly) { - mSigKeys.remove(CMS); - for (auto &protocolKeysMap: mEncKeys) { - protocolKeysMap.remove(CMS); - } - } else if (cmsOnly) { - mSigKeys.remove(OpenPGP); - for (auto &protocolKeysMap: mEncKeys) { - protocolKeysMap.remove(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)} + }; } + } - qCDebug(LIBKLEO_LOG) << "Automatic key resolution done."; - return true; + const auto bestEncryptionKeys = getBestEncryptionKeys(mEncKeys, mPreferredProtocol); + const bool allAddressesAreResolved = std::all_of(std::begin(bestEncryptionKeys), std::end(bestEncryptionKeys), + [] (const auto &keys) { return !keys.empty(); }); + if (allAddressesAreResolved) { + return { + SolutionFlags(AllResolved | MixedProtocols), + {UnknownProtocol, concatenate(mSigKeys.value(OpenPGP), mSigKeys.value(CMS)), bestEncryptionKeys}, + {} + }; } - return false; + const bool allKeysAreOpenPGP = std::all_of(std::begin(bestEncryptionKeys), std::end(bestEncryptionKeys), + [] (const auto &keys) { return allKeysHaveProtocol(keys, OpenPGP); }); + if (allKeysAreOpenPGP) { + // keys marked as mixed (UnknownProtocol) as hint that S/MIME keys are also allowed in key approval + return { + SolutionFlags(SomeUnresolved | OpenPGPOnly), + {UnknownProtocol, 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) { + // keys marked as mixed (UnknownProtocol) as hint that S/MIME keys are also allowed in key approval + return { + SolutionFlags(SomeUnresolved | CMSOnly), + {UnknownProtocol, 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; } -bool KeyResolverCore::resolve() +KeyResolverCore::Result KeyResolverCore::resolve() { return d->resolve(); } - -QMap > KeyResolverCore::signingKeys() const -{ - return d->mSigKeys; -} - -QMap>> KeyResolverCore::encryptionKeys() const -{ - QMap>> result; - - for (auto addressIt = d->mEncKeys.cbegin(); addressIt != d->mEncKeys.cend(); ++addressIt) { - const QString &address = addressIt.key(); - const auto &protocolKeysMap = addressIt.value(); - for (auto protocolIt = protocolKeysMap.cbegin(); protocolIt != protocolKeysMap.cend(); ++protocolIt) { - const Protocol protocol = protocolIt.key(); - const auto &keys = protocolIt.value(); - if (!keys.empty()) { - result[protocol][address] = keys; - } - } - } - - return result; -} - -QStringList KeyResolverCore::unresolvedRecipients(GpgME::Protocol protocol) const -{ - return d->unresolvedRecipients(protocol); -} diff --git a/src/kleo/keyresolvercore.h b/src/kleo/keyresolvercore.h index 490d1c69d..15d7643dd 100644 --- a/src/kleo/keyresolvercore.h +++ b/src/kleo/keyresolvercore.h @@ -1,70 +1,87 @@ /* -*- c++ -*- kleo/keyresolvercore.h This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2018 Intevation GmbH SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once +#include + #include "kleo_export.h" #include #include #include #include class QString; class QStringList; namespace GpgME { class Key; } namespace Kleo { class KLEO_EXPORT KeyResolverCore { public: + enum SolutionFlags + { + SomeUnresolved = 0, + AllResolved = 1, + + OpenPGPOnly = 2, + CMSOnly = 4, + MixedProtocols = OpenPGPOnly | CMSOnly, + + Error = 0x1000, + + ResolvedMask = AllResolved | Error, + ProtocolsMask = OpenPGPOnly | CMSOnly | Error, + }; + struct Result + { + SolutionFlags flags; + KeyResolver::Solution solution; + KeyResolver::Solution alternative; + }; + explicit KeyResolverCore(bool encrypt, bool sign, GpgME::Protocol format = GpgME::UnknownProtocol); ~KeyResolverCore(); void setSender(const QString &sender); QString normalizedSender() const; void setRecipients(const QStringList &addresses); void setSigningKeys(const QStringList &fingerprints); void setOverrideKeys(const QMap > &overrides); void setAllowMixedProtocols(bool allowMixed); void setPreferredProtocol(GpgME::Protocol proto); void setMinimumValidity(int validity); - bool resolve(); - - QMap > signingKeys() const; - - QMap > > encryptionKeys() const; - - QStringList unresolvedRecipients(GpgME::Protocol protocol) const; + Result resolve(); private: class Private; std::unique_ptr d; }; } // namespace Kleo diff --git a/src/tests/test_keyresolver.cpp b/src/tests/test_keyresolver.cpp index b2bf0ecaf..59f133ab8 100644 --- a/src/tests/test_keyresolver.cpp +++ b/src/tests/test_keyresolver.cpp @@ -1,150 +1,146 @@ /* test_keyresolver.cpp This file is part of libkleopatra's test suite. SPDX-FileCopyrightText: 2018 Intevation GmbH SPDX-License-Identifier: GPL-2.0-only */ #include "kleo/keyresolver.h" #include "utils/formatting.h" #include #include #include #include #include using namespace Kleo; using namespace GpgME; -void dumpKeys(const QMap > > &fmtMap) +void dumpKeys(const QMap> &keysMap) { - for (const Protocol fmt: fmtMap.keys()) { - qDebug () << "Format:" << Formatting::displayName(fmt) << fmt; - for (const auto &mbox: fmtMap[fmt].keys()) { - qDebug() << "Address:" << mbox; - qDebug() << "Keys:"; - for (const auto &key: fmtMap[fmt][mbox]) { - qDebug () << key.primaryFingerprint(); - } + for (auto it = std::begin(keysMap); it != std::end(keysMap); ++it) { + const auto &address = it.key(); + const auto &keys = it.value(); + qDebug() << "Address:" << address; + qDebug() << "Keys:"; + for (const auto &key: keys) { + qDebug() << key.primaryFingerprint(); } } } -void dumpSigKeys(const QMap > &fmtMap) +void dumpSigKeys(const std::vector &keys) { - for (const Protocol fmt: fmtMap.keys()) { - qDebug () << "Format:" << Formatting::displayName(fmt) << fmt; - qDebug() << "Keys:"; - for (const auto &key: fmtMap[fmt]) { - qDebug () << key.primaryFingerprint(); - } + for (const auto &key: keys) { + qDebug() << key.primaryFingerprint(); } } class SignalRecipient: public QObject { Q_OBJECT public: SignalRecipient(KeyResolver *res) : resolver(res) {} void keysResolved(bool success, bool sendUnencrypted) { if (!success) { qDebug() << "Canceled"; exit(1); } + const auto result = resolver->result(); qDebug() << "Resolved Signing keys:"; - dumpSigKeys (resolver->signingKeys()); + dumpSigKeys(result.signingKeys); qDebug() << "Resolved Encryption keys:"; - dumpKeys (resolver->encryptionKeys()); + dumpKeys(result.encryptionKeys); qDebug() << "Send Unencrypted:" << sendUnencrypted; exit(0); } private: KeyResolver *resolver; }; int main(int argc, char **argv) { QApplication app(argc, argv); QCommandLineParser parser; parser.setApplicationDescription(QStringLiteral("Test KeyResolver class")); parser.addHelpOption(); parser.addPositionalArgument(QStringLiteral("recipients"), QStringLiteral("Recipients to resolve"), QStringLiteral("[mailboxes]")); parser.addOption(QCommandLineOption({QStringLiteral("overrides"), QStringLiteral("o")}, QStringLiteral("Override where format can be:\n" "OpenPGP\n" "SMIME\n" "Auto"), QStringLiteral("mailbox:fpr,fpr,...[:format]"))); parser.addOption(QCommandLineOption({QStringLiteral("sender"), QStringLiteral("s")}, QStringLiteral("Mailbox of the sender"), QStringLiteral("mailbox"))); parser.addOption(QCommandLineOption({QStringLiteral("sigkeys"), QStringLiteral("k")}, QStringLiteral("Explicit signing keys"), QStringLiteral("signing key"))); parser.addOption(QCommandLineOption({QStringLiteral("encrypt"), QStringLiteral("e")}, QStringLiteral("Only select encryption keys"))); parser.addOption(QCommandLineOption({QStringLiteral("approval"), QStringLiteral("a")}, QStringLiteral("Always show approval dlg"))); parser.process(app); const QStringList recps = parser.positionalArguments(); if (recps.size() < 1) { parser.showHelp(1); } KeyResolver resolver(true, !parser.isSet(QStringLiteral("encrypt"))); resolver.setRecipients(recps); resolver.setSender(parser.value(QStringLiteral("sender"))); QMap > overrides; for (const QString &oride: parser.values(QStringLiteral("overrides"))) { const QStringList split = oride.split(QLatin1Char(':')); Protocol fmt = UnknownProtocol; if (split.size() < 2 || split.size() > 3) { parser.showHelp(1); } if (split.size() == 3) { const QString fmtStr = split[2].toLower(); if (fmtStr == QLatin1String("openpgp")) { fmt = OpenPGP; } else if (fmtStr == QLatin1String("smime")) { fmt = CMS; } else if (fmtStr == QLatin1String("auto")) { fmt = UnknownProtocol; } else { parser.showHelp(1); } } const QStringList fingerprints = split[1].split(QLatin1Char(',')); auto map = overrides.value(fmt); map.insert(split[0], fingerprints); overrides.insert(fmt, map); } resolver.setOverrideKeys(overrides); auto recp = new SignalRecipient(&resolver); QObject::connect (&resolver, &KeyResolver::keysResolved, recp, &SignalRecipient::keysResolved); QTimer::singleShot(1000, [&parser, &resolver]() { resolver.start(parser.isSet(QStringLiteral("approval"))); }); app.exec(); return 0; } #include "test_keyresolver.moc" diff --git a/src/ui/newkeyapprovaldialog.cpp b/src/ui/newkeyapprovaldialog.cpp index 5698eb6c9..86e3eac32 100644 --- a/src/ui/newkeyapprovaldialog.cpp +++ b/src/ui/newkeyapprovaldialog.cpp @@ -1,833 +1,870 @@ /* -*- c++ -*- newkeyapprovaldialog.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2018 Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include "newkeyapprovaldialog.h" #include "kleo/defaultkeyfilter.h" #include "keyselectioncombo.h" #include "progressdialog.h" #include "utils/formatting.h" #include "libkleo_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace GpgME; +QDebug operator<<(QDebug debug, const GpgME::Key &key) +{ + if (key.isNull()) { + debug << "Null"; + } else { + debug << Formatting::summaryLine(key); + } + return debug.maybeSpace(); +} + namespace { +static std::shared_ptr s_defaultFilter= std::shared_ptr (new DefaultKeyFilter); + class OpenPGPFilter: public DefaultKeyFilter { public: OpenPGPFilter() : DefaultKeyFilter() { setIsOpenPGP(DefaultKeyFilter::Set); setCanEncrypt(DefaultKeyFilter::Set); } }; static std::shared_ptr s_pgpFilter = std::shared_ptr (new OpenPGPFilter); + class OpenPGPSignFilter: public DefaultKeyFilter { public: OpenPGPSignFilter() : DefaultKeyFilter() { /* Also list unusable keys to make it transparent why they are unusable */ setDisabled(DefaultKeyFilter::NotSet); setRevoked(DefaultKeyFilter::NotSet); setExpired(DefaultKeyFilter::NotSet); setCanSign(DefaultKeyFilter::Set); setHasSecret(DefaultKeyFilter::Set); setIsOpenPGP(DefaultKeyFilter::Set); } }; static std::shared_ptr s_pgpSignFilter = std::shared_ptr (new OpenPGPSignFilter); + class SMIMEFilter: public DefaultKeyFilter { public: SMIMEFilter(): DefaultKeyFilter() { setIsOpenPGP(DefaultKeyFilter::NotSet); setCanEncrypt(DefaultKeyFilter::Set); } }; static std::shared_ptr s_smimeFilter = std::shared_ptr (new SMIMEFilter); + class SMIMESignFilter: public DefaultKeyFilter { public: SMIMESignFilter(): DefaultKeyFilter() { setDisabled(DefaultKeyFilter::NotSet); setRevoked(DefaultKeyFilter::NotSet); setExpired(DefaultKeyFilter::NotSet); setCanSign(DefaultKeyFilter::Set); setIsOpenPGP(DefaultKeyFilter::NotSet); setHasSecret(DefaultKeyFilter::Set); } }; static std::shared_ptr s_smimeSignFilter = std::shared_ptr (new SMIMESignFilter); -static std::shared_ptr s_defaultFilter= std::shared_ptr (new DefaultKeyFilter); -class SignFilter: public DefaultKeyFilter -{ -public: - SignFilter(): DefaultKeyFilter() - { - setHasSecret(DefaultKeyFilter::Set); - } -}; -static std::shared_ptr s_signFilter = std::shared_ptr (new SignFilter); /* Some decoration and a button to remove the filter for a keyselectioncombo */ class ComboWidget: public QWidget { Q_OBJECT public: explicit ComboWidget(KeySelectionCombo *combo): mCombo(combo), mFilterBtn(new QPushButton), mFromOverride(GpgME::UnknownProtocol) { auto hLay = new QHBoxLayout(this); auto infoBtn = new QPushButton; infoBtn->setIcon(QIcon::fromTheme(QStringLiteral("help-contextual"))); infoBtn->setIconSize(QSize(22,22)); infoBtn->setFlat(true); hLay->addWidget(infoBtn); hLay->addWidget(combo, 1); hLay->addWidget(mFilterBtn, 0); connect(infoBtn, &QPushButton::clicked, this, [this, infoBtn] () { QToolTip::showText(infoBtn->mapToGlobal(QPoint()) + QPoint(infoBtn->width(), 0), mCombo->currentData(Qt::ToolTipRole).toString(), infoBtn, QRect(), 30000); }); // FIXME: This is ugly to enforce but otherwise the // icon is broken. combo->setMinimumHeight(22); mFilterBtn->setMinimumHeight(23); updateFilterButton(); connect(mFilterBtn, &QPushButton::clicked, this, [this] () { const QString curFilter = mCombo->idFilter(); if (curFilter.isEmpty()) { setIdFilter(mLastIdFilter); mLastIdFilter = QString(); } else { setIdFilter(QString()); mLastIdFilter = curFilter; } }); } void setIdFilter(const QString &id) { mCombo->setIdFilter(id); updateFilterButton(); } void updateFilterButton() { if (mCombo->idFilter().isEmpty()) { mFilterBtn->setIcon(QIcon::fromTheme(QStringLiteral("kt-add-filters"))); mFilterBtn->setToolTip(i18n("Show keys matching the email address")); } else { mFilterBtn->setIcon(QIcon::fromTheme(QStringLiteral("kt-remove-filters"))); mFilterBtn->setToolTip(i18n("Show all keys")); } } KeySelectionCombo *combo() { return mCombo; } GpgME::Protocol fromOverride() const { return mFromOverride; } void setFromOverride(GpgME::Protocol proto) { mFromOverride = proto; } GpgME::Protocol fixedProtocol() const { return mFixedProtocol; } void setFixedProtocol(GpgME::Protocol proto) { mFixedProtocol = proto; } private: KeySelectionCombo *mCombo; QPushButton *mFilterBtn; QString mLastIdFilter; GpgME::Protocol mFromOverride; GpgME::Protocol mFixedProtocol = GpgME::UnknownProtocol; }; static enum GpgME::UserID::Validity keyValidity(const GpgME::Key &key) { enum GpgME::UserID::Validity validity = GpgME::UserID::Validity::Unknown; for (const auto &uid: key.userIDs()) { if (validity == GpgME::UserID::Validity::Unknown || validity > uid.validity()) { validity = uid.validity(); } } return validity; } static bool key_has_addr(const GpgME::Key &key, const QString &addr) { for (const auto &uid: key.userIDs()) { if (QString::fromStdString(uid.addrSpec()).toLower() == addr.toLower()) { return true; } } return false; } +bool anyKeyHasProtocol(const std::vector &keys, Protocol protocol) +{ + return std::any_of(std::begin(keys), std::end(keys), [protocol] (const auto &key) { return key.protocol() == protocol; }); +} + +Key findfirstKeyOfType(const std::vector &keys, Protocol protocol) +{ + const auto it = std::find_if(std::begin(keys), std::end(keys), [protocol] (const auto &key) { return key.protocol() == protocol; }); + return it != std::end(keys) ? *it : Key(); +} + } // namespace class NewKeyApprovalDialog::Private { private: enum Action { Unset, GenerateKey, IgnoreKey, }; public: Private(NewKeyApprovalDialog *qq, + bool encrypt, + bool sign, GpgME::Protocol forcedProtocol, GpgME::Protocol presetProtocol, const QString &sender, bool allowMixed) - : mForcedProtocol(forcedProtocol) - , mPreferredProtocol(presetProtocol) - , mSender(sender) - , mAllowMixed(allowMixed) - , q(qq) + : mForcedProtocol{forcedProtocol} + , mSender{sender} + , mSign{sign} + , mEncrypt{encrypt} + , mAllowMixed{allowMixed} + , q{qq} { Q_ASSERT(forcedProtocol == GpgME::UnknownProtocol || presetProtocol == GpgME::UnknownProtocol || presetProtocol == forcedProtocol); Q_ASSERT(!allowMixed || (allowMixed && forcedProtocol == GpgME::UnknownProtocol)); // We do the translation here to avoid having the same string multiple times. mGenerateTooltip = i18nc("@info:tooltip for a 'Generate new key pair' action " "in a combobox when a user does not yet have an OpenPGP or S/MIME key.", "Generate a new key using your E-Mail address.

" "The key is necessary to decrypt and sign E-Mails. " "You will be asked for a passphrase to protect this key and the protected key " "will be stored in your home directory."); mMainLay = new QVBoxLayout; QDialogButtonBox *btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); mOkButton = btnBox->button(QDialogButtonBox::Ok); QObject::connect (btnBox, &QDialogButtonBox::accepted, q, [this] () { accepted(); }); QObject::connect (btnBox, &QDialogButtonBox::rejected, q, &QDialog::reject); mScrollArea = new QScrollArea; mScrollArea->setWidget(new QWidget); mScrollLayout = new QVBoxLayout; mScrollArea->widget()->setLayout(mScrollLayout); mScrollArea->setWidgetResizable(true); mScrollArea->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContentsOnFirstShow); mScrollArea->setFrameStyle(QFrame::NoFrame); mScrollLayout->setContentsMargins(0, 0, 0, 0); q->setWindowTitle(i18nc("@title:window", "Security approval")); auto fmtLayout = new QHBoxLayout; mFormatBtns = new QButtonGroup; auto pgpBtn = new QRadioButton(i18n("OpenPGP")); auto smimeBtn = new QRadioButton(i18n("S/MIME")); mFormatBtns->addButton(pgpBtn, 1); mFormatBtns->addButton(smimeBtn, 2); mFormatBtns->setExclusive(true); fmtLayout->addStretch(-1); fmtLayout->addWidget(pgpBtn); fmtLayout->addWidget(smimeBtn); mMainLay->addLayout(fmtLayout); - // Handle force / preset - if (mForcedProtocol != GpgME::UnknownProtocol) { - mPreferredProtocol = mForcedProtocol; - } - if (mPreferredProtocol == GpgME::UnknownProtocol) { - mPreferredProtocol = GpgME::OpenPGP; - } if (mAllowMixed) { smimeBtn->setVisible(false); pgpBtn->setVisible(false); } else if (mForcedProtocol != GpgME::UnknownProtocol) { pgpBtn->setChecked(mForcedProtocol == GpgME::OpenPGP); smimeBtn->setChecked(mForcedProtocol == GpgME::CMS); pgpBtn->setVisible(false); smimeBtn->setVisible(false); } else { - pgpBtn->setChecked(mPreferredProtocol == GpgME::OpenPGP); - smimeBtn->setChecked(mPreferredProtocol == GpgME::CMS); + pgpBtn->setChecked(presetProtocol == GpgME::OpenPGP); + smimeBtn->setChecked(presetProtocol == GpgME::CMS); } - updateFilter(); - - QObject::connect (mFormatBtns, static_cast (&QButtonGroup::buttonToggled), q, [this](QAbstractButton *, bool) { - updateFilter(); + updateWidgetVisibility(); }); mMainLay->addWidget(mScrollArea); mComplianceLbl = new QLabel; mComplianceLbl->setVisible(false); auto btnLayout = new QHBoxLayout; btnLayout->addWidget(mComplianceLbl); btnLayout->addWidget(btnBox); mMainLay->addLayout(btnLayout); q->setLayout(mMainLay); } ~Private() = default; Protocol currentProtocol() { - if (!mAllowMixed && mFormatBtns->checkedId() == 1) { + if (mAllowMixed) { + return UnknownProtocol; + } else if (mFormatBtns->checkedId() == 1) { return OpenPGP; - } else if (!mAllowMixed && mFormatBtns->checkedId() == 2) { + } else if (mFormatBtns->checkedId() == 2) { return CMS; } return UnknownProtocol; } void generateKey(KeySelectionCombo *combo) { const auto &addr = combo->property("address").toString(); auto job = new QGpgME::DefaultKeyGenerationJob(q); auto progress = new Kleo::ProgressDialog(job, i18n("Generating key for '%1'...", addr) + QStringLiteral("\n\n") + i18n("This can take several minutes."), q); progress->setWindowFlags(progress->windowFlags() & ~Qt::WindowContextHelpButtonHint); progress->setWindowTitle(i18nc("@title:window", "Key generation")); progress->setModal(true); progress->setAutoClose(true); progress->setMinimumDuration(0); progress->setValue(0); mRunningJobs << job; connect (job, &QGpgME::DefaultKeyGenerationJob::result, q, [this, job, combo] (const GpgME::KeyGenerationResult &result) { handleKeyGenResult(result, job, combo); }); job->start(addr, QString()); return; } void handleKeyGenResult(const GpgME::KeyGenerationResult &result, QGpgME::Job *job, KeySelectionCombo *combo) { mLastError = result.error(); if (!mLastError || mLastError.isCanceled()) { combo->setDefaultKey(QString::fromLatin1(result.fingerprint()), GpgME::OpenPGP); connect (combo, &KeySelectionCombo::keyListingFinished, q, [this, job] () { mRunningJobs.removeAll(job); }); combo->refreshKeys(); } else { mRunningJobs.removeAll(job); } } void checkAccepted() { if (mLastError || mLastError.isCanceled()) { KMessageBox::error(q, QString::fromLocal8Bit(mLastError.asString()), i18n("Operation Failed")); mRunningJobs.clear(); return; } if (!mRunningJobs.empty()) { return; } /* Save the keys */ const Protocol protocol = currentProtocol(); - mAcceptedEnc.clear(); - mAcceptedSig.clear(); + mAcceptedResult.encryptionKeys.clear(); + mAcceptedResult.signingKeys.clear(); for (const auto combo: qAsConst(mEncCombos)) { const auto &addr = combo->property("address").toString(); const auto &key = combo->currentKey(); if (!combo->isVisible()) { continue; } if (protocol != UnknownProtocol && key.protocol() != protocol) { continue; } - if (mAcceptedEnc.contains(addr)) { - mAcceptedEnc[addr].push_back(key); - } else { - std::vector vec; - vec.push_back(key); - mAcceptedEnc.insert(addr, vec); - } + mAcceptedResult.encryptionKeys[addr].push_back(key); } for (const auto combo: qAsConst(mSigningCombos)) { const auto key = combo->currentKey(); if (!combo->isVisible()) { continue; } if (protocol != UnknownProtocol && key.protocol() != protocol) { continue; } - mAcceptedSig.push_back(combo->currentKey()); + mAcceptedResult.signingKeys.push_back(key); } q->accept(); } void accepted() { // We can assume everything was validly resolved, otherwise // the OK button would have been disabled. // Handle custom items now. for (auto combo: qAsConst(mAllCombos)) { auto act = combo->currentData(Qt::UserRole).toInt(); if (act == GenerateKey) { generateKey(combo); // Only generate once return; } } checkAccepted(); } - void updateFilter() + void updateWidgetVisibility() { const Protocol protocol = currentProtocol(); - switch (protocol) { - case CMS: - mCurEncFilter = s_smimeFilter; - mCurSigFilter = s_smimeSignFilter; - break; - case OpenPGP: - mCurEncFilter = s_pgpFilter; - mCurSigFilter = s_pgpSignFilter; - break; - default: - mCurEncFilter = s_defaultFilter; - mCurSigFilter = s_signFilter; - } for (auto combo: qAsConst(mSigningCombos)) { auto widget = qobject_cast(combo->parentWidget()); if (!widget) { qCDebug(LIBKLEO_LOG) << "Failed to find signature combo widget"; continue; } - if (widget->fixedProtocol() == GpgME::UnknownProtocol) { - combo->setKeyFilter(mCurSigFilter); - } - widget->setVisible(mAllowMixed || - widget->fromOverride() == GpgME::UnknownProtocol || - (protocol != UnknownProtocol && widget->fromOverride() == protocol)); + widget->setVisible(protocol == UnknownProtocol || widget->fixedProtocol() == protocol); } for (auto combo: qAsConst(mEncCombos)) { auto widget = qobject_cast(combo->parentWidget()); if (!widget) { qCDebug(LIBKLEO_LOG) << "Failed to find combo widget"; continue; } - if (widget->fixedProtocol() == GpgME::UnknownProtocol) { - combo->setKeyFilter(mCurEncFilter); - } - widget->setVisible(mAllowMixed || - widget->fromOverride() == GpgME::UnknownProtocol || - (protocol != UnknownProtocol && widget->fromOverride() == protocol)); + widget->setVisible(protocol == UnknownProtocol || widget->fixedProtocol() == protocol); } } - ComboWidget *createSigningCombo(const QString &addr, const GpgME::Key &key) + ComboWidget *createSigningCombo(const QString &addr, const GpgME::Key &key, Protocol protocol = UnknownProtocol) { + Q_ASSERT(!key.isNull() || protocol != UnknownProtocol); + protocol = !key.isNull() ? key.protocol() : protocol; + auto combo = new KeySelectionCombo(); auto comboWidget = new ComboWidget(combo); #ifndef NDEBUG combo->setObjectName(QStringLiteral("signing key")); #endif - if (!key.isNull()) { - if (mAllowMixed) { - if (key.protocol() == GpgME::OpenPGP) { - combo->setKeyFilter(s_pgpSignFilter); - } else if (key.protocol() == GpgME::CMS) { - combo->setKeyFilter(s_smimeSignFilter); - } - comboWidget->setFixedProtocol(key.protocol()); - } - combo->setDefaultKey(QString::fromLatin1(key.primaryFingerprint()), key.protocol()); + if (protocol == GpgME::OpenPGP) { + combo->setKeyFilter(s_pgpSignFilter); + } else if (protocol == GpgME::CMS) { + combo->setKeyFilter(s_smimeSignFilter); } - if (!combo->keyFilter()) { - combo->setKeyFilter(mCurSigFilter); + if (key.isNull() || key_has_addr(key, mSender)) { + comboWidget->setIdFilter(mSender); + } + comboWidget->setFixedProtocol(protocol); + if (!key.isNull()) { + combo->setDefaultKey(QString::fromLatin1(key.primaryFingerprint()), protocol); } - if (key.isNull() && mForcedProtocol != GpgME::CMS) { + if (key.isNull() && protocol == OpenPGP) { combo->appendCustomItem(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Generate a new key pair"), GenerateKey, mGenerateTooltip); } combo->appendCustomItem(QIcon::fromTheme(QStringLiteral("emblem-unavailable")), i18n("Don't confirm identity and integrity"), IgnoreKey, i18nc("@info:tooltip for not selecting a key for signing.", "The E-Mail will not be cryptographically signed.")); mSigningCombos << combo; mAllCombos << combo; combo->setProperty("address", addr); connect(combo, &KeySelectionCombo::currentKeyChanged, q, [this] () { updateOkButton(); }); connect(combo, QOverload::of(&QComboBox::currentIndexChanged), q, [this] () { updateOkButton(); }); return comboWidget; } - void addSigningKeys(const QMap > &resolved, - const QStringList &unresolved) + void setSigningKeys(std::vector preferredKeys, std::vector alternativeKeys) { - if (resolved.empty() && unresolved.empty()) { - return; + auto group = new QGroupBox(i18nc("Caption for signing key selection", "Signing keys for '%1", mSender)); + group->setAlignment(Qt::AlignLeft); + auto sigLayout = new QVBoxLayout(group); + + const bool mayNeedOpenPGP = mForcedProtocol != CMS; + const bool mayNeedCMS = mForcedProtocol != OpenPGP; + if (mayNeedOpenPGP) { + if (mAllowMixed) { + sigLayout->addWidget(new QLabel(Formatting::displayName(OpenPGP))); + } + const Key preferredKey = findfirstKeyOfType(preferredKeys, OpenPGP); + const Key alternativeKey = findfirstKeyOfType(alternativeKeys, OpenPGP); + if (!preferredKey.isNull()) { + qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for" << preferredKey; + auto comboWidget = createSigningCombo(mSender, preferredKey); + sigLayout->addWidget(comboWidget); + } else if (!alternativeKey.isNull()) { + qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for" << alternativeKey; + auto comboWidget = createSigningCombo(mSender, alternativeKey); + sigLayout->addWidget(comboWidget); + } else { + qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for OpenPGP key"; + auto comboWidget = createSigningCombo(mSender, Key(), OpenPGP); + sigLayout->addWidget(comboWidget); + } } - for (const QString &addr: resolved.keys()) { - auto group = new QGroupBox(i18nc("Caption for signing key selection", - "Confirm identity '%1' as:", addr)); - group->setAlignment(Qt::AlignLeft); - mScrollLayout->addWidget(group); - auto sigLayout = new QVBoxLayout; - - group->setLayout(sigLayout); - for (const auto &key: resolved[addr]) - { - auto comboWidget = createSigningCombo(addr, key); - if (key_has_addr (key, addr)) { - comboWidget->setIdFilter(addr); - } - if (resolved[addr].size() > 1) { - comboWidget->setFromOverride(key.protocol()); - } + if (mayNeedCMS) { + if (mAllowMixed) { + sigLayout->addWidget(new QLabel(Formatting::displayName(CMS))); + } + const Key preferredKey = findfirstKeyOfType(preferredKeys, CMS); + const Key alternativeKey = findfirstKeyOfType(alternativeKeys, CMS); + if (!preferredKey.isNull()) { + qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for" << preferredKey; + auto comboWidget = createSigningCombo(mSender, preferredKey); + sigLayout->addWidget(comboWidget); + } else if (!alternativeKey.isNull()) { + qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for" << alternativeKey; + auto comboWidget = createSigningCombo(mSender, alternativeKey); + sigLayout->addWidget(comboWidget); + } else { + qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for S/MIME key"; + auto comboWidget = createSigningCombo(mSender, Key(), CMS); sigLayout->addWidget(comboWidget); } } - for (const QString &addr: qAsConst(unresolved)) { - auto group = new QGroupBox(i18nc("Caption for signing key selection, no key found", - "No key found for the address '%1':", addr)); - group->setAlignment(Qt::AlignLeft); - mScrollLayout->addWidget(group); - auto sigLayout = new QHBoxLayout; - group->setLayout(sigLayout); - - auto comboWidget = createSigningCombo(addr, GpgME::Key()); - comboWidget->setIdFilter(addr); - sigLayout->addWidget(comboWidget); - } + mScrollLayout->addWidget(group); } - ComboWidget *createEncryptionCombo(const QString &addr, const GpgME::Key &key) + ComboWidget *createEncryptionCombo(const QString &addr, const GpgME::Key &key, Protocol fixedProtocol) { auto combo = new KeySelectionCombo(false); auto comboWidget = new ComboWidget(combo); #ifndef NDEBUG combo->setObjectName(QStringLiteral("encryption key")); #endif - combo->setKeyFilter(mCurEncFilter); + if (fixedProtocol == GpgME::OpenPGP) { + combo->setKeyFilter(s_pgpFilter); + } else if (fixedProtocol == GpgME::CMS) { + combo->setKeyFilter(s_smimeFilter); + } else { + combo->setKeyFilter(s_defaultFilter); + } + if (key.isNull() || key_has_addr (key, addr)) { + comboWidget->setIdFilter(addr); + } + comboWidget->setFixedProtocol(fixedProtocol); if (!key.isNull()) { - if (mAllowMixed && addr == mSender) { - if (key.protocol() == GpgME::OpenPGP) { - combo->setKeyFilter(s_pgpFilter); - } else if (key.protocol() == GpgME::CMS) { - combo->setKeyFilter(s_smimeFilter); - } - comboWidget->setFixedProtocol(key.protocol()); - } - if (mAllowMixed && addr != mSender) { - // do not set the key for a specific protocol if mixed protocols are allowed - combo->setDefaultKey(QString::fromLatin1(key.primaryFingerprint())); - } - else { - combo->setDefaultKey(QString::fromLatin1(key.primaryFingerprint()), key.protocol()); - } + combo->setDefaultKey(QString::fromLatin1(key.primaryFingerprint()), fixedProtocol); } - if (!combo->keyFilter()) { - combo->setKeyFilter(mCurEncFilter); - } - if (mSender == addr && key.isNull()) { + if (addr == mSender && key.isNull() && fixedProtocol == OpenPGP) { combo->appendCustomItem(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Generate a new key pair"), GenerateKey, mGenerateTooltip); } combo->appendCustomItem(QIcon::fromTheme(QStringLiteral("emblem-unavailable")), i18n("No key. Recipient will be unable to decrypt."), IgnoreKey, i18nc("@info:tooltip for No Key selected for a specific recipient.", "Do not select a key for this recipient.

" "The recipient will receive the encrypted E-Mail, but it can only " "be decrypted with the other keys selected in this dialog.")); - if (key.isNull() || key_has_addr (key, addr)) { - comboWidget->setIdFilter(addr); - } - connect(combo, &KeySelectionCombo::currentKeyChanged, q, [this] () { updateOkButton(); }); connect(combo, QOverload::of(&QComboBox::currentIndexChanged), q, [this] () { updateOkButton(); }); mEncCombos << combo; mAllCombos << combo; combo->setProperty("address", addr); return comboWidget; } - void addEncryptionAddr(const QString &addr, const std::vector &keys, + void addEncryptionAddr(const QString &addr, + Protocol preferredKeysProtocol, const std::vector &preferredKeys, + Protocol alternativeKeysProtocol, const std::vector &alternativeKeys, QGridLayout *encGrid) { - if (addr != mSender) { - encGrid->addWidget(new QLabel(addr), encGrid->rowCount(), 0); - } - if (keys.empty()) { - ComboWidget* comboWidget = createEncryptionCombo(addr, GpgME::Key()); - encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); - } else { - // in mixed mode prefer the keys with the preferred protocol for the other recipients - std::vector preferredKeys; - if (mAllowMixed && addr != mSender) { - std::copy_if(keys.cbegin(), keys.cend(), - std::back_inserter(preferredKeys), - [this] (const auto &key) { return key.protocol() == mPreferredProtocol; }); + if (addr == mSender) { + const bool mayNeedOpenPGP = mForcedProtocol != CMS; + const bool mayNeedCMS = mForcedProtocol != OpenPGP; + if (mayNeedOpenPGP) { + if (mAllowMixed) { + encGrid->addWidget(new QLabel(Formatting::displayName(OpenPGP)), encGrid->rowCount(), 0); + } + for (const auto &key : preferredKeys) { + if (key.protocol() == OpenPGP) { + qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key; + auto comboWidget = createEncryptionCombo(addr, key, OpenPGP); + encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); + } + } + for (const auto &key : alternativeKeys) { + if (key.protocol() == OpenPGP) { + qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key; + auto comboWidget = createEncryptionCombo(addr, key, OpenPGP); + encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); + } + } + if (!anyKeyHasProtocol(preferredKeys, OpenPGP) && !anyKeyHasProtocol(alternativeKeys, OpenPGP)) { + qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for OpenPGP key"; + auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), OpenPGP); + encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); + } } - const std::vector &encryptionKeys = !preferredKeys.empty() ? preferredKeys : keys; - for (const auto &key: encryptionKeys) { - ComboWidget* comboWidget = createEncryptionCombo(addr, key); - if (keys.size() > 1) { - comboWidget->setFromOverride(key.protocol()); + if (mayNeedCMS) { + if (mAllowMixed) { + encGrid->addWidget(new QLabel(Formatting::displayName(CMS)), encGrid->rowCount(), 0); + } + for (const auto &key : preferredKeys) { + if (key.protocol() == CMS) { + qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key; + auto comboWidget = createEncryptionCombo(addr, key, CMS); + encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); + } + } + for (const auto &key : alternativeKeys) { + if (key.protocol() == CMS) { + qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key; + auto comboWidget = createEncryptionCombo(addr, key, CMS); + encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); + } + } + if (!anyKeyHasProtocol(preferredKeys, CMS) && !anyKeyHasProtocol(alternativeKeys, CMS)) { + qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for S/MIME key"; + auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), CMS); + encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } + } + } else { + encGrid->addWidget(new QLabel(addr), encGrid->rowCount(), 0); + + for (const auto &key : preferredKeys) { + qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key; + auto comboWidget = createEncryptionCombo(addr, key, preferredKeysProtocol); + encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); + } + for (const auto &key : alternativeKeys) { + qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key; + auto comboWidget = createEncryptionCombo(addr, key, alternativeKeysProtocol); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } + if (!mAllowMixed) { + if (preferredKeys.empty()) { + qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << Formatting::displayName(preferredKeysProtocol) << "key"; + auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), preferredKeysProtocol); + encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); + } + if (alternativeKeys.empty() && alternativeKeysProtocol != UnknownProtocol) { + qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << Formatting::displayName(alternativeKeysProtocol) << "key"; + auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), alternativeKeysProtocol); + encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); + } + } else { + if (preferredKeys.empty() && alternativeKeys.empty()) { + qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for any key"; + auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), UnknownProtocol); + encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); + } + } } } - void addEncryptionKeys(const QMap > &resolved, - const QStringList &unresolved) + void setEncryptionKeys(Protocol preferredKeysProtocol, const QMap> &preferredKeys, + Protocol alternativeKeysProtocol, const QMap> &alternativeKeys) { - if (resolved.empty() && unresolved.empty()) { - return; - } - { auto group = new QGroupBox(i18nc("Encrypt to self (email address):", "Encrypt to self (%1):", mSender)); group->setAlignment(Qt::AlignLeft); - auto encGrid = new QGridLayout; - group->setLayout(encGrid); + auto encGrid = new QGridLayout(group); + + addEncryptionAddr(mSender, preferredKeysProtocol, preferredKeys.value(mSender), alternativeKeysProtocol, alternativeKeys.value(mSender), encGrid); + mScrollLayout->addWidget(group); - addEncryptionAddr(mSender, resolved.value(mSender), encGrid); } auto group = new QGroupBox(i18n("Encrypt to others:")); group->setAlignment(Qt::AlignLeft); auto encGrid = new QGridLayout; group->setLayout(encGrid); mScrollLayout->addWidget(group); - for (const QString &addr: resolved.keys()) { - if (addr != mSender) { - addEncryptionAddr(addr, resolved[addr], encGrid); - } - } - for (const QString &addr: unresolved) { - if (addr != mSender) { - addEncryptionAddr(addr, {}, encGrid); + for (auto it = std::begin(preferredKeys); it != std::end(preferredKeys); ++it) { + const auto &address = it.key(); + const auto &keys = it.value(); + if (address != mSender) { + addEncryptionAddr(address, preferredKeysProtocol, keys, alternativeKeysProtocol, alternativeKeys.value(address), encGrid); } } encGrid->setColumnStretch(1, -1); mScrollLayout->addStretch(-1); } void updateOkButton() { static QString origOkText = mOkButton->text(); bool isGenerate = false; bool isAllIgnored = true; // Check if generate is selected. for (auto combo: mAllCombos) { auto act = combo->currentData(Qt::UserRole).toInt(); if (act == GenerateKey) { mOkButton->setText(i18n("Generate")); isGenerate = true; } if (act != IgnoreKey) { isAllIgnored = false; } } // If we don't encrypt the ok button is always enabled. But otherwise // we only enable it if we encrypt to at least one recipient. - if (!mEncCombos.size()) { + if (!mEncrypt) { mOkButton->setEnabled(true); } else { mOkButton->setEnabled(!isAllIgnored); } if (!isGenerate) { mOkButton->setText(origOkText); } if (Formatting::complianceMode() != QLatin1String("de-vs")) { return; } // Handle compliance bool de_vs = true; const Protocol protocol = currentProtocol(); for (const auto combo: qAsConst(mEncCombos)) { const auto &key = combo->currentKey(); if (!combo->isVisible()) { continue; } if (protocol != UnknownProtocol && key.protocol() != protocol) { continue; } if (!Formatting::isKeyDeVs(key) || keyValidity(key) < GpgME::UserID::Validity::Full) { de_vs = false; break; } } if (de_vs) { for (const auto combo: qAsConst(mSigningCombos)) { const auto key = combo->currentKey(); if (!combo->isVisible()) { continue; } if (protocol != UnknownProtocol && key.protocol() != protocol) { continue; } if (!Formatting::isKeyDeVs(key) || keyValidity(key) < GpgME::UserID::Validity::Full) { de_vs = false; break; } } } mOkButton->setIcon(QIcon::fromTheme(de_vs ? QStringLiteral("security-high") : QStringLiteral("security-medium"))); mOkButton->setStyleSheet(QStringLiteral("background-color: ") + (de_vs ? QStringLiteral("#D5FAE2") // KColorScheme(QPalette::Active, KColorScheme::View).background(KColorScheme::PositiveBackground).color().name() : QStringLiteral("#FAE9EB"))); //KColorScheme(QPalette::Active, KColorScheme::View).background(KColorScheme::NegativeBackground).color().name())); mComplianceLbl->setText(de_vs ? i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "%1 communication possible.", Formatting::deVsString()) : i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "%1 communication not possible.", Formatting::deVsString())); mComplianceLbl->setVisible(true); } GpgME::Protocol mForcedProtocol; - GpgME::Protocol mPreferredProtocol; QList mSigningCombos; QList mEncCombos; QList mAllCombos; QScrollArea *mScrollArea; QVBoxLayout *mScrollLayout; QPushButton *mOkButton; QVBoxLayout *mMainLay; QButtonGroup *mFormatBtns; - std::shared_ptr mCurSigFilter; - std::shared_ptr mCurEncFilter; QString mSender; + bool mSign; + bool mEncrypt; bool mAllowMixed; NewKeyApprovalDialog *q; QList mRunningJobs; GpgME::Error mLastError; QLabel *mComplianceLbl; - QMap > mAcceptedEnc; - std::vector mAcceptedSig; + KeyResolver::Solution mAcceptedResult; QString mGenerateTooltip; }; -NewKeyApprovalDialog::NewKeyApprovalDialog(const QMap > &resolvedSigningKeys, - const QMap > &resolvedRecp, - const QStringList &unresolvedSigKeys, - const QStringList &unresolvedRecp, +NewKeyApprovalDialog::NewKeyApprovalDialog(bool encrypt, + bool sign, const QString &sender, + KeyResolver::Solution preferredSolution, + KeyResolver::Solution alternativeSolution, bool allowMixed, GpgME::Protocol forcedProtocol, - GpgME::Protocol presetProtocol, QWidget *parent, - Qt::WindowFlags f): QDialog(parent, f), - d(new Private(this, forcedProtocol, presetProtocol, sender, allowMixed)) + Qt::WindowFlags f) + : QDialog(parent, f) + , d{std::make_unique(this, encrypt, sign, forcedProtocol, preferredSolution.protocol, sender, allowMixed)} { - d->addSigningKeys(resolvedSigningKeys, unresolvedSigKeys); - d->addEncryptionKeys(resolvedRecp, unresolvedRecp); - d->updateFilter(); + if (sign) { + d->setSigningKeys(std::move(preferredSolution.signingKeys), std::move(alternativeSolution.signingKeys)); + } + if (encrypt) { + d->setEncryptionKeys(preferredSolution.protocol, std::move(preferredSolution.encryptionKeys), + alternativeSolution.protocol, std::move(alternativeSolution.encryptionKeys)); + } + d->updateWidgetVisibility(); d->updateOkButton(); const auto size = sizeHint(); const auto desk = QApplication::desktop()->screenGeometry(this); resize(QSize(desk.width() / 3, qMin(size.height(), desk.height() / 2))); } Kleo::NewKeyApprovalDialog::~NewKeyApprovalDialog() = default; -std::vector NewKeyApprovalDialog::signingKeys() -{ - return d->mAcceptedSig; -} - -QMap > NewKeyApprovalDialog::encryptionKeys() +KeyResolver::Solution NewKeyApprovalDialog::result() { - return d->mAcceptedEnc; + return d->mAcceptedResult; } #include "newkeyapprovaldialog.moc" diff --git a/src/ui/newkeyapprovaldialog.h b/src/ui/newkeyapprovaldialog.h index e185223ba..8571ba629 100644 --- a/src/ui/newkeyapprovaldialog.h +++ b/src/ui/newkeyapprovaldialog.h @@ -1,90 +1,80 @@ /* -*- c++ -*- newkeyapprovaldialog.h This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2018 Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once +#include + #include "kleo_export.h" #include #include #include namespace Kleo { /** @brief A dialog to show for encryption / signing key approval or selection. * * This class is intended to replace the old KeyApprovalDialog with a new * and simpler interface. * * Resolved recipients in this API means a recipient could be resolved * to a single useful key. An unresolved recipient is a recipient for * whom no key could be found. Import / Search will be offered for such * a recipient. Multiple keys for signing / recipient can come e.g. from * group configuration or Addressbook / Identity configuration. * * The Dialog uses the Level System for validity display and shows an * overall outgoing level. * */ class KLEO_EXPORT NewKeyApprovalDialog : public QDialog { Q_OBJECT public: /** @brief Create a new Key Approval Dialog. * - * @param resolvedSigningKeys: A map of signing addresses and Keys. Usually the - * map would contain a single element and a single key - * but configuration may allow more. - * @param resolvedRecp: A map of a recipient address and the keys for that address. Multiple - * keys could for example be configured through Address book or GnuPG - * Groups. - * @param unresolvedSigKeys: A list of signing addresses for which no key was found. Should - * usually be only one. If resolved and unresolved sig keys are - * empty it is assumed signing was not selected. - * @param unresolvedRecp: A list of encryption target addresses if both unresolved and - * resolved recipients are empty it is assumed no encryption should - * take place. - * @param senderAddr: The address of the sender, this may be used if singing is not - * specified to identify a recipient for which "Generate Key" should - * be offered. - * @param allowMixed: Whether or not the dialog should allow mixed CMS / PGP key selection. + * @param sender: The address of the sender, this may be used if signing is not + * specified to identify a recipient for which "Generate Key" should + * be offered. + * @param preferredSolution: The preferred signing and/or encryption keys for the sender + * and the recipients. + * @param alternativeSolution: An alternative set of signing and/or encryption keys for the sender + * and the recipients. Typically, S/MIME-only, if preferred solution is OpenPGP-only, + * and vice versa. Ignored, if mixed protocol selection is allowed. + * @param allowMixed: Whether or not the dialog should allow mixed S/MIME / OpenPGP key selection. * @param forcedProtocol: A specific forced protocol. - * @param presetProtocol: A specific preselected protocol. If Protocol is unknown it will allow - * both (depending on allowMixed) S/MIME and OpenPGP. * @param parent: The parent widget. * @param f: The Qt window flags. */ - explicit NewKeyApprovalDialog(const QMap > &resolvedSigningKeys, - const QMap > &resolvedRecp, - const QStringList &unresolvedSigKeys, - const QStringList &unresolvedRecp, - const QString &senderAddr, + explicit NewKeyApprovalDialog(bool encrypt, + bool sign, + const QString &sender, + KeyResolver::Solution preferredSolution, + KeyResolver::Solution alternativeSolution, bool allowMixed, GpgME::Protocol forcedProtocol, - GpgME::Protocol presetProtocol, QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); ~NewKeyApprovalDialog() override; - /** @brief The selected signing Keys. Only valid after Dialog was accepted. */ - std::vector signingKeys(); - /** @brief The selected encryption Keys. Only valid after Dialog was accepted. */ - QMap > encryptionKeys(); + /** @brief The selected signing and/or encryption keys. Only valid after the dialog was accepted. */ + KeyResolver::Solution result(); private: class Private; std::unique_ptr d; }; } // namespace kleo