diff --git a/src/commands/certificatetopivcardcommand.cpp b/src/commands/certificatetopivcardcommand.cpp index be0fbd267..d5e48f297 100644 --- a/src/commands/certificatetopivcardcommand.cpp +++ b/src/commands/certificatetopivcardcommand.cpp @@ -1,260 +1,274 @@ /* commands/certificatetopivcardcommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2020 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "certificatetopivcardcommand.h" #include "cardcommand_p.h" #include "commands/authenticatepivcardapplicationcommand.h" #include "smartcard/pivcard.h" #include "smartcard/readerstatus.h" #include "utils/writecertassuantransaction.h" #include #include #include #include #include #include #include #if GPG_ERROR_VERSION_NUMBER >= 0x12400 // 1.36 # define GPG_ERROR_HAS_NO_AUTH #endif #include "kleopatra_debug.h" +#include using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::SmartCard; using namespace GpgME; class CertificateToPIVCardCommand::Private : public CardCommand::Private { friend class ::Kleo::Commands::CertificateToPIVCardCommand; CertificateToPIVCardCommand *q_func() const { return static_cast(q); } public: explicit Private(CertificateToPIVCardCommand *qq, const std::string &slot, const std::string &serialno); ~Private() override; private: void start(); void startCertificateToPIVCard(); void authenticate(); void authenticationFinished(); void authenticationCanceled(); private: std::string cardSlot; Key certificate; bool hasBeenCanceled = false; }; CertificateToPIVCardCommand::Private *CertificateToPIVCardCommand::d_func() { return static_cast(d.get()); } const CertificateToPIVCardCommand::Private *CertificateToPIVCardCommand::d_func() const { return static_cast(d.get()); } #define q q_func() #define d d_func() CertificateToPIVCardCommand::Private::Private(CertificateToPIVCardCommand *qq, const std::string &slot, const std::string &serialno) : CardCommand::Private(qq, serialno, nullptr) , cardSlot(slot) { } CertificateToPIVCardCommand::Private::~Private() { } namespace { static Key getCertificateToWriteToPIVCard(const std::string &cardSlot, const std::shared_ptr &card) { if (!cardSlot.empty()) { const std::string cardKeygrip = card->keyInfo(cardSlot).grip; const auto certificate = KeyCache::instance()->findSubkeyByKeyGrip(cardKeygrip).parent(); if (certificate.isNull() || certificate.protocol() != GpgME::CMS) { return Key(); } if ((cardSlot == PIVCard::pivAuthenticationKeyRef() && certificate.canSign()) || (cardSlot == PIVCard::cardAuthenticationKeyRef() && certificate.canSign()) || (cardSlot == PIVCard::digitalSignatureKeyRef() && certificate.canSign()) || (cardSlot == PIVCard::keyManagementKeyRef() && certificate.canEncrypt())) { return certificate; } } return Key(); } } void CertificateToPIVCardCommand::Private::start() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::Private::start()"; const auto pivCard = SmartCard::ReaderStatus::instance()->getCard(serialNumber()); if (!pivCard) { error(i18n("Failed to find the PIV card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } certificate = getCertificateToWriteToPIVCard(cardSlot, pivCard); if (certificate.isNull()) { error(i18n("Sorry! No suitable certificate to write to this card slot was found.")); finished(); return; } const QString certificateInfo = i18nc("X.509 certificate DN (validity, created: date)", "%1 (%2, created: %3)", DN(certificate.userID(0).id()).prettyDN(), Formatting::complianceStringShort(certificate), Formatting::creationDateString(certificate)); const QString message = i18nc( "@info %1 name of card slot, %2 serial number of card", "

Please confirm that you want to write the following certificate to the %1 slot of card %2:

" "
%3
", PIVCard::keyDisplayName(cardSlot), QString::fromStdString(serialNumber()), certificateInfo); auto confirmButton = KStandardGuiItem::yes(); confirmButton.setText(i18nc("@action:button", "Write certificate")); confirmButton.setToolTip(QString()); +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + const auto choice = KMessageBox::questionTwoActions( +#else const auto choice = KMessageBox::questionYesNo( - parentWidgetOrView(), message, i18nc("@title:window", "Write certificate to card"), - confirmButton, KStandardGuiItem::cancel(), QString(), KMessageBox::Notify | KMessageBox::WindowModal); +#endif + parentWidgetOrView(), + message, + i18nc("@title:window", "Write certificate to card"), + confirmButton, + KStandardGuiItem::cancel(), + QString(), + KMessageBox::Notify | KMessageBox::WindowModal); +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + if (choice != KMessageBox::ButtonCode::PrimaryAction) { +#else if (choice != KMessageBox::Yes) { +#endif finished(); return; } startCertificateToPIVCard(); } void CertificateToPIVCardCommand::Private::startCertificateToPIVCard() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::Private::startCertificateToPIVCard()"; auto ctx = Context::createForProtocol(GpgME::CMS); QGpgME::QByteArrayDataProvider dp; Data data(&dp); const Error err = ctx->exportPublicKeys(certificate.primaryFingerprint(), data); if (err) { error(i18nc("@info", "Exporting the certificate failed: %1", QString::fromUtf8(err.asString()))); finished(); return; } const QByteArray certificateData = dp.data(); const auto pivCard = SmartCard::ReaderStatus::instance()->getCard(serialNumber()); if (!pivCard) { error(i18n("Failed to find the PIV card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } const QByteArray command = QByteArrayLiteral("SCD WRITECERT ") + QByteArray::fromStdString(cardSlot); auto transaction = std::unique_ptr(new WriteCertAssuanTransaction(certificateData)); ReaderStatus::mutableInstance()->startTransaction( pivCard, command, q_func(), [this](const GpgME::Error &err) { q->certificateToPIVCardDone(err); }, std::move(transaction)); } void CertificateToPIVCardCommand::Private::authenticate() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::authenticate()"; auto cmd = new AuthenticatePIVCardApplicationCommand(serialNumber(), parentWidgetOrView()); connect(cmd, &AuthenticatePIVCardApplicationCommand::finished, q, [this]() { authenticationFinished(); }); connect(cmd, &AuthenticatePIVCardApplicationCommand::canceled, q, [this]() { authenticationCanceled(); }); cmd->start(); } void CertificateToPIVCardCommand::Private::authenticationFinished() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::authenticationFinished()"; if (!hasBeenCanceled) { startCertificateToPIVCard(); } } void CertificateToPIVCardCommand::Private::authenticationCanceled() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::authenticationCanceled()"; hasBeenCanceled = true; canceled(); } CertificateToPIVCardCommand::CertificateToPIVCardCommand(const std::string& cardSlot, const std::string &serialno) : CardCommand(new Private(this, cardSlot, serialno)) { } CertificateToPIVCardCommand::~CertificateToPIVCardCommand() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::~CertificateToPIVCardCommand()"; } void CertificateToPIVCardCommand::certificateToPIVCardDone(const Error &err) { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::certificateToPIVCardDone():" << err.asString() << "(" << err.code() << ")"; if (err) { #ifdef GPG_ERROR_HAS_NO_AUTH // gpgme 1.13 reports "BAD PIN" instead of "NO AUTH" if (err.code() == GPG_ERR_NO_AUTH || err.code() == GPG_ERR_BAD_PIN) { d->authenticate(); return; } #endif d->error(i18nc("@info", "Writing the certificate to the card failed: %1", QString::fromUtf8(err.asString()))); } else if (!err.isCanceled()) { d->success(i18nc("@info", "Writing the certificate to the card succeeded.")); ReaderStatus::mutableInstance()->updateStatus(); } d->finished(); } void CertificateToPIVCardCommand::doStart() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::doStart()"; d->start(); } void CertificateToPIVCardCommand::doCancel() { } #undef q_func #undef d_func diff --git a/src/commands/genrevokecommand.cpp b/src/commands/genrevokecommand.cpp index 6d881e84c..205223d07 100644 --- a/src/commands/genrevokecommand.cpp +++ b/src/commands/genrevokecommand.cpp @@ -1,199 +1,208 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/genrevokecommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2017 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "genrevokecommand.h" #include #include #include #include #include #include #include #include #include #include -#include "kleopatra_debug.h" #include "command_p.h" +#include "kleopatra_debug.h" +#include using namespace Kleo; using namespace Kleo::Commands; using namespace GpgME; GenRevokeCommand::GenRevokeCommand(QAbstractItemView *v, KeyListController *c) : GnuPGProcessCommand(v, c) { } GenRevokeCommand::GenRevokeCommand(KeyListController *c) : GnuPGProcessCommand(c) { } GenRevokeCommand::GenRevokeCommand(const Key &key) : GnuPGProcessCommand(key) { } // Fixup the revocation certificate similar to GnuPG void GenRevokeCommand::postSuccessHook(QWidget *parentWidget) { QFile f(mOutputFileName); if (!f.open(QIODevice::ReadOnly)) { // Should never happen because in this case we would not have had a success. KMessageBox::error(parentWidget, errorCaption(), QStringLiteral("Failed to access the created output file.")); return; } const QString revCert = QString::fromLocal8Bit(f.readAll()); f.close(); if (!f.open(QIODevice::WriteOnly)) { KMessageBox::error(parentWidget, errorCaption(), QStringLiteral("Failed to write to the created output file.")); return; } QTextStream s(&f); s << i18n("This is a revocation certificate for the OpenPGP key:") << "\n\n " << Formatting::prettyNameAndEMail(d->key()) << "\n Fingerprint: " << d->key().primaryFingerprint() << "\n\n" << i18n("A revocation certificate is a kind of \"kill switch\" to publicly\n" "declare that a key shall not anymore be used. It is not possible\n" "to retract such a revocation certificate once it has been published.") << "\n\n" << i18n("Use it to revoke this key in case of a compromise or loss of\n" "the secret key.") << "\n\n" << i18n("To avoid an accidental use of this file, a colon has been inserted\n" "before the 5 dashes below. Remove this colon with a text editor\n" "before importing and publishing this revocation certificate.") << "\n\n:" << revCert; s.flush(); qCDebug(KLEOPATRA_LOG) << "revocation certificate stored as:" << mOutputFileName; f.close(); KMessageBox::information(d->parentWidgetOrView(), i18nc("@info", "Certificate successfully created.

" "Note:
To prevent accidental import of the revocation
" "it is required to manually edit the certificate
" "before it can be imported."), i18n("Revocation certificate created")); } /* Well not much to do with GnuPGProcessCommand anymore I guess.. */ void GenRevokeCommand::doStart() { auto proposedFileName = ApplicationState::lastUsedExportDirectory() + u'/' + QString::fromLatin1(d->key().primaryFingerprint()) + QLatin1String{".rev"}; while (mOutputFileName.isEmpty()) { mOutputFileName = QFileDialog::getSaveFileName(d->parentWidgetOrView(), i18n("Generate revocation certificate"), proposedFileName, QStringLiteral("%1 (*.rev)").arg(i18n("Revocation Certificates ")), {}, QFileDialog::DontConfirmOverwrite); if (mOutputFileName.isEmpty()) { d->finished(); return; } if (!mOutputFileName.endsWith(QLatin1String(".rev"))) { mOutputFileName += QLatin1String(".rev"); } const QFileInfo fi{mOutputFileName}; if (fi.exists()) { +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + auto sel = KMessageBox::questionTwoActions(d->parentWidgetOrView(), +#else auto sel = KMessageBox::questionYesNo(d->parentWidgetOrView(), - xi18n("The file %1 already exists. Do you wish to overwrite it?", fi.fileName()), - i18nc("@title:window", "Overwrite File?"), - KStandardGuiItem::overwrite(), - KStandardGuiItem::cancel(), - {}, - KMessageBox::Notify|KMessageBox::Dangerous); +#endif + xi18n("The file %1 already exists. Do you wish to overwrite it?", fi.fileName()), + i18nc("@title:window", "Overwrite File?"), + KStandardGuiItem::overwrite(), + KStandardGuiItem::cancel(), + {}, + KMessageBox::Notify | KMessageBox::Dangerous); +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + if (sel == KMessageBox::ButtonCode::SecondaryAction) { +#else if (sel == KMessageBox::No) { +#endif proposedFileName = mOutputFileName; mOutputFileName.clear(); } } } ApplicationState::setLastUsedExportDirectory(mOutputFileName); auto proc = process(); // We do custom io disconnect(m_procReadyReadStdErrConnection); proc->setReadChannel(QProcess::StandardOutput); GnuPGProcessCommand::doStart(); connect(proc, &QProcess::readyReadStandardOutput, this, [proc] () { while (proc->canReadLine()) { const QString line = QString::fromUtf8(proc->readLine()).trimmed(); // Command-fd is a stable interface, while this is all kind of hacky we // are on a deadline :-/ if (line == QLatin1String("[GNUPG:] GET_BOOL gen_revoke.okay")) { proc->write("y\n"); } else if (line == QLatin1String("[GNUPG:] GET_LINE ask_revocation_reason.code")) { proc->write("0\n"); } else if (line == QLatin1String("[GNUPG:] GET_LINE ask_revocation_reason.text")) { proc->write("\n"); } else if (line == QLatin1String("[GNUPG:] GET_BOOL openfile.overwrite.okay")) { // We asked before proc->write("y\n"); } else if (line == QLatin1String("[GNUPG:] GET_BOOL ask_revocation_reason.okay")) { proc->write("y\n"); } } }); } QStringList GenRevokeCommand::arguments() const { const Key key = d->key(); QStringList result; result << gpgPath() << QStringLiteral("--command-fd") << QStringLiteral("0") << QStringLiteral("--status-fd") << QStringLiteral("1") << QStringLiteral("-o") << mOutputFileName << QStringLiteral("--gen-revoke") << QLatin1String(key.primaryFingerprint()); return result; } QString GenRevokeCommand::errorCaption() const { return i18nc("@title:window", "Error creating revocation certificate"); } QString GenRevokeCommand::crashExitMessage(const QStringList &) const { // We show a success message so a failure is either the user aborted // or a bug. qCDebug(KLEOPATRA_LOG) << "Crash exit of GenRevokeCommand"; return QString(); } QString GenRevokeCommand::errorExitMessage(const QStringList &) const { // We show a success message so a failure is either the user aborted // or a bug. qCDebug(KLEOPATRA_LOG) << "Error exit of GenRevokeCommand"; return QString(); } diff --git a/src/commands/keytocardcommand.cpp b/src/commands/keytocardcommand.cpp index 3dc766cdb..e23be6b1a 100644 --- a/src/commands/keytocardcommand.cpp +++ b/src/commands/keytocardcommand.cpp @@ -1,520 +1,530 @@ /* commands/keytocardcommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2017 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-FileCopyrightText: 2020 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "keytocardcommand.h" #include "cardcommand_p.h" #include "commands/authenticatepivcardapplicationcommand.h" #include "smartcard/openpgpcard.h" #include "smartcard/pivcard.h" #include "smartcard/readerstatus.h" #include "smartcard/utils.h" #include #include #include #include #include #include #include #include #include #if GPG_ERROR_VERSION_NUMBER >= 0x12400 // 1.36 # define GPG_ERROR_HAS_NO_AUTH #endif #include "kleopatra_debug.h" +#include using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::SmartCard; using namespace GpgME; class KeyToCardCommand::Private : public CardCommand::Private { friend class ::Kleo::Commands::KeyToCardCommand; KeyToCardCommand *q_func() const { return static_cast(q); } public: explicit Private(KeyToCardCommand *qq, const GpgME::Subkey &subkey); explicit Private(KeyToCardCommand *qq, const std::string &slot, const std::string &serialNumber, const std::string &appName); ~Private() override; private: void start(); void startKeyToOpenPGPCard(); Subkey getSubkeyToTransferToPIVCard(const std::string &cardSlot, const std::shared_ptr &card); void startKeyToPIVCard(); void authenticate(); void authenticationFinished(); void authenticationCanceled(); private: std::string appName; GpgME::Subkey subkey; std::string cardSlot; bool overwriteExistingAlreadyApproved = false; bool hasBeenCanceled = false; }; KeyToCardCommand::Private *KeyToCardCommand::d_func() { return static_cast(d.get()); } const KeyToCardCommand::Private *KeyToCardCommand::d_func() const { return static_cast(d.get()); } #define q q_func() #define d d_func() KeyToCardCommand::Private::Private(KeyToCardCommand *qq, const GpgME::Subkey &subkey_) : CardCommand::Private(qq, "", nullptr) , subkey(subkey_) { } KeyToCardCommand::Private::Private(KeyToCardCommand *qq, const std::string &slot, const std::string &serialNumber, const std::string &appName_) : CardCommand::Private(qq, serialNumber, nullptr) , appName(appName_) , cardSlot(slot) { } KeyToCardCommand::Private::~Private() { } namespace { static std::shared_ptr getCardToTransferSubkeyTo(const Subkey &subkey, QWidget *parent) { const std::vector > suitableCards = KeyToCardCommand::getSuitableCards(subkey); if (suitableCards.empty()) { return std::shared_ptr(); } else if (suitableCards.size() == 1) { return suitableCards[0]; } QStringList options; for (const auto &card: suitableCards) { options.push_back(i18nc("smartcard application - serial number of smartcard", "%1 - %2", displayAppName(card->appName()), card->displaySerialNumber())); } bool ok; const QString choice = QInputDialog::getItem(parent, i18n("Select Card"), i18n("Please select the card the key should be written to:"), options, /* current= */ 0, /* editable= */ false, &ok); if (!ok) { return std::shared_ptr(); } const int index = options.indexOf(choice); return suitableCards[index]; } } void KeyToCardCommand::Private::start() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::Private::start()"; if (!subkey.isNull() && serialNumber().empty()) { const auto card = getCardToTransferSubkeyTo(subkey, parentWidgetOrView()); if (!card) { finished(); return; } setSerialNumber(card->serialNumber()); appName = card->appName(); } const auto card = SmartCard::ReaderStatus::instance()->getCard(serialNumber(), appName); if (!card) { error(i18n("Failed to find the card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } if (card->appName() == SmartCard::OpenPGPCard::AppName) { startKeyToOpenPGPCard(); } else if (card->appName() == SmartCard::PIVCard::AppName) { startKeyToPIVCard(); } else { error(i18n("Sorry! Transferring keys to this card is not supported.")); finished(); return; } } namespace { static int getOpenPGPCardSlotForKey(const GpgME::Subkey &subKey, QWidget *parent) { // Check if we need to ask the user for the slot if ((subKey.canSign() || subKey.canCertify()) && !subKey.canEncrypt() && !subKey.canAuthenticate()) { // Signing only return 1; } if (subKey.canEncrypt() && !(subKey.canSign() || subKey.canCertify()) && !subKey.canAuthenticate()) { // Encrypt only return 2; } if (subKey.canAuthenticate() && !(subKey.canSign() || subKey.canCertify()) && !subKey.canEncrypt()) { // Auth only return 3; } // Multiple uses, ask user. QStringList options; if (subKey.canSign() || subKey.canCertify()) { options << i18nc("Placeholder is the number of a slot on a smart card", "Signature (%1)", 1); } if (subKey.canEncrypt()) { options << i18nc("Placeholder is the number of a slot on a smart card", "Encryption (%1)", 2); } if (subKey.canAuthenticate()) { options << i18nc("Placeholder is the number of a slot on a smart card", "Authentication (%1)", 3); } bool ok; const QString choice = QInputDialog::getItem(parent, i18n("Select Card Slot"), i18n("Please select the card slot the key should be written to:"), options, /* current= */ 0, /* editable= */ false, &ok); const int slot = options.indexOf(choice) + 1; return ok ? slot : -1; } } void KeyToCardCommand::Private::startKeyToOpenPGPCard() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::Private::startKeyToOpenPGPCard()"; const auto pgpCard = SmartCard::ReaderStatus::instance()->getCard(serialNumber()); if (!pgpCard) { error(i18n("Failed to find the OpenPGP card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } if (subkey.isNull()) { finished(); return; } if (subkey.parent().protocol() != GpgME::OpenPGP) { error(i18n("Sorry! This key cannot be transferred to an OpenPGP card.")); finished(); return; } const auto slot = getOpenPGPCardSlotForKey(subkey, parentWidgetOrView()); if (slot < 1) { finished(); return; } // Check if we need to do the overwrite warning. std::string existingKey; QString encKeyWarning; if (slot == 1) { existingKey = pgpCard->keyFingerprint(OpenPGPCard::pgpSigKeyRef()); } else if (slot == 2) { existingKey = pgpCard->keyFingerprint(OpenPGPCard::pgpEncKeyRef()); encKeyWarning = i18n("It will no longer be possible to decrypt past communication " "encrypted for the existing key."); } else if (slot == 3) { existingKey = pgpCard->keyFingerprint(OpenPGPCard::pgpAuthKeyRef()); } if (!existingKey.empty()) { const QString message = i18nc("@info", "

This card already contains a key in this slot. Continuing will overwrite that key.

" "

If there is no backup the existing key will be irrecoverably lost.

") + i18n("The existing key has the fingerprint:") + QStringLiteral("
%1
").arg(QString::fromStdString(existingKey)) + encKeyWarning; const auto choice = KMessageBox::warningContinueCancel(parentWidgetOrView(), message, i18nc("@title:window", "Overwrite existing key"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QString(), KMessageBox::Notify | KMessageBox::Dangerous); if (choice != KMessageBox::Continue) { finished(); return; } } // Now do the deed const auto time = QDateTime::fromSecsSinceEpoch(quint32(subkey.creationTime()), Qt::UTC); const auto timestamp = time.toString(QStringLiteral("yyyyMMdd'T'HHmmss")); const QString cmd = QStringLiteral("KEYTOCARD --force %1 %2 OPENPGP.%3 %4") .arg(QString::fromLatin1(subkey.keyGrip()), QString::fromStdString(serialNumber())) .arg(slot) .arg(timestamp); ReaderStatus::mutableInstance()->startSimpleTransaction(pgpCard, cmd.toUtf8(), q_func(), [this](const GpgME::Error &err) { q->keyToOpenPGPCardDone(err); }); } namespace { static std::vector getSigningCertificates() { std::vector signingCertificates = KeyCache::instance()->secretKeys(); const auto it = std::remove_if(signingCertificates.begin(), signingCertificates.end(), [](const Key &key) { return ! (key.protocol() == GpgME::CMS && !key.subkey(0).isNull() && key.subkey(0).canSign() && !key.subkey(0).canEncrypt() && key.subkey(0).isSecret() && !key.subkey(0).isCardKey()); }); signingCertificates.erase(it, signingCertificates.end()); return signingCertificates; } static std::vector getEncryptionCertificates() { std::vector encryptionCertificates = KeyCache::instance()->secretKeys(); const auto it = std::remove_if(encryptionCertificates.begin(), encryptionCertificates.end(), [](const Key &key) { return ! (key.protocol() == GpgME::CMS && !key.subkey(0).isNull() && key.subkey(0).canEncrypt() && key.subkey(0).isSecret() && !key.subkey(0).isCardKey()); }); encryptionCertificates.erase(it, encryptionCertificates.end()); return encryptionCertificates; } } Subkey KeyToCardCommand::Private::getSubkeyToTransferToPIVCard(const std::string &cardSlot, const std::shared_ptr &/*card*/) { if (cardSlot != PIVCard::cardAuthenticationKeyRef() && cardSlot != PIVCard::keyManagementKeyRef()) { return Subkey(); } const std::vector certificates = cardSlot == PIVCard::cardAuthenticationKeyRef() ? getSigningCertificates() : getEncryptionCertificates(); if (certificates.empty()) { error(i18n("Sorry! No suitable certificate to write to this card slot was found.")); return Subkey(); } auto dialog = new KeySelectionDialog(parentWidgetOrView()); dialog->setWindowTitle(i18nc("@title:window", "Select Certificate")); dialog->setText(i18n("Please select the certificate whose key pair you want to write to the card:")); dialog->setKeys(certificates); if (dialog->exec() == QDialog::Rejected) { return Subkey(); } return dialog->selectedKey().subkey(0); } void KeyToCardCommand::Private::startKeyToPIVCard() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::Private::startKeyToPIVCard()"; const auto pivCard = SmartCard::ReaderStatus::instance()->getCard(serialNumber()); if (!pivCard) { error(i18n("Failed to find the PIV card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } if (cardSlot != PIVCard::cardAuthenticationKeyRef() && cardSlot != PIVCard::keyManagementKeyRef()) { // key to card is only supported for the Card Authentication key and the Key Management key finished(); return; } if (subkey.isNull()) { subkey = getSubkeyToTransferToPIVCard(cardSlot, pivCard); } if (subkey.isNull()) { finished(); return; } if (subkey.parent().protocol() != GpgME::CMS) { error(i18n("Sorry! This key cannot be transferred to a PIV card.")); finished(); return; } if (!subkey.canEncrypt() && !subkey.canSign()) { error(i18n("Sorry! Only encryption keys and signing keys can be transferred to a PIV card.")); finished(); return; } // Check if we need to do the overwrite warning. if (!overwriteExistingAlreadyApproved) { const std::string existingKey = pivCard->keyInfo(cardSlot).grip; if (!existingKey.empty() && (existingKey != subkey.keyGrip())) { const QString decryptionWarning = (cardSlot == PIVCard::keyManagementKeyRef()) ? i18n("It will no longer be possible to decrypt past communication encrypted for the existing key.") : QString(); const QString message = i18nc("@info", "

This card already contains a key in this slot. Continuing will overwrite that key.

" "

If there is no backup the existing key will be irrecoverably lost.

") + i18n("The existing key has the key grip:") + QStringLiteral("
%1
").arg(QString::fromStdString(existingKey)) + decryptionWarning; const auto choice = KMessageBox::warningContinueCancel(parentWidgetOrView(), message, i18nc("@title:window", "Overwrite existing key"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QString(), KMessageBox::Notify | KMessageBox::Dangerous); if (choice != KMessageBox::Continue) { finished(); return; } overwriteExistingAlreadyApproved = true; } } const QString cmd = QStringLiteral("KEYTOCARD --force %1 %2 %3") .arg(QString::fromLatin1(subkey.keyGrip()), QString::fromStdString(serialNumber())) .arg(QString::fromStdString(cardSlot)); ReaderStatus::mutableInstance()->startSimpleTransaction(pivCard, cmd.toUtf8(), q_func(), [this](const GpgME::Error &err) { q->keyToPIVCardDone(err); }); } void KeyToCardCommand::Private::authenticate() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::authenticate()"; auto cmd = new AuthenticatePIVCardApplicationCommand(serialNumber(), parentWidgetOrView()); connect(cmd, &AuthenticatePIVCardApplicationCommand::finished, q, [this]() { authenticationFinished(); }); connect(cmd, &AuthenticatePIVCardApplicationCommand::canceled, q, [this]() { authenticationCanceled(); }); cmd->start(); } void KeyToCardCommand::Private::authenticationFinished() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::authenticationFinished()"; if (!hasBeenCanceled) { startKeyToPIVCard(); } } void KeyToCardCommand::Private::authenticationCanceled() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::authenticationCanceled()"; hasBeenCanceled = true; canceled(); } KeyToCardCommand::KeyToCardCommand(const GpgME::Subkey &subkey) : CardCommand(new Private(this, subkey)) { } KeyToCardCommand::KeyToCardCommand(const std::string& cardSlot, const std::string &serialNumber, const std::string &appName) : CardCommand(new Private(this, cardSlot, serialNumber, appName)) { } KeyToCardCommand::~KeyToCardCommand() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::~KeyToCardCommand()"; } // static std::vector > KeyToCardCommand::getSuitableCards(const GpgME::Subkey &subkey) { std::vector > suitableCards; if (subkey.isNull() || subkey.parent().protocol() != GpgME::OpenPGP) { return suitableCards; } for (const auto &card: ReaderStatus::instance()->getCards()) { if (card->appName() == OpenPGPCard::AppName) { suitableCards.push_back(card); } } return suitableCards; } void KeyToCardCommand::keyToOpenPGPCardDone(const GpgME::Error &err) { if (err) { d->error(i18nc("@info", "Moving the key to the card failed: %1", QString::fromUtf8(err.asString()))); } else if (!err.isCanceled()) { /* TODO DELETE_KEY is too strong, because it also deletes the stub * of the secret key. I could not find out how GnuPG does this. Question * to GnuPG Developers is pending an answer +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + if (KMessageBox::questionTwoActions(d->parentWidgetOrView(), +#else if (KMessageBox::questionYesNo(d->parentWidgetOrView(), + +#endif i18n("Do you want to delete the key on this computer?"), i18nc("@title:window", +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + "Key transferred to card")) == KMessageBox::ButtonCode::PrimaryAction) { +#else "Key transferred to card")) == KMessageBox::Yes) { +#endif const QString cmd = QStringLiteral("DELETE_KEY --force %1").arg(d->subkey.keyGrip()); // Using readerstatus is a bit overkill but it's an easy way to talk to the agent. ReaderStatus::mutableInstance()->startSimpleTransaction(card, cmd.toUtf8(), this, "deleteDone"); } */ d->success(i18nc("@info", "Successfully copied the key to the card.")); ReaderStatus::mutableInstance()->updateStatus(); } d->finished(); } void KeyToCardCommand::keyToPIVCardDone(const GpgME::Error &err) { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::keyToPIVCardDone():" << err.asString() << "(" << err.code() << ")"; if (err) { #ifdef GPG_ERROR_HAS_NO_AUTH // gpgme 1.13 reports "BAD PIN" instead of "NO AUTH" if (err.code() == GPG_ERR_NO_AUTH || err.code() == GPG_ERR_BAD_PIN) { d->authenticate(); return; } #endif d->error(i18nc("@info", "Copying the key pair to the card failed: %1", QString::fromUtf8(err.asString()))); } else if (!err.isCanceled()) { d->success(i18nc("@info", "Successfully copied the key pair to the card.")); ReaderStatus::mutableInstance()->updateStatus(); } d->finished(); } void KeyToCardCommand::deleteDone(const GpgME::Error &err) { if (err) { d->error(i18nc("@info", "Failed to delete the key: %1", QString::fromUtf8(err.asString()))); } d->finished(); } void KeyToCardCommand::doStart() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::doStart()"; d->start(); } void KeyToCardCommand::doCancel() { } #undef q_func #undef d_func diff --git a/src/commands/revokecertificationcommand.cpp b/src/commands/revokecertificationcommand.cpp index 44ba336ac..d85c985f1 100644 --- a/src/commands/revokecertificationcommand.cpp +++ b/src/commands/revokecertificationcommand.cpp @@ -1,359 +1,381 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/revokecertificationcommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2020, 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "revokecertificationcommand.h" #include "command_p.h" #include "exportopenpgpcertstoservercommand.h" #include #include #include #include #include #include #include #include #include +#include #include #include #include using namespace Kleo; using namespace Kleo::Commands; using namespace GpgME; using namespace QGpgME; namespace { enum class InputType { Key, UserIDs, Certifications, }; struct CertificationData { UserID userId; Key certificationKey; }; static std::vector getCertificationKeys(const GpgME::UserID &userId) { std::vector keys; if (userId.numSignatures() == 0) { qCWarning(KLEOPATRA_LOG) << __func__ << "- Error: Signatures of user ID" << QString::fromUtf8(userId.id()) << "not available"; return keys; } std::vector revokableCertifications; Kleo::copy_if(userId.signatures(), std::back_inserter(revokableCertifications), [](const auto &certification) { return userCanRevokeCertification(certification) == CertificationCanBeRevoked; }); Kleo::transform(revokableCertifications, std::back_inserter(keys), [](const auto &certification) { return KeyCache::instance()->findByKeyIDOrFingerprint(certification.signerKeyID()); }); return keys; } static bool confirmRevocations(QWidget *parent, const std::vector &certifications) { KMessageBox::ButtonCode answer; if (certifications.size() == 1) { const auto [userId, certificationKey] = certifications.front(); const auto message = xi18nc("@info", "You are about to revoke the certification of user ID%1made with the key%2.", QString::fromUtf8(userId.id()), Formatting::formatForComboBox(certificationKey)); +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + answer = KMessageBox::questionTwoActions(parent, +#else answer = KMessageBox::questionYesNo(parent, - message, - i18nc("@title:window", "Confirm Revocation"), - KGuiItem{i18n("Revoke Certification")}, - KStandardGuiItem::cancel()); +#endif + message, + i18nc("@title:window", "Confirm Revocation"), + KGuiItem{i18n("Revoke Certification")}, + KStandardGuiItem::cancel()); } else { QStringList l; Kleo::transform(certifications, std::back_inserter(l), [](const auto &c) { return i18n("User ID '%1' certified with key %2", QString::fromUtf8(c.userId.id()), Formatting::formatForComboBox(c.certificationKey)); }); const auto message = i18np("You are about to revoke the following certification:", // "You are about to revoke the following %1 certifications:", certifications.size()); +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + answer = KMessageBox::questionTwoActionsList(parent, +#else answer = KMessageBox::questionYesNoList(parent, - message, - l, - i18nc("@title:window", "Confirm Revocation"), - KGuiItem{i18n("Revoke Certifications")}, - KStandardGuiItem::cancel()); +#endif + message, + l, + i18nc("@title:window", "Confirm Revocation"), + KGuiItem{i18n("Revoke Certifications")}, + KStandardGuiItem::cancel()); } +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + return answer == KMessageBox::ButtonCode::PrimaryAction; +#else return answer == KMessageBox::Yes; +#endif } } class RevokeCertificationCommand::Private : public Command::Private { friend class ::Kleo::Commands::RevokeCertificationCommand; RevokeCertificationCommand *q_func() const { return static_cast(q); } public: Private(InputType i, RevokeCertificationCommand *qq, KeyListController *c = nullptr); void init(); private: std::vector getCertificationsToRevoke(); void scheduleNextRevocation(); QGpgME::QuickJob *createJob(); void slotResult(const Error &err); private: InputType inputType = InputType::Key; Key certificationTarget; std::vector uids; std::vector certificationsToRevoke; std::vector completedRevocations; QPointer job; }; RevokeCertificationCommand::Private *RevokeCertificationCommand::d_func() { return static_cast(d.get()); } const RevokeCertificationCommand::Private *RevokeCertificationCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() RevokeCertificationCommand::Private::Private(InputType i, RevokeCertificationCommand *qq, KeyListController *c) : Command::Private{qq, c} , inputType{i} { } void RevokeCertificationCommand::Private::init() { const std::vector keys_ = keys(); if (keys_.size() != 1) { qCWarning(KLEOPATRA_LOG) << q << "Expected exactly one key, but got" << keys_.size(); return; } if (keys_.front().protocol() != GpgME::OpenPGP) { qCWarning(KLEOPATRA_LOG) << q << "Expected OpenPGP key, but got" << keys_.front().protocolAsString(); return; } certificationTarget = keys_.front(); } std::vector RevokeCertificationCommand::Private::getCertificationsToRevoke() { if (inputType != InputType::Certifications) { // ensure that the certifications of the key have been loaded if (certificationTarget.userID(0).numSignatures() == 0) { certificationTarget.update(); } // build list of user IDs and revokable certifications const auto userIDsToConsider = (inputType == InputType::Key) ? certificationTarget.userIDs() : uids; for (const auto &userId : userIDsToConsider) { Kleo::transform(getCertificationKeys(userId), std::back_inserter(certificationsToRevoke), [userId](const auto &k) { return CertificationData{userId, k}; }); } } Kleo::erase_if(certificationsToRevoke, [](const auto &c) { return c.certificationKey.isNull(); }); return certificationsToRevoke; } void RevokeCertificationCommand::Private::scheduleNextRevocation() { if (!certificationsToRevoke.empty()) { const auto nextCertification = certificationsToRevoke.back(); job = createJob(); if (!job) { qCWarning(KLEOPATRA_LOG) << q << "Failed to create job"; finished(); return; } job->startRevokeSignature(certificationTarget, nextCertification.certificationKey, {nextCertification.userId}); } else { const auto message = xi18ncp("@info", "The certification has been revoked successfully." "Do you want to publish the revocation?", "%1 certifications have been revoked successfully." "Do you want to publish the revocations?", completedRevocations.size()); const auto yesButton = KGuiItem{i18ncp("@action:button", "Publish Revocation", "Publish Revocations", completedRevocations.size()), QIcon::fromTheme(QStringLiteral("view-certificate-export-server"))}; +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + const auto answer = KMessageBox::questionTwoActions(parentWidgetOrView(), +#else const auto answer = KMessageBox::questionYesNo(parentWidgetOrView(), - message, - i18nc("@title:window", "Confirm Publication"), - yesButton, - KStandardGuiItem::cancel(), - {}, - KMessageBox::Notify | KMessageBox::Dangerous); +#endif + message, + i18nc("@title:window", "Confirm Publication"), + yesButton, + KStandardGuiItem::cancel(), + {}, + KMessageBox::Notify | KMessageBox::Dangerous); +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + if (answer == KMessageBox::ButtonCode::PrimaryAction) { +#else if (answer == KMessageBox::Yes) { +#endif const auto cmd = new ExportOpenPGPCertsToServerCommand{certificationTarget}; cmd->start(); } finished(); } } void RevokeCertificationCommand::Private::slotResult(const Error &err) { if (err.isCanceled()) { canceled(); return; } if (err) { const auto failedRevocation = certificationsToRevoke.back(); error(xi18nc("@info", "The revocation of the certification of user ID%1made with key%2failed:" "%3", Formatting::prettyNameAndEMail(failedRevocation.userId), Formatting::formatForComboBox(failedRevocation.certificationKey), QString::fromUtf8(err.asString()))); finished(); return; } completedRevocations.push_back(certificationsToRevoke.back()); certificationsToRevoke.pop_back(); scheduleNextRevocation(); } QGpgME::QuickJob *RevokeCertificationCommand::Private::createJob() { const auto j = QGpgME::openpgp()->quickJob(); if (j) { connect(j, &Job::progress, q, &Command::progress); connect(j, &QGpgME::QuickJob::result, q, [this](const GpgME::Error &error) { slotResult(error); }); } return j; } RevokeCertificationCommand::RevokeCertificationCommand(QAbstractItemView *v, KeyListController *c) : Command{v, new Private{InputType::Key, this, c}} { d->init(); } RevokeCertificationCommand::RevokeCertificationCommand(const GpgME::Key &key) : Command{key, new Private{InputType::Key, this}} { d->init(); } RevokeCertificationCommand::RevokeCertificationCommand(const GpgME::UserID &uid) : Command{uid.parent(), new Private{InputType::UserIDs, this}} { std::vector(1, uid).swap(d->uids); d->init(); } RevokeCertificationCommand::RevokeCertificationCommand(const std::vector &uids) : Command{uids.empty() ? Key{} : uids.front().parent(), new Private{InputType::UserIDs, this}} { d->uids = uids; d->init(); } RevokeCertificationCommand::RevokeCertificationCommand(const GpgME::UserID::Signature &signature) : Command{signature.parent().parent(), new Private{InputType::Certifications, this}} { if (!signature.isNull()) { const Key certificationKey = KeyCache::instance()->findByKeyIDOrFingerprint(signature.signerKeyID()); d->certificationsToRevoke = {{signature.parent(), certificationKey}}; } d->init(); } RevokeCertificationCommand::~RevokeCertificationCommand() { qCDebug(KLEOPATRA_LOG) << this << __func__; } // static bool RevokeCertificationCommand::isSupported() { return engineInfo(GpgEngine).engineVersion() >= "2.2.24"; } void RevokeCertificationCommand::doStart() { if (d->certificationTarget.isNull()) { d->finished(); return; } if (!Kleo::all_of(d->uids, userIDBelongsToKey(d->certificationTarget))) { qCWarning(KLEOPATRA_LOG) << this << "User ID <-> Key mismatch!"; d->finished(); return; } const auto certificationsToRevoke = d->getCertificationsToRevoke(); if (certificationsToRevoke.empty()) { switch (d->inputType) { case InputType::Key: d->information(i18n("You cannot revoke any certifications of this key.")); break; case InputType::UserIDs: d->information(i18np("You cannot revoke any certifications of this user ID.", // "You cannot revoke any certifications of these user IDs.", d->uids.size())); break; case InputType::Certifications: d->information(i18n("You cannot revoke this certification.")); break; } d->finished(); return; } if (!confirmRevocations(d->parentWidgetOrView(), certificationsToRevoke)) { d->canceled(); return; } d->scheduleNextRevocation(); } void RevokeCertificationCommand::doCancel() { qCDebug(KLEOPATRA_LOG) << this << __func__; if (d->job) { d->job->slotCancel(); } } #undef d #undef q #include "moc_revokecertificationcommand.cpp" + diff --git a/src/conf/cryptooperationsconfigwidget.cpp b/src/conf/cryptooperationsconfigwidget.cpp index ef2e65822..90585abfe 100644 --- a/src/conf/cryptooperationsconfigwidget.cpp +++ b/src/conf/cryptooperationsconfigwidget.cpp @@ -1,415 +1,426 @@ /* cryptooperationsconfigwidget.cpp This file is part of kleopatra, the KDE key manager SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "cryptooperationsconfigwidget.h" #include "kleopatra_debug.h" #include "emailoperationspreferences.h" #include "fileoperationspreferences.h" #include "settings.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include using namespace Kleo; using namespace Kleo::Config; CryptoOperationsConfigWidget::CryptoOperationsConfigWidget(QWidget *p, Qt::WindowFlags f) : QWidget{p, f} { setupGui(); } static void resetDefaults() { auto config = QGpgME::cryptoConfig(); if (!config) { qCWarning(KLEOPATRA_LOG) << "Failed to obtain config"; return; } const QStringList componentList = config->componentList(); for (const auto &compName: componentList) { auto comp = config->component(compName); if (!comp) { qCWarning(KLEOPATRA_LOG) << "Failed to find component:" << comp; return; } const QStringList groupList = comp->groupList(); for (const auto &grpName: groupList) { auto grp = comp->group(grpName); if (!grp) { qCWarning(KLEOPATRA_LOG) << "Failed to find group:" << grp << "in component:" << compName; return; } const QStringList entries = grp->entryList(); for (const auto &entryName: entries) { auto entry = grp->entry(entryName); if (!entry) { qCWarning(KLEOPATRA_LOG) << "Failed to find entry:" << entry << "in group:"<< grp << "in component:" << compName; return; } entry->resetToDefault(); } } } config->sync(true); return; } void CryptoOperationsConfigWidget::applyProfile(const QString &profile) { if (profile.isEmpty()) { return; } qCDebug(KLEOPATRA_LOG) << "Applying profile " << profile; if (profile == i18n("default")) { +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + if (KMessageBox::warningTwoActions( +#else if (KMessageBox::warningYesNo( - this, - i18n("This means that every configuration option of the GnuPG System will be reset to its default."), - i18n("Apply profile"), - KStandardGuiItem::apply(), - KStandardGuiItem::no()) != KMessageBox::Yes) { + +#endif + this, + i18n("This means that every configuration option of the GnuPG System will be reset to its default."), + i18n("Apply profile"), + KStandardGuiItem::apply(), + KStandardGuiItem::cancel()) + #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + != KMessageBox::ButtonCode::PrimaryAction) { +#else + != KMessageBox::Yes) { + #endif return; } resetDefaults(); KeyFilterManager::instance()->reload(); return; } mApplyBtn->setEnabled(false); QDir datadir(QString::fromUtf8(GpgME::dirInfo("datadir")) + QStringLiteral("/../doc/gnupg/examples")); const auto path = datadir.filePath(profile + QStringLiteral(".prf")); auto gpgconf = new QProcess; const auto ei = GpgME::engineInfo(GpgME::GpgConfEngine); Q_ASSERT (ei.fileName()); gpgconf->setProgram(QFile::decodeName(ei.fileName())); gpgconf->setProcessChannelMode(QProcess::MergedChannels); gpgconf->setArguments(QStringList() << QStringLiteral("--runtime") << QStringLiteral("--apply-profile") << path); qDebug() << "Starting" << ei.fileName() << "with args" << gpgconf->arguments(); #if QT_DEPRECATED_SINCE(5, 13) connect(gpgconf, qOverload(&QProcess::finished), #else connect(gpgconf, &QProcess::finished, #endif this, [this, gpgconf, profile] () { mApplyBtn->setEnabled(true); if (gpgconf->exitStatus() != QProcess::NormalExit) { KMessageBox::error(this, QStringLiteral("
%1
").arg(QString::fromUtf8(gpgconf->readAll()))); delete gpgconf; return; } delete gpgconf; KMessageBox::information(this, i18nc("%1 is the name of the profile", "The configuration profile \"%1\" was applied.", profile), i18n("GnuPG Profile - Kleopatra")); auto config = QGpgME::cryptoConfig(); if (config) { config->clear(); } KeyFilterManager::instance()->reload(); }); gpgconf->start(); } // Get a list of available profile files and add a configuration // group if there are any. void CryptoOperationsConfigWidget::setupProfileGui(QBoxLayout *layout) { const Settings settings; if (settings.profilesDisabled() || GpgME::engineInfo(GpgME::GpgEngine).engineVersion() < "2.1.20" || !layout) { // Profile support is new in 2.1.20 qCDebug(KLEOPATRA_LOG) << "Profile settings disabled"; return; } QDir datadir(QString::fromUtf8(GpgME::dirInfo("datadir")) + QStringLiteral("/../doc/gnupg/examples")); if (!datadir.exists()) { qCDebug(KLEOPATRA_LOG) << "Failed to find gnupg's example profile directory" << datadir.path(); return; } const auto profiles = datadir.entryInfoList(QStringList() << QStringLiteral("*.prf"), QDir::Readable | QDir::Files, QDir::Name); if (profiles.isEmpty()) { qCDebug(KLEOPATRA_LOG) << "Failed to find any profiles in: " << datadir.path(); return; } auto genGrp = new QGroupBox(i18nc("@title", "General Operations")); auto profLayout = new QHBoxLayout; genGrp->setLayout(profLayout); layout->addWidget(genGrp); auto profLabel = new QLabel(i18n("Activate GnuPG Profile:")); profLabel->setToolTip(i18n("A profile consists of various settings that can apply to multiple components of the GnuPG system.")); auto combo = new QComboBox; profLabel->setBuddy(combo); // Add an empty Item to avoid the impression that this GUI element // shows the currently selected profile. combo->addItem(QString()); // We don't translate "default" here because the other profile names are // also not translated as they are taken directly from file. combo->addItem(i18n("default")); for (const auto &profile: profiles) { combo->addItem(profile.baseName()); } mApplyBtn = new QPushButton(i18n("Apply")); mApplyBtn->setEnabled(false); profLayout->addWidget(profLabel); profLayout->addWidget(combo); profLayout->addWidget(mApplyBtn); profLayout->addStretch(1); connect(mApplyBtn, &QPushButton::clicked, this, [this, combo] () { applyProfile(combo->currentText()); }); connect(combo, &QComboBox::currentTextChanged, this, [this] (const QString &text) { mApplyBtn->setEnabled(!text.isEmpty()); }); } void CryptoOperationsConfigWidget::setupGui() { auto baseLay = new QVBoxLayout(this); baseLay->setContentsMargins(0, 0, 0, 0); auto mailGrp = new QGroupBox(i18n("EMail Operations")); auto mailGrpLayout = new QVBoxLayout; mQuickSignCB = new QCheckBox(i18n("Don't confirm signing certificate if there is only one valid certificate for the identity")); mQuickEncryptCB = new QCheckBox(i18n("Don't confirm encryption certificates if there is exactly one valid certificate for each recipient")); mailGrpLayout->addWidget(mQuickSignCB); mailGrpLayout->addWidget(mQuickEncryptCB); mailGrp->setLayout(mailGrpLayout); baseLay->addWidget(mailGrp); auto fileGrp = new QGroupBox(i18n("File Operations")); auto fileGrpLay = new QVBoxLayout; mPGPFileExtCB = new QCheckBox(i18n(R"(Create OpenPGP encrypted files with ".pgp" file extensions instead of ".gpg")")); mASCIIArmorCB = new QCheckBox(i18n("Create signed or encrypted files as text files.")); mASCIIArmorCB->setToolTip(i18nc("@info", "Set this option to encode encrypted or signed files as base64 encoded text. " "So that they can be opened with an editor or sent in a mail body. " "This will increase file size by one third.")); mAutoDecryptVerifyCB = new QCheckBox(i18n("Automatically start operation based on input detection for decrypt/verify.")); mAutoExtractArchivesCB = new QCheckBox(i18n("Automatically extract file archives after decryption")); mTmpDirCB = new QCheckBox(i18n("Create temporary decrypted files in the folder of the encrypted file.")); mTmpDirCB->setToolTip(i18nc("@info", "Set this option to avoid using the users temporary directory.")); mSymmetricOnlyCB = new QCheckBox(i18n("Use symmetric encryption only.")); mSymmetricOnlyCB->setToolTip(i18nc("@info", "Set this option to disable public key encryption.")); fileGrpLay->addWidget(mPGPFileExtCB); fileGrpLay->addWidget(mAutoDecryptVerifyCB); fileGrpLay->addWidget(mAutoExtractArchivesCB); fileGrpLay->addWidget(mASCIIArmorCB); fileGrpLay->addWidget(mTmpDirCB); fileGrpLay->addWidget(mSymmetricOnlyCB); auto comboLay = new QGridLayout; mChecksumDefinitionCB.createWidgets(this); mChecksumDefinitionCB.label()->setText(i18n("Checksum program to use when creating checksum files:")); comboLay->addWidget(mChecksumDefinitionCB.label(), 0, 0); comboLay->addWidget(mChecksumDefinitionCB.widget(), 0, 1); mArchiveDefinitionCB.createWidgets(this); mArchiveDefinitionCB.label()->setText(i18n("Archive command to use when archiving files:")); comboLay->addWidget(mArchiveDefinitionCB.label(), 1, 0); comboLay->addWidget(mArchiveDefinitionCB.widget(), 1, 1); fileGrpLay->addLayout(comboLay); fileGrp->setLayout(fileGrpLay); baseLay->addWidget(fileGrp); setupProfileGui(baseLay); baseLay->addStretch(1); for (auto cb : findChildren()) { connect(cb, &QCheckBox::toggled, this, &CryptoOperationsConfigWidget::changed); } for (auto combo : findChildren()) { connect(combo, qOverload(&QComboBox::currentIndexChanged), this, &CryptoOperationsConfigWidget::changed); } } CryptoOperationsConfigWidget::~CryptoOperationsConfigWidget() {} void CryptoOperationsConfigWidget::defaults() { EMailOperationsPreferences emailPrefs; emailPrefs.setQuickSignEMail(emailPrefs.findItem(QStringLiteral("QuickSignEMail"))->getDefault().toBool()); emailPrefs.setQuickEncryptEMail(emailPrefs.findItem(QStringLiteral("QuickEncryptEMail"))->getDefault().toBool()); FileOperationsPreferences filePrefs; filePrefs.setUsePGPFileExt(filePrefs.findItem(QStringLiteral("UsePGPFileExt"))->getDefault().toBool()); filePrefs.setAutoDecryptVerify(filePrefs.findItem(QStringLiteral("AutoDecryptVerify"))->getDefault().toBool()); filePrefs.setAutoExtractArchives(filePrefs.findItem(QStringLiteral("AutoExtractArchives"))->getDefault().toBool()); filePrefs.setAddASCIIArmor(filePrefs.findItem(QStringLiteral("AddASCIIArmor"))->getDefault().toBool()); filePrefs.setDontUseTmpDir(filePrefs.findItem(QStringLiteral("DontUseTmpDir"))->getDefault().toBool()); filePrefs.setSymmetricEncryptionOnly(filePrefs.findItem(QStringLiteral("SymmetricEncryptionOnly"))->getDefault().toBool()); filePrefs.setArchiveCommand(filePrefs.findItem(QStringLiteral("ArchiveCommand"))->getDefault().toString()); Settings settings; settings.setChecksumDefinitionId(settings.findItem(QStringLiteral("ChecksumDefinitionId"))->getDefault().toString()); load(emailPrefs, filePrefs, settings); } void CryptoOperationsConfigWidget::load(const Kleo::EMailOperationsPreferences &emailPrefs, const Kleo::FileOperationsPreferences &filePrefs, const Kleo::Settings &settings) { mQuickSignCB->setChecked(emailPrefs.quickSignEMail()); mQuickSignCB->setEnabled(!emailPrefs.isImmutable(QStringLiteral("QuickSignEMail"))); mQuickEncryptCB->setChecked(emailPrefs.quickEncryptEMail()); mQuickEncryptCB->setEnabled(!emailPrefs.isImmutable(QStringLiteral("QuickEncryptEMail"))); mPGPFileExtCB->setChecked(filePrefs.usePGPFileExt()); mPGPFileExtCB->setEnabled(!filePrefs.isImmutable(QStringLiteral("UsePGPFileExt"))); mAutoDecryptVerifyCB->setChecked(filePrefs.autoDecryptVerify()); mAutoDecryptVerifyCB->setEnabled(!filePrefs.isImmutable(QStringLiteral("AutoDecryptVerify"))); mAutoExtractArchivesCB->setChecked(filePrefs.autoExtractArchives()); mAutoExtractArchivesCB->setEnabled(!filePrefs.isImmutable(QStringLiteral("AutoExtractArchives"))); mASCIIArmorCB->setChecked(filePrefs.addASCIIArmor()); mASCIIArmorCB->setEnabled(!filePrefs.isImmutable(QStringLiteral("AddASCIIArmor"))); mTmpDirCB->setChecked(filePrefs.dontUseTmpDir()); mTmpDirCB->setEnabled(!filePrefs.isImmutable(QStringLiteral("DontUseTmpDir"))); mSymmetricOnlyCB->setChecked(filePrefs.symmetricEncryptionOnly()); mSymmetricOnlyCB->setEnabled(!filePrefs.isImmutable(QStringLiteral("SymmetricEncryptionOnly"))); const auto defaultChecksumDefinitionId = settings.checksumDefinitionId(); { const auto index = mChecksumDefinitionCB.widget()->findData(defaultChecksumDefinitionId); if (index >= 0) { mChecksumDefinitionCB.widget()->setCurrentIndex(index); } else { qCWarning(KLEOPATRA_LOG) << "No checksum definition found with id" << defaultChecksumDefinitionId; } } mChecksumDefinitionCB.setEnabled(!settings.isImmutable(QStringLiteral("ChecksumDefinitionId"))); const auto ad_default_id = filePrefs.archiveCommand(); { const auto index = mArchiveDefinitionCB.widget()->findData(ad_default_id); if (index >= 0) { mArchiveDefinitionCB.widget()->setCurrentIndex(index); } else { qCWarning(KLEOPATRA_LOG) << "No archive definition found with id" << ad_default_id; } } mArchiveDefinitionCB.setEnabled(!filePrefs.isImmutable(QStringLiteral("ArchiveCommand"))); } void CryptoOperationsConfigWidget::load() { mChecksumDefinitionCB.widget()->clear(); const auto cds = ChecksumDefinition::getChecksumDefinitions(); for (const std::shared_ptr &cd : cds) { mChecksumDefinitionCB.widget()->addItem(cd->label(), QVariant{cd->id()}); } // This is a weird hack but because we are a KCM we can't link // against ArchiveDefinition which pulls in loads of other classes. // So we do the parsing which archive definitions exist here ourself. mArchiveDefinitionCB.widget()->clear(); if (KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("libkleopatrarc"))) { const QStringList groups = config->groupList().filter(QRegularExpression(QStringLiteral("^Archive Definition #"))); for (const QString &group : groups) { const KConfigGroup cGroup(config, group); const QString id = cGroup.readEntryUntranslated(QStringLiteral("id")); const QString name = cGroup.readEntry("Name"); mArchiveDefinitionCB.widget()->addItem(name, QVariant(id)); } } load(EMailOperationsPreferences{}, FileOperationsPreferences{}, Settings{}); } void CryptoOperationsConfigWidget::save() { EMailOperationsPreferences emailPrefs; emailPrefs.setQuickSignEMail(mQuickSignCB ->isChecked()); emailPrefs.setQuickEncryptEMail(mQuickEncryptCB->isChecked()); emailPrefs.save(); FileOperationsPreferences filePrefs; filePrefs.setUsePGPFileExt(mPGPFileExtCB->isChecked()); filePrefs.setAutoDecryptVerify(mAutoDecryptVerifyCB->isChecked()); filePrefs.setAutoExtractArchives(mAutoExtractArchivesCB->isChecked()); filePrefs.setAddASCIIArmor(mASCIIArmorCB->isChecked()); filePrefs.setDontUseTmpDir(mTmpDirCB->isChecked()); filePrefs.setSymmetricEncryptionOnly(mSymmetricOnlyCB->isChecked()); Settings settings; const int idx = mChecksumDefinitionCB.widget()->currentIndex(); if (idx >= 0) { const auto id = mChecksumDefinitionCB.widget()->itemData(idx).toString(); settings.setChecksumDefinitionId(id); } settings.save(); const int aidx = mArchiveDefinitionCB.widget()->currentIndex(); if (aidx >= 0) { const QString id = mArchiveDefinitionCB.widget()->itemData(aidx).toString(); filePrefs.setArchiveCommand(id); } filePrefs.save(); } diff --git a/src/conf/groupsconfigpage.cpp b/src/conf/groupsconfigpage.cpp index eb3a26bcd..40cfb6103 100644 --- a/src/conf/groupsconfigpage.cpp +++ b/src/conf/groupsconfigpage.cpp @@ -1,138 +1,149 @@ /* conf/groupsconfigpage.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "groupsconfigpage.h" #include "groupsconfigwidget.h" #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" +#include using namespace Kleo; class GroupsConfigPage::Private { friend class ::GroupsConfigPage; GroupsConfigPage *const q; Private(GroupsConfigPage *qq); public: ~Private() = default; void setChanged(bool changed); void onKeysMayHaveChanged(); private: GroupsConfigWidget *widget = nullptr; bool changed = false; bool savingChanges = false; }; GroupsConfigPage::Private::Private(GroupsConfigPage *qq) : q(qq) { } void GroupsConfigPage::Private::setChanged(bool state) { changed = state; Q_EMIT q->changed(changed); } void GroupsConfigPage::Private::onKeysMayHaveChanged() { static QMutex mutex; const UniqueLock lock{mutex, Kleo::tryToLock}; if (!lock) { // prevent reentrace return; } if (savingChanges) { qCDebug(KLEOPATRA_LOG) << __func__ << "ignoring changes caused by ourselves"; return; } if (!changed) { q->load(); } else { auto buttonYes = KStandardGuiItem::yes(); buttonYes.setText(i18n("Save changes")); auto buttonNo = KStandardGuiItem::no(); buttonNo.setText(i18n("Discard changes")); +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + const auto answer = KMessageBox::questionTwoActions( +#else const auto answer = KMessageBox::questionYesNo( +#endif q->topLevelWidget(), - xi18nc("@info", "The certificates or the certificate groups have been updated in the background." - "Do you want to save your changes?"), + xi18nc("@info", + "The certificates or the certificate groups have been updated in the background." + "Do you want to save your changes?"), i18nc("@title::window", "Save changes?"), - buttonYes, buttonNo); + buttonYes, + buttonNo); +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + if (answer == KMessageBox::ButtonCode::PrimaryAction) { +#else if (answer == KMessageBox::Yes) { +#endif q->save(); } else { q->load(); } } } GroupsConfigPage::GroupsConfigPage(QWidget *parent) : QWidget(parent) , d(new Private(this)) { auto layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); d->widget = new Kleo::GroupsConfigWidget(this); if (QLayout *l = d->widget->layout()) { l->setContentsMargins(0, 0, 0, 0); } layout->addWidget(d->widget); connect(d->widget, &GroupsConfigWidget::changed, this, [this] () { d->setChanged(true); }); connect(KeyCache::instance().get(), &KeyCache::keysMayHaveChanged, this, [this]() { d->onKeysMayHaveChanged(); }); } GroupsConfigPage::~GroupsConfigPage() = default; bool GroupsConfigPage::hasChanged() const { return d->changed; } void GroupsConfigPage::load() { d->widget->setGroups(KeyCache::instance()->configurableGroups()); d->setChanged(false); } void GroupsConfigPage::save() { d->savingChanges = true; KeyCache::mutableInstance()->saveConfigurableGroups(d->widget->groups()); d->savingChanges = false; // reload after saving to ensure that the groups reflect the saved groups (e.g. in case of immutable entries) load(); } diff --git a/src/conf/kleopageconfigdialog.cpp b/src/conf/kleopageconfigdialog.cpp index 6a35dd8ba..8b915f2c9 100644 --- a/src/conf/kleopageconfigdialog.cpp +++ b/src/conf/kleopageconfigdialog.cpp @@ -1,250 +1,263 @@ /* kleopageconfigdialog.cpp This file is part of Kleopatra SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-only It is derived from KCMultidialog which is: SPDX-FileCopyrightText: 2000 Matthias Elter SPDX-FileCopyrightText: 2003 Daniel Molkentin SPDX-FileCopyrightText: 2003, 2006 Matthias Kretz SPDX-FileCopyrightText: 2004 Frans Englich SPDX-FileCopyrightText: 2006 Tobias Koenig SPDX-License-Identifier: LGPL-2.0-or-later */ #include #include "kleopageconfigdialog.h" #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" +#include KleoPageConfigDialog::KleoPageConfigDialog(QWidget *parent) : KPageDialog(parent) { setModal(false); QDialogButtonBox *buttonBox = new QDialogButtonBox(this); buttonBox->setStandardButtons(QDialogButtonBox::Help | QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Cancel | QDialogButtonBox::Apply | QDialogButtonBox::Ok | QDialogButtonBox::Reset); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Ok), KStandardGuiItem::ok()); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel()); KGuiItem::assign(buttonBox->button(QDialogButtonBox::RestoreDefaults), KStandardGuiItem::defaults()); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Apply), KStandardGuiItem::apply()); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Reset), KStandardGuiItem::reset()); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Help), KStandardGuiItem::help()); buttonBox->button(QDialogButtonBox::Reset)->setEnabled(false); buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); connect(buttonBox->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &KleoPageConfigDialog::slotApplyClicked); connect(buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, this, &KleoPageConfigDialog::slotOkClicked); connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, this, &KleoPageConfigDialog::slotDefaultClicked); connect(buttonBox->button(QDialogButtonBox::Help), &QAbstractButton::clicked, this, &KleoPageConfigDialog::slotHelpClicked); connect(buttonBox->button(QDialogButtonBox::Reset), &QAbstractButton::clicked, this, &KleoPageConfigDialog::slotUser1Clicked); setButtonBox(buttonBox); connect(this, &KPageDialog::currentPageChanged, this, &KleoPageConfigDialog::slotCurrentPageChanged); } void KleoPageConfigDialog::slotCurrentPageChanged(KPageWidgetItem *current, KPageWidgetItem *previous) { if (!previous) { return; } blockSignals(true); setCurrentPage(previous); KCModule *previousModule = qobject_cast(previous->widget()); bool canceled = false; if (previousModule && mChangedModules.contains(previousModule)) { +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + const int queryUser = KMessageBox::warningTwoActionsCancel( +#else const int queryUser = KMessageBox::warningYesNoCancel( - this, - i18n("The settings of the current module have changed.\n" - "Do you want to apply the changes or discard them?"), - i18n("Apply Settings"), - KStandardGuiItem::apply(), - KStandardGuiItem::discard(), - KStandardGuiItem::cancel()); +#endif + this, + i18n("The settings of the current module have changed.\n" + "Do you want to apply the changes or discard them?"), + i18n("Apply Settings"), + KStandardGuiItem::apply(), + KStandardGuiItem::discard(), + KStandardGuiItem::cancel()); +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + if (queryUser == KMessageBox::ButtonCode::PrimaryAction) { +#else if (queryUser == KMessageBox::Yes) { +#endif previousModule->save(); +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + } else if (queryUser == KMessageBox::ButtonCode::SecondaryAction) { +#else } else if (queryUser == KMessageBox::No) { +#endif previousModule->load(); } canceled = queryUser == KMessageBox::Cancel; } if (!canceled) { mChangedModules.removeAll(previousModule); setCurrentPage(current); } blockSignals(false); clientChanged(); } void KleoPageConfigDialog::apply() { QPushButton *applyButton = buttonBox()->button(QDialogButtonBox::Apply); applyButton->setFocus(); for (KCModule *module : mChangedModules) { module->save(); } mChangedModules.clear(); Q_EMIT configCommitted(); clientChanged(); } void KleoPageConfigDialog::slotDefaultClicked() { const KPageWidgetItem *item = currentPage(); if (!item) { return; } KCModule *module = qobject_cast(item->widget()); if (!module) { return; } module->defaults(); clientChanged(); } void KleoPageConfigDialog::slotUser1Clicked() { const KPageWidgetItem *item = currentPage(); if (!item) { return; } KCModule *module = qobject_cast(item->widget()); if (!module) { return; } module->load(); mChangedModules.removeAll(module); clientChanged(); } void KleoPageConfigDialog::slotApplyClicked() { apply(); } void KleoPageConfigDialog::slotOkClicked() { apply(); accept(); } void KleoPageConfigDialog::slotHelpClicked() { const KPageWidgetItem *item = currentPage(); if (!item) { return; } const QString docPath = mHelpUrls.value(item->name()); QUrl docUrl; #ifdef Q_OS_WIN docUrl = QUrl(QLatin1String("https://docs.kde.org/index.php?branch=stable5&language=") + QLocale().name() + QLatin1String("&application=kleopatra")); #else docUrl = QUrl(QStringLiteral("help:/")).resolved(QUrl(docPath)); // same code as in KHelpClient::invokeHelp #endif if (docUrl.scheme() == QLatin1String("help") || docUrl.scheme() == QLatin1String("man") || docUrl.scheme() == QLatin1String("info")) { const QString exec = QStandardPaths::findExecutable(QStringLiteral("khelpcenter")); if (exec.isEmpty()) { qCWarning(KLEOPATRA_LOG) << "Could not find khelpcenter in PATH."; } else { QProcess::startDetached(QStringLiteral("khelpcenter"), QStringList() << docUrl.toString()); } } else { QDesktopServices::openUrl(docUrl); } } void KleoPageConfigDialog::addModule(const QString &name, const QString &docPath, const QString &icon, KCModule *module) { mModules << module; KPageWidgetItem *item = addPage(module, name); item->setIcon(QIcon::fromTheme(icon)); connect(module, SIGNAL(changed(bool)), this, SLOT(moduleChanged(bool))); mHelpUrls.insert(name, docPath); } void KleoPageConfigDialog::moduleChanged(bool state) { KCModule *module = qobject_cast(sender()); qCDebug(KLEOPATRA_LOG) << "Module changed: " << state << " mod " << module; if (mChangedModules.contains(module)) { if (!state) { mChangedModules.removeAll(module); } else { return; } } if (state) { mChangedModules << module; } clientChanged(); } void KleoPageConfigDialog::clientChanged() { const KPageWidgetItem *item = currentPage(); if (!item) { return; } KCModule *module = qobject_cast(item->widget()); if (!module) { return; } qCDebug(KLEOPATRA_LOG) << "Client changed: " << " mod " << module; bool change = mChangedModules.contains(module); QPushButton *resetButton = buttonBox()->button(QDialogButtonBox::Reset); if (resetButton) { resetButton->setEnabled(change); } QPushButton *applyButton = buttonBox()->button(QDialogButtonBox::Apply); if (applyButton) { applyButton->setEnabled(change); } } diff --git a/src/crypto/autodecryptverifyfilescontroller.cpp b/src/crypto/autodecryptverifyfilescontroller.cpp index 68c6a3412..5ff45235f 100644 --- a/src/crypto/autodecryptverifyfilescontroller.cpp +++ b/src/crypto/autodecryptverifyfilescontroller.cpp @@ -1,558 +1,581 @@ /* -*- mode: c++; c-basic-offset:4 -*- autodecryptverifyfilescontroller.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "autodecryptverifyfilescontroller.h" #include "fileoperationspreferences.h" #include #include #include #include #include "commands/decryptverifyfilescommand.h" #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" +#include #include #include #include #include #include #include #include using namespace GpgME; using namespace Kleo; using namespace Kleo::Crypto; using namespace Kleo::Crypto::Gui; class AutoDecryptVerifyFilesController::Private { AutoDecryptVerifyFilesController *const q; public: explicit Private(AutoDecryptVerifyFilesController *qq); void slotDialogCanceled(); void schedule(); QString getEmbeddedFileName(const QString &fileName) const; void exec(); std::vector > buildTasks(const QStringList &, QStringList &); struct CryptoFile { QString baseName; QString fileName; GpgME::Protocol protocol = GpgME::UnknownProtocol; int classification = 0; std::shared_ptr output; }; QVector classifyAndSortFiles(const QStringList &files); void reportError(int err, const QString &details) { q->setLastError(err, details); q->emitDoneOrError(); } void cancelAllTasks(); QStringList m_passedFiles, m_filesAfterPreparation; std::vector > m_results; std::vector > m_runnableTasks, m_completedTasks; std::shared_ptr m_runningTask; bool m_errorDetected = false; DecryptVerifyOperation m_operation = DecryptVerify; DecryptVerifyFilesDialog *m_dialog = nullptr; std::unique_ptr m_workDir; }; AutoDecryptVerifyFilesController::Private::Private(AutoDecryptVerifyFilesController *qq) : q(qq) { qRegisterMetaType(); } void AutoDecryptVerifyFilesController::Private::slotDialogCanceled() { qCDebug(KLEOPATRA_LOG); } void AutoDecryptVerifyFilesController::Private::schedule() { if (!m_runningTask && !m_runnableTasks.empty()) { const std::shared_ptr t = m_runnableTasks.back(); m_runnableTasks.pop_back(); t->start(); m_runningTask = t; } if (!m_runningTask) { kleo_assert(m_runnableTasks.empty()); for (const std::shared_ptr &i : std::as_const(m_results)) { Q_EMIT q->verificationResult(i->verificationResult()); } } } QString AutoDecryptVerifyFilesController::Private::getEmbeddedFileName(const QString &fileName) const { auto it = std::find_if(m_results.cbegin(), m_results.cend(), [fileName](const auto &r) { return r->fileName() == fileName; }); if (it != m_results.cend()) { const auto embeddedFilePath = QString::fromUtf8((*it)->decryptionResult().fileName()); if (embeddedFilePath.contains(QLatin1Char{'\\'})) { // ignore embedded file names containing '\' return {}; } // strip the path from the embedded file name return QFileInfo{embeddedFilePath}.fileName(); } else { return {}; } } void AutoDecryptVerifyFilesController::Private::exec() { Q_ASSERT(!m_dialog); QStringList undetected; std::vector > tasks = buildTasks(m_passedFiles, undetected); if (!undetected.isEmpty()) { // Since GpgME 1.7.0 Classification is supposed to be reliable // so we really can't do anything with this data. reportError(makeGnuPGError(GPG_ERR_GENERAL), xi18n("Failed to find encrypted or signed data in one or more files." "You can manually select what to do with the files now." "If they contain signed or encrypted data please report a bug (see Help->Report Bug).")); auto cmd = new Commands::DecryptVerifyFilesCommand(undetected, nullptr, true); cmd->start(); } if (tasks.empty()) { q->emitDoneOrError(); return; } Q_ASSERT(m_runnableTasks.empty()); m_runnableTasks.swap(tasks); std::shared_ptr coll(new TaskCollection); for (const std::shared_ptr &i : std::as_const(m_runnableTasks)) { q->connectTask(i); } coll->setTasks(m_runnableTasks); m_dialog = new DecryptVerifyFilesDialog(coll); m_dialog->setOutputLocation(heuristicBaseDirectory(m_passedFiles)); QTimer::singleShot(0, q, SLOT(schedule())); if (m_dialog->exec() == QDialog::Accepted && m_workDir) { // Without workdir there is nothing to move. const QDir workdir(m_workDir->path()); const QDir outDir(m_dialog->outputLocation()); bool overWriteAll = false; qCDebug(KLEOPATRA_LOG) << workdir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); for (const QFileInfo &fi: workdir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot)) { const auto inpath = fi.absoluteFilePath(); if (fi.isDir()) { // A directory. Assume that the input was an archive // and avoid directory merges by trying to find a non // existing directory. auto candidate = fi.baseName(); if (candidate.startsWith(QLatin1Char('-'))) { // Bug in GpgTar Extracts stdout passed archives to a dir named - candidate = QFileInfo(m_passedFiles.first()).baseName(); } QString suffix; QFileInfo ofi; int i = 0; do { ofi = QFileInfo(outDir.absoluteFilePath(candidate + suffix)); if (!ofi.exists()) { break; } suffix = QStringLiteral("_%1").arg(++i); } while (i < 1000); if (!moveDir(inpath, ofi.absoluteFilePath())) { reportError(makeGnuPGError(GPG_ERR_GENERAL), xi18n("Failed to move %1 to %2.", inpath, ofi.absoluteFilePath())); } continue; } const auto embeddedFileName = getEmbeddedFileName(inpath); QString outFileName = fi.fileName(); if (!embeddedFileName.isEmpty() && embeddedFileName != fi.fileName()) { // we switch "Yes" and "No" because Yes is default, but saving with embedded file name could be dangerous - const auto answer = KMessageBox::questionYesNoCancel(m_dialog, - xi18n("Shall the file be saved with the original file name %1?", embeddedFileName), - i18n("Use Original File Name?"), - KGuiItem(xi18n("No, Save As %1", fi.fileName())), - KGuiItem(xi18n("Yes, Save As %1", embeddedFileName))); +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + const auto answer = KMessageBox::questionTwoActionsCancel( + m_dialog, +#else + const auto answer = + KMessageBox::questionYesNoCancel(m_dialog, +#endif + xi18n("Shall the file be saved with the original file name %1?", embeddedFileName), + i18n("Use Original File Name?"), + KGuiItem(xi18n("No, Save As %1", fi.fileName())), + KGuiItem(xi18n("Yes, Save As %1", embeddedFileName))); if (answer == KMessageBox::Cancel) { qCDebug(KLEOPATRA_LOG) << "Saving canceled for:" << inpath; continue; +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + } else if (answer == KMessageBox::ButtonCode::SecondaryAction) { +#else } else if (answer == KMessageBox::No) { +#endif outFileName = embeddedFileName; } } const auto outpath = outDir.absoluteFilePath(outFileName); qCDebug(KLEOPATRA_LOG) << "Moving " << inpath << " to " << outpath; const QFileInfo ofi(outpath); if (ofi.exists()) { int sel = KMessageBox::No; if (!overWriteAll) { - sel = KMessageBox::questionYesNoCancel(m_dialog, i18n("The file %1 already exists.\n" - "Overwrite?", outpath), - i18n("Overwrite Existing File?"), - KStandardGuiItem::overwrite(), - KGuiItem(i18n("Overwrite All")), - KStandardGuiItem::cancel()); +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + sel = KMessageBox::questionTwoActionsCancel(m_dialog, + i18n("The file %1 already exists.\n" +#else + sel = KMessageBox::questionYesNoCancel(m_dialog, + i18n("The file %1 already exists.\n" +#endif + "Overwrite?", + outpath), + i18n("Overwrite Existing File?"), + KStandardGuiItem::overwrite(), + KGuiItem(i18n("Overwrite All")), + KStandardGuiItem::cancel()); } if (sel == KMessageBox::Cancel) { qCDebug(KLEOPATRA_LOG) << "Overwriting canceled for: " << outpath; continue; } +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + if (sel == KMessageBox::ButtonCode::SecondaryAction) { // Overwrite All +#else if (sel == KMessageBox::No) { //Overwrite All +#endif overWriteAll = true; } if (!QFile::remove(outpath)) { reportError(makeGnuPGError(GPG_ERR_GENERAL), xi18n("Failed to delete %1.", outpath)); continue; } } if (!QFile::rename(inpath, outpath)) { reportError(makeGnuPGError(GPG_ERR_GENERAL), xi18n("Failed to move %1 to %2.", inpath, outpath)); } } } q->emitDoneOrError(); delete m_dialog; m_dialog = nullptr; } QVector AutoDecryptVerifyFilesController::Private::classifyAndSortFiles(const QStringList &files) { const auto isSignature = [](int classification) -> bool { return mayBeDetachedSignature(classification) || mayBeOpaqueSignature(classification) || (classification & Class::TypeMask) == Class::ClearsignedMessage; }; QVector out; for (const auto &file : files) { CryptoFile cFile; cFile.fileName = file; cFile.baseName = stripSuffix(file); cFile.classification = classify(file); cFile.protocol = findProtocol(cFile.classification); auto it = std::find_if(out.begin(), out.end(), [&cFile](const CryptoFile &other) { return other.protocol == cFile.protocol && other.baseName == cFile.baseName; }); if (it != out.end()) { // If we found a file with the same basename, make sure that encrypted // file is before the signature file, so that we first decrypt and then // verify if (isSignature(cFile.classification) && isCipherText(it->classification)) { out.insert(it + 1, cFile); } else if (isCipherText(cFile.classification) && isSignature(it->classification)) { out.insert(it, cFile); } else { // both are signatures or both are encrypted files, in which // case order does not matter out.insert(it, cFile); } } else { out.push_back(cFile); } } return out; } std::vector< std::shared_ptr > AutoDecryptVerifyFilesController::Private::buildTasks(const QStringList &fileNames, QStringList &undetected) { // sort files so that we make sure we first decrypt and then verify QVector cryptoFiles = classifyAndSortFiles(fileNames); std::vector > tasks; for (auto it = cryptoFiles.begin(), end = cryptoFiles.end(); it != end; ++it) { auto &cFile = (*it); QFileInfo fi(cFile.fileName); qCDebug(KLEOPATRA_LOG) << "classified" << cFile.fileName << "as" << printableClassification(cFile.classification); if (!fi.isReadable()) { reportError(makeGnuPGError(GPG_ERR_ASS_NO_INPUT), xi18n("Cannot open %1 for reading.", cFile.fileName)); continue; } if (mayBeAnyCertStoreType(cFile.classification)) { // Trying to verify a certificate. Possible because extensions are often similar // for PGP Keys. reportError(makeGnuPGError(GPG_ERR_ASS_NO_INPUT), xi18n("The file %1 contains certificates and can't be decrypted or verified.", cFile.fileName)); qCDebug(KLEOPATRA_LOG) << "reported error"; continue; } // We can't reliably detect CMS detached signatures, so we will try to do // our best to use the current file as a detached signature and fallback to // opaque signature otherwise. if (cFile.protocol == GpgME::CMS && mayBeDetachedSignature(cFile.classification)) { // First, see if previous task was a decryption task for the same file // and "pipe" it's output into our input std::shared_ptr input; bool prepend = false; if (it != cryptoFiles.begin()) { const auto prev = it - 1; if (prev->protocol == cFile.protocol && prev->baseName == cFile.baseName) { input = Input::createFromOutput(prev->output); prepend = true; } } if (!input) { if (QFile::exists(cFile.baseName)) { input = Input::createFromFile(cFile.baseName); } } if (input) { qCDebug(KLEOPATRA_LOG) << "Detached CMS verify: " << cFile.fileName; std::shared_ptr t(new VerifyDetachedTask); t->setInput(Input::createFromFile(cFile.fileName)); t->setSignedData(input); t->setProtocol(cFile.protocol); if (prepend) { // Put the verify task BEFORE the decrypt task in the tasks queue, // because the tasks are executed in reverse order! tasks.insert(tasks.end() - 1, t); } else { tasks.push_back(t); } continue; } else { // No signed data, maybe not a detached signature } } if (isDetachedSignature(cFile.classification)) { // Detached signature, try to find data or ask the user. QString signedDataFileName = cFile.baseName; if (!QFile::exists(signedDataFileName)) { signedDataFileName = QFileDialog::getOpenFileName(nullptr, xi18n("Select the file to verify with the signature %1", fi.fileName()), fi.path()); } if (signedDataFileName.isEmpty()) { qCDebug(KLEOPATRA_LOG) << "No signed data selected. Verify aborted."; } else { qCDebug(KLEOPATRA_LOG) << "Detached verify: " << cFile.fileName << " Data: " << signedDataFileName; std::shared_ptr t(new VerifyDetachedTask); t->setInput(Input::createFromFile(cFile.fileName)); t->setSignedData(Input::createFromFile(signedDataFileName)); t->setProtocol(cFile.protocol); tasks.push_back(t); } continue; } if (!mayBeAnyMessageType(cFile.classification)) { // Not a Message? Maybe there is a signature for this file? const auto signatures = findSignatures(cFile.fileName); bool foundSig = false; if (!signatures.empty()) { for (const QString &sig : signatures) { const auto classification = classify(sig); qCDebug(KLEOPATRA_LOG) << "Guessing: " << sig << " is a signature for: " << cFile.fileName << "Classification: " << classification; const auto proto = findProtocol(classification); if (proto == GpgME::UnknownProtocol) { qCDebug(KLEOPATRA_LOG) << "Could not determine protocol. Skipping guess."; continue; } foundSig = true; std::shared_ptr t(new VerifyDetachedTask); t->setInput(Input::createFromFile(sig)); t->setSignedData(Input::createFromFile(cFile.fileName)); t->setProtocol(proto); tasks.push_back(t); } } if (!foundSig) { undetected << cFile.fileName; qCDebug(KLEOPATRA_LOG) << "Failed detection for: " << cFile.fileName << " adding to undetected."; } } else { const FileOperationsPreferences fileOpSettings; // Any Message type so we have input and output. const auto input = Input::createFromFile(cFile.fileName); std::shared_ptr ad; if (fileOpSettings.autoExtractArchives()) { const auto archiveDefinitions = ArchiveDefinition::getArchiveDefinitions(); ad = q->pick_archive_definition(cFile.protocol, archiveDefinitions, cFile.fileName); } if (fileOpSettings.dontUseTmpDir()) { if (!m_workDir) { m_workDir = std::make_unique(heuristicBaseDirectory(fileNames) + QStringLiteral("/kleopatra-XXXXXX")); } if (!m_workDir->isValid()) { qCDebug(KLEOPATRA_LOG) << heuristicBaseDirectory(fileNames) << "not a valid temporary directory."; m_workDir.reset(); } } if (!m_workDir) { m_workDir = std::make_unique(); } qCDebug(KLEOPATRA_LOG) << "Using:" << m_workDir->path() << "as temporary directory."; const auto wd = QDir(m_workDir->path()); const auto output = ad ? ad->createOutputFromUnpackCommand(cFile.protocol, cFile.fileName, wd) : Output::createFromFile(wd.absoluteFilePath(outputFileName(fi.fileName())), false); // If this might be opaque CMS signature, then try that. We already handled // detached CMS signature above const auto isCMSOpaqueSignature = cFile.protocol == GpgME::CMS && mayBeOpaqueSignature(cFile.classification); if (isOpaqueSignature(cFile.classification) || isCMSOpaqueSignature) { qCDebug(KLEOPATRA_LOG) << "creating a VerifyOpaqueTask"; std::shared_ptr t(new VerifyOpaqueTask); t->setInput(input); t->setOutput(output); t->setProtocol(cFile.protocol); tasks.push_back(t); } else { // Any message. That is not an opaque signature needs to be // decrypted. Verify we always do because we can't know if // an encrypted message is also signed. qCDebug(KLEOPATRA_LOG) << "creating a DecryptVerifyTask"; std::shared_ptr t(new DecryptVerifyTask); t->setInput(input); t->setOutput(output); t->setProtocol(cFile.protocol); cFile.output = output; tasks.push_back(t); } } } return tasks; } void AutoDecryptVerifyFilesController::setFiles(const QStringList &files) { d->m_passedFiles = files; } AutoDecryptVerifyFilesController::AutoDecryptVerifyFilesController(QObject *parent) : DecryptVerifyFilesController(parent), d(new Private(this)) { } AutoDecryptVerifyFilesController::AutoDecryptVerifyFilesController(const std::shared_ptr &ctx, QObject *parent) : DecryptVerifyFilesController(ctx, parent), d(new Private(this)) { } AutoDecryptVerifyFilesController::~AutoDecryptVerifyFilesController() { qCDebug(KLEOPATRA_LOG); } void AutoDecryptVerifyFilesController::start() { d->exec(); } void AutoDecryptVerifyFilesController::setOperation(DecryptVerifyOperation op) { d->m_operation = op; } DecryptVerifyOperation AutoDecryptVerifyFilesController::operation() const { return d->m_operation; } void AutoDecryptVerifyFilesController::Private::cancelAllTasks() { // we just kill all runnable tasks - this will not result in // signal emissions. m_runnableTasks.clear(); // a cancel() will result in a call to if (m_runningTask) { m_runningTask->cancel(); } } void AutoDecryptVerifyFilesController::cancel() { qCDebug(KLEOPATRA_LOG); try { d->m_errorDetected = true; if (d->m_dialog) { d->m_dialog->close(); } d->cancelAllTasks(); } catch (const std::exception &e) { qCDebug(KLEOPATRA_LOG) << "Caught exception: " << e.what(); } } void AutoDecryptVerifyFilesController::doTaskDone(const Task *task, const std::shared_ptr &result) { Q_ASSERT(task); Q_UNUSED(task) // We could just delete the tasks here, but we can't use // Qt::QueuedConnection here (we need sender()) and other slots // might not yet have executed. Therefore, we push completed tasks // into a burial container d->m_completedTasks.push_back(d->m_runningTask); d->m_runningTask.reset(); if (const std::shared_ptr &dvr = std::dynamic_pointer_cast(result)) { d->m_results.push_back(dvr); } QTimer::singleShot(0, this, SLOT(schedule())); } #include "moc_autodecryptverifyfilescontroller.cpp" + diff --git a/src/dialogs/certificatedetailswidget.cpp b/src/dialogs/certificatedetailswidget.cpp index 6de9f710e..0dece5dc8 100644 --- a/src/dialogs/certificatedetailswidget.cpp +++ b/src/dialogs/certificatedetailswidget.cpp @@ -1,1144 +1,1159 @@ /* dialogs/certificatedetailswidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2017 Intevation GmbH SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-FileCopyrightText: 2022 Felix Tiede SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "certificatedetailswidget.h" #include "kleopatra_debug.h" #include "exportdialog.h" #include "trustchainwidget.h" #include "subkeyswidget.h" #include "weboftrustdialog.h" #include "commands/changepassphrasecommand.h" #include "commands/changeexpirycommand.h" #include "commands/certifycertificatecommand.h" #ifdef MAILAKONADI_ENABLED #include "commands/exportopenpgpcerttoprovidercommand.h" #endif // MAILAKONADI_ENABLED #include "commands/refreshcertificatecommand.h" #include "commands/revokecertificationcommand.h" #include "commands/revokeuseridcommand.h" #include "commands/setprimaryuseridcommand.h" #include "commands/adduseridcommand.h" #include "commands/genrevokecommand.h" #include "commands/detailscommand.h" #include "commands/dumpcertificatecommand.h" #include "utils/accessibility.h" #include "utils/keys.h" #include "utils/tags.h" #include "view/infofield.h" #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if __has_include() #include #define USE_RANGES #endif #include Q_DECLARE_METATYPE(GpgME::UserID) using namespace Kleo; namespace { std::vector selectedUserIDs(const QTreeWidget *treeWidget) { if (!treeWidget) { return {}; } std::vector userIDs; const auto selected = treeWidget->selectedItems(); std::transform(selected.begin(), selected.end(), std::back_inserter(userIDs), [](const QTreeWidgetItem *item) { return item->data(0, Qt::UserRole).value(); }); return userIDs; } } class CertificateDetailsWidget::Private { public: Private(CertificateDetailsWidget *qq); void setupCommonProperties(); void updateUserIDActions(); void setUpUserIDTable(); void setUpSMIMEAdressList(); void setupPGPProperties(); void setupSMIMEProperties(); void revokeUserID(const GpgME::UserID &uid); void revokeSelectedUserID(); void genRevokeCert(); void refreshCertificate(); void certifyUserIDs(); void revokeCertifications(); void webOfTrustClicked(); void exportClicked(); void addUserID(); void setPrimaryUserID(const GpgME::UserID &uid = {}); void changePassphrase(); void changeExpiration(); void keysMayHaveChanged(); void showTrustChainDialog(); void showMoreDetails(); void userIDTableContextMenuRequested(const QPoint &p); QString tofuTooltipString(const GpgME::UserID &uid) const; QIcon trustLevelIcon(const GpgME::UserID &uid) const; QString trustLevelText(const GpgME::UserID &uid) const; void showIssuerCertificate(); void updateKey(); void setUpdatedKey(const GpgME::Key &key); void keyListDone(const GpgME::KeyListResult &, const std::vector &, const QString &, const GpgME::Error &); void copyFingerprintToClipboard(); private: CertificateDetailsWidget *const q; public: GpgME::Key key; bool updateInProgress = false; private: InfoField *attributeField(const QString &attributeName) { const auto keyValuePairIt = ui.smimeAttributeFields.find(attributeName); if (keyValuePairIt != ui.smimeAttributeFields.end()) { return (*keyValuePairIt).second.get(); } return nullptr; } private: struct UI { QWidget *userIDs = nullptr; QLabel *userIDTableLabel = nullptr; NavigatableTreeWidget *userIDTable = nullptr; QPushButton *addUserIDBtn = nullptr; QPushButton *setPrimaryUserIDBtn = nullptr; QPushButton *certifyBtn = nullptr; QPushButton *revokeCertificationsBtn = nullptr; QPushButton *revokeUserIDBtn = nullptr; QPushButton *webOfTrustBtn = nullptr; std::map> smimeAttributeFields; std::unique_ptr smimeTrustLevelField; std::unique_ptr validFromField; std::unique_ptr expiresField; QAction *changeExpirationAction = nullptr; std::unique_ptr fingerprintField; QAction *copyFingerprintAction = nullptr; std::unique_ptr smimeIssuerField; QAction *showIssuerCertificateAction = nullptr; std::unique_ptr complianceField; std::unique_ptr trustedIntroducerField; QLabel *smimeRelatedAddresses = nullptr; QListWidget *smimeAddressList = nullptr; QPushButton *moreDetailsBtn = nullptr; QPushButton *trustChainDetailsBtn = nullptr; QPushButton *refreshBtn = nullptr; QPushButton *changePassphraseBtn = nullptr; QPushButton *exportBtn = nullptr; QPushButton *genRevokeBtn = nullptr; void setupUi(QWidget *parent) { auto mainLayout = new QVBoxLayout{parent}; userIDs = new QWidget{parent}; { auto userIDsLayout = new QVBoxLayout{userIDs}; userIDsLayout->setContentsMargins({}); userIDTableLabel = new QLabel(i18n("User IDs:"), parent); userIDsLayout->addWidget(userIDTableLabel); userIDTable = new NavigatableTreeWidget{parent}; userIDTableLabel->setBuddy(userIDTable); userIDTable->setAccessibleName(i18n("User IDs")); QTreeWidgetItem *__qtreewidgetitem = new QTreeWidgetItem(); __qtreewidgetitem->setText(0, QString::fromUtf8("1")); userIDTable->setHeaderItem(__qtreewidgetitem); userIDTable->setEditTriggers(QAbstractItemView::NoEditTriggers); userIDTable->setSelectionMode(QAbstractItemView::ExtendedSelection); userIDTable->setRootIsDecorated(false); userIDTable->setUniformRowHeights(true); userIDTable->setAllColumnsShowFocus(false); userIDsLayout->addWidget(userIDTable); { auto buttonRow = new QHBoxLayout; addUserIDBtn = new QPushButton(i18nc("@action:button", "Add User ID"), parent); buttonRow->addWidget(addUserIDBtn); setPrimaryUserIDBtn = new QPushButton{i18nc("@action:button", "Flag as Primary"), parent}; setPrimaryUserIDBtn->setToolTip(i18nc("@info:tooltip", "Flag the selected user ID as the primary user ID of this key.")); buttonRow->addWidget(setPrimaryUserIDBtn); certifyBtn = new QPushButton(i18nc("@action:button", "Certify User IDs"), parent); buttonRow->addWidget(certifyBtn); webOfTrustBtn = new QPushButton(i18nc("@action:button", "Show Certifications"), parent); buttonRow->addWidget(webOfTrustBtn); revokeCertificationsBtn = new QPushButton(i18nc("@action:button", "Revoke Certifications"), parent); buttonRow->addWidget(revokeCertificationsBtn); revokeUserIDBtn = new QPushButton(i18nc("@action:button", "Revoke User ID"), parent); buttonRow->addWidget(revokeUserIDBtn); buttonRow->addStretch(1); userIDsLayout->addLayout(buttonRow); } userIDsLayout->addWidget(new KSeparator{Qt::Horizontal, parent}); } mainLayout->addWidget(userIDs); { auto gridLayout = new QGridLayout; gridLayout->setColumnStretch(1, 1); int row = -1; for (const auto &attribute : DN::attributeOrder()) { const auto attributeLabel = DN::attributeNameToLabel(attribute); if (attributeLabel.isEmpty()) { continue; } const auto labelWithColon = i18nc("interpunctation for labels", "%1:", attributeLabel); const auto & [it, inserted] = smimeAttributeFields.try_emplace(attribute, std::make_unique(labelWithColon, parent)); if (inserted) { row++; const auto &field = it->second; gridLayout->addWidget(field->label(), row, 0); gridLayout->addLayout(field->layout(), row, 1); } } row++; smimeTrustLevelField = std::make_unique(i18n("Trust level:"), parent); gridLayout->addWidget(smimeTrustLevelField->label(), row, 0); gridLayout->addLayout(smimeTrustLevelField->layout(), row, 1); row++; validFromField = std::make_unique(i18n("Valid from:"), parent); gridLayout->addWidget(validFromField->label(), row, 0); gridLayout->addLayout(validFromField->layout(), row, 1); row++; expiresField = std::make_unique(i18n("Valid until:"), parent); changeExpirationAction = new QAction{parent}; changeExpirationAction->setIcon(QIcon::fromTheme(QStringLiteral("editor"))); changeExpirationAction->setToolTip(i18nc("@info:tooltip", "Change the end of the validity period")); Kleo::setAccessibleName(changeExpirationAction, i18nc("@action:button", "Change Validity")); expiresField->setAction(changeExpirationAction); gridLayout->addWidget(expiresField->label(), row, 0); gridLayout->addLayout(expiresField->layout(), row, 1); row++; fingerprintField = std::make_unique(i18n("Fingerprint:"), parent); if (QGuiApplication::clipboard()) { copyFingerprintAction = new QAction{parent}; copyFingerprintAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); copyFingerprintAction->setToolTip(i18nc("@info:tooltip", "Copy the fingerprint to the clipboard")); Kleo::setAccessibleName(copyFingerprintAction, i18nc("@action:button", "Copy fingerprint")); fingerprintField->setAction(copyFingerprintAction); } gridLayout->addWidget(fingerprintField->label(), row, 0); gridLayout->addLayout(fingerprintField->layout(), row, 1); row++; smimeIssuerField = std::make_unique(i18n("Issuer:"), parent); showIssuerCertificateAction = new QAction{parent}; showIssuerCertificateAction->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information"))); showIssuerCertificateAction->setToolTip(i18nc("@info:tooltip", "Show the issuer certificate")); Kleo::setAccessibleName(showIssuerCertificateAction, i18nc("@action:button", "Show certificate")); smimeIssuerField->setAction(showIssuerCertificateAction); gridLayout->addWidget(smimeIssuerField->label(), row, 0); gridLayout->addLayout(smimeIssuerField->layout(), row, 1); row++; complianceField = std::make_unique(i18n("Compliance:"), parent); gridLayout->addWidget(complianceField->label(), row, 0); gridLayout->addLayout(complianceField->layout(), row, 1); row++; trustedIntroducerField = std::make_unique(i18n("Trusted introducer for:"), parent); gridLayout->addWidget(trustedIntroducerField->label(), row, 0); trustedIntroducerField->setToolTip(i18n("See certifications for details.")); gridLayout->addLayout(trustedIntroducerField->layout(), row, 1); mainLayout->addLayout(gridLayout); } smimeRelatedAddresses = new QLabel(i18n("Related addresses:"), parent); mainLayout->addWidget(smimeRelatedAddresses); smimeAddressList = new QListWidget{parent}; smimeRelatedAddresses->setBuddy(smimeAddressList); smimeAddressList->setAccessibleName(i18n("Related addresses")); smimeAddressList->setEditTriggers(QAbstractItemView::NoEditTriggers); smimeAddressList->setSelectionMode(QAbstractItemView::SingleSelection); mainLayout->addWidget(smimeAddressList); mainLayout->addStretch(); { auto buttonRow = new QHBoxLayout; moreDetailsBtn = new QPushButton(i18nc("@action:button", "More Details..."), parent); buttonRow->addWidget(moreDetailsBtn); trustChainDetailsBtn = new QPushButton(i18nc("@action:button", "Trust Chain Details"), parent); buttonRow->addWidget(trustChainDetailsBtn); refreshBtn = new QPushButton{i18nc("@action:button", "Update"), parent}; #ifndef QGPGME_SUPPORTS_KEY_REFRESH refreshBtn->setVisible(false); #endif buttonRow->addWidget(refreshBtn); exportBtn = new QPushButton(i18nc("@action:button", "Export"), parent); buttonRow->addWidget(exportBtn); changePassphraseBtn = new QPushButton(i18nc("@action:button", "Change Passphrase"), parent); buttonRow->addWidget(changePassphraseBtn); genRevokeBtn = new QPushButton(i18nc("@action:button", "Generate Revocation Certificate"), parent); genRevokeBtn->setToolTip(u"" % i18n("A revocation certificate is a file that serves as a \"kill switch\" to publicly " "declare that a key shall not anymore be used. It is not possible " "to retract such a revocation certificate once it has been published.") % u""); buttonRow->addWidget(genRevokeBtn); buttonRow->addStretch(1); mainLayout->addLayout(buttonRow); } } } ui; }; CertificateDetailsWidget::Private::Private(CertificateDetailsWidget *qq) : q{qq} { ui.setupUi(q); ui.userIDTable->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui.userIDTable, &QAbstractItemView::customContextMenuRequested, q, [this](const QPoint &p) { userIDTableContextMenuRequested(p); }); connect(ui.userIDTable, &QTreeWidget::itemSelectionChanged, q, [this]() { updateUserIDActions(); }); connect(ui.addUserIDBtn, &QPushButton::clicked, q, [this]() { addUserID(); }); connect(ui.setPrimaryUserIDBtn, &QPushButton::clicked, q, [this]() { setPrimaryUserID(); }); connect(ui.revokeUserIDBtn, &QPushButton::clicked, q, [this]() { revokeSelectedUserID(); }); connect(ui.changePassphraseBtn, &QPushButton::clicked, q, [this]() { changePassphrase(); }); connect(ui.genRevokeBtn, &QPushButton::clicked, q, [this]() { genRevokeCert(); }); connect(ui.changeExpirationAction, &QAction::triggered, q, [this]() { changeExpiration(); }); connect(ui.showIssuerCertificateAction, &QAction::triggered, q, [this]() { showIssuerCertificate(); }); connect(ui.trustChainDetailsBtn, &QPushButton::pressed, q, [this]() { showTrustChainDialog(); }); connect(ui.moreDetailsBtn, &QPushButton::pressed, q, [this]() { showMoreDetails(); }); connect(ui.refreshBtn, &QPushButton::clicked, q, [this]() { refreshCertificate(); }); connect(ui.certifyBtn, &QPushButton::clicked, q, [this]() { certifyUserIDs(); }); connect(ui.revokeCertificationsBtn, &QPushButton::clicked, q, [this]() { revokeCertifications(); }); connect(ui.webOfTrustBtn, &QPushButton::clicked, q, [this]() { webOfTrustClicked(); }); connect(ui.exportBtn, &QPushButton::clicked, q, [this]() { exportClicked(); }); if (ui.copyFingerprintAction) { connect(ui.copyFingerprintAction, &QAction::triggered, q, [this]() { copyFingerprintToClipboard(); }); } connect(Kleo::KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, q, [this]() { keysMayHaveChanged(); }); } void CertificateDetailsWidget::Private::setupCommonProperties() { const bool isOpenPGP = key.protocol() == GpgME::OpenPGP; const bool isSMIME = key.protocol() == GpgME::CMS; const bool isOwnKey = key.hasSecret(); const auto isLocalKey = !isRemoteKey(key); const auto keyCanBeCertified = Kleo::canBeCertified(key); // update visibility of UI elements ui.userIDs->setVisible(isOpenPGP); ui.addUserIDBtn->setVisible(isOwnKey); #ifdef QGPGME_SUPPORTS_SET_PRIMARY_UID ui.setPrimaryUserIDBtn->setVisible(isOwnKey); #else ui.setPrimaryUserIDBtn->setVisible(false); #endif // ui.certifyBtn->setVisible(true); // always visible (for OpenPGP keys) // ui.webOfTrustBtn->setVisible(true); // always visible (for OpenPGP keys) ui.revokeCertificationsBtn->setVisible(Kleo::Commands::RevokeCertificationCommand::isSupported()); ui.revokeUserIDBtn->setVisible(isOwnKey); for (const auto &[_, field] : ui.smimeAttributeFields) { field->setVisible(isSMIME); } ui.smimeTrustLevelField->setVisible(isSMIME); // ui.validFromField->setVisible(true); // always visible // ui.expiresField->setVisible(true); // always visible if (isOpenPGP && isOwnKey) { ui.expiresField->setAction(ui.changeExpirationAction); } else { ui.expiresField->setAction(nullptr); } // ui.fingerprintField->setVisible(true); // always visible ui.smimeIssuerField->setVisible(isSMIME); ui.complianceField->setVisible(DeVSCompliance::isCompliant()); ui.trustedIntroducerField->setVisible(isOpenPGP); // may be hidden again by setupPGPProperties() ui.smimeRelatedAddresses->setVisible(isSMIME); ui.smimeAddressList->setVisible(isSMIME); ui.moreDetailsBtn->setVisible(isLocalKey); ui.trustChainDetailsBtn->setVisible(isSMIME); ui.refreshBtn->setVisible(isLocalKey); ui.changePassphraseBtn->setVisible(isSecretKeyStoredInKeyRing(key)); ui.exportBtn->setVisible(isLocalKey); ui.genRevokeBtn->setVisible(isOpenPGP && isOwnKey); // update availability of buttons const auto userCanSignUserIDs = userHasCertificationKey(); ui.addUserIDBtn->setEnabled(canBeUsedForSecretKeyOperations(key)); ui.setPrimaryUserIDBtn->setEnabled(false); // requires a selected user ID ui.certifyBtn->setEnabled(isLocalKey && keyCanBeCertified && userCanSignUserIDs); ui.webOfTrustBtn->setEnabled(isLocalKey); ui.revokeCertificationsBtn->setEnabled(userCanSignUserIDs && isLocalKey); ui.revokeUserIDBtn->setEnabled(false); // requires a selected user ID ui.changeExpirationAction->setEnabled(canBeUsedForSecretKeyOperations(key)); ui.changePassphraseBtn->setEnabled(isSecretKeyStoredInKeyRing(key)); ui.genRevokeBtn->setEnabled(canBeUsedForSecretKeyOperations(key)); // update values of protocol-independent UI elements ui.validFromField->setValue(Formatting::creationDateString(key), Formatting::accessibleCreationDate(key)); ui.expiresField->setValue(Formatting::expirationDateString(key, i18nc("Valid until:", "unlimited")), Formatting::accessibleExpirationDate(key)); ui.fingerprintField->setValue(Formatting::prettyID(key.primaryFingerprint()), Formatting::accessibleHexID(key.primaryFingerprint())); if (DeVSCompliance::isCompliant()) { ui.complianceField->setValue(Kleo::Formatting::complianceStringForKey(key)); } } void CertificateDetailsWidget::Private::updateUserIDActions() { const auto userIDs = selectedUserIDs(ui.userIDTable); const auto singleUserID = userIDs.size() == 1 ? userIDs.front() : GpgME::UserID{}; const bool isPrimaryUserID = !singleUserID.isNull() && (ui.userIDTable->selectedItems().front() == ui.userIDTable->topLevelItem(0)); ui.setPrimaryUserIDBtn->setEnabled(!singleUserID.isNull() // && !isPrimaryUserID // && !Kleo::isRevokedOrExpired(singleUserID) // && canBeUsedForSecretKeyOperations(key)); ui.revokeUserIDBtn->setEnabled(!singleUserID.isNull() && canCreateCertifications(key) && canRevokeUserID(singleUserID)); } void CertificateDetailsWidget::Private::setUpUserIDTable() { ui.userIDTable->clear(); QStringList headers = { i18n("Email"), i18n("Name"), i18n("Trust Level"), i18n("Tags") }; ui.userIDTable->setColumnCount(headers.count()); ui.userIDTable->setColumnWidth(0, 200); ui.userIDTable->setColumnWidth(1, 200); ui.userIDTable->setHeaderLabels(headers); const auto uids = key.userIDs(); for (unsigned int i = 0; i < uids.size(); ++i) { const auto &uid = uids[i]; auto item = new QTreeWidgetItem; const QString toolTip = tofuTooltipString(uid); item->setData(0, Qt::UserRole, QVariant::fromValue(uid)); auto pMail = Kleo::Formatting::prettyEMail(uid); auto pName = Kleo::Formatting::prettyName(uid); item->setData(0, Qt::DisplayRole, pMail); item->setData(0, Qt::ToolTipRole, toolTip); item->setData(0, Qt::AccessibleTextRole, pMail.isEmpty() ? i18nc("text for screen readers for an empty email address", "no email") : pMail); item->setData(1, Qt::DisplayRole, pName); item->setData(1, Qt::ToolTipRole, toolTip); item->setData(2, Qt::DecorationRole, trustLevelIcon(uid)); item->setData(2, Qt::DisplayRole, trustLevelText(uid)); item->setData(2, Qt::ToolTipRole, toolTip); GpgME::Error err; QStringList tagList; for (const auto &tag: uid.remarks(Tags::tagKeys(), err)) { if (err) { qCWarning(KLEOPATRA_LOG) << "Getting remarks for user ID" << uid.id() << "failed:" << err; } tagList << QString::fromStdString(tag); } qCDebug(KLEOPATRA_LOG) << "tagList:" << tagList; const auto tags = tagList.join(QStringLiteral("; ")); item->setData(3, Qt::DisplayRole, tags); item->setData(3, Qt::ToolTipRole, toolTip); ui.userIDTable->addTopLevelItem(item); } if (!Tags::tagsEnabled()) { ui.userIDTable->hideColumn(3); } } void CertificateDetailsWidget::Private::setUpSMIMEAdressList() { ui.smimeAddressList->clear(); const auto *const emailField = attributeField(QStringLiteral("EMAIL")); // add email address from primary user ID if not listed already as attribute field if (!emailField) { const auto ownerId = key.userID(0); const Kleo::DN dn(ownerId.id()); const QString dnEmail = dn[QStringLiteral("EMAIL")]; if (!dnEmail.isEmpty()) { ui.smimeAddressList->addItem(dnEmail); } } if (key.numUserIDs() > 1) { // iterate over the secondary user IDs #ifdef USE_RANGES for (const auto uids = key.userIDs(); const auto &uid : std::ranges::subrange(std::next(uids.begin()), uids.end())) { #else const auto uids = key.userIDs(); for (auto it = std::next(uids.begin()); it != uids.end(); ++it) { const auto &uid = *it; #endif const auto name = Kleo::Formatting::prettyName(uid); const auto email = Kleo::Formatting::prettyEMail(uid); QString itemText; if (name.isEmpty() && !email.isEmpty()) { // skip email addresses already listed in email attribute field if (emailField && email == emailField->value()) { continue; } itemText = email; } else { // S/MIME certificates sometimes contain urls where both // name and mail is empty. In that case we print whatever // the uid is as name. // // Can be ugly like (3:uri24:http://ca.intevation.org), but // this is better then showing an empty entry. itemText = QString::fromUtf8(uid.id()); } // avoid duplicate entries in the list if (ui.smimeAddressList->findItems(itemText, Qt::MatchExactly).empty()) { ui.smimeAddressList->addItem(itemText); } } } if (ui.smimeAddressList->count() == 0) { ui.smimeRelatedAddresses->setVisible(false); ui.smimeAddressList->setVisible(false); } } void CertificateDetailsWidget::Private::revokeUserID(const GpgME::UserID &userId) { const QString message = xi18nc( "@info", "Do you really want to revoke the user ID%1 ?", QString::fromUtf8(userId.id())); auto confirmButton = KStandardGuiItem::yes(); confirmButton.setText(i18nc("@action:button", "Revoke User ID")); confirmButton.setToolTip({}); +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + const auto choice = KMessageBox::questionTwoActions( +#else const auto choice = KMessageBox::questionYesNo( - q->window(), message, i18nc("@title:window", "Confirm Revocation"), - confirmButton, KStandardGuiItem::cancel(), {}, KMessageBox::Notify | KMessageBox::WindowModal); +#endif + q->window(), + message, + i18nc("@title:window", "Confirm Revocation"), + confirmButton, + KStandardGuiItem::cancel(), + {}, + KMessageBox::Notify | KMessageBox::WindowModal); +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + if (choice != KMessageBox::ButtonCode::PrimaryAction) { +#else if (choice != KMessageBox::Yes) { +#endif return; } auto cmd = new Commands::RevokeUserIDCommand(userId); cmd->setParentWidget(q); connect(cmd, &Command::finished, q, [this]() { ui.userIDTable->setEnabled(true); // the Revoke User ID button will be updated by the key update updateKey(); }); ui.userIDTable->setEnabled(false); ui.revokeUserIDBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::revokeSelectedUserID() { const auto userIDs = selectedUserIDs(ui.userIDTable); if (userIDs.size() != 1) { return; } revokeUserID(userIDs.front()); } void CertificateDetailsWidget::Private::changeExpiration() { auto cmd = new Kleo::Commands::ChangeExpiryCommand(key); QObject::connect(cmd, &Kleo::Commands::ChangeExpiryCommand::finished, q, [this]() { ui.changeExpirationAction->setEnabled(true); }); ui.changeExpirationAction->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::changePassphrase() { auto cmd = new Kleo::Commands::ChangePassphraseCommand(key); QObject::connect(cmd, &Kleo::Commands::ChangePassphraseCommand::finished, q, [this]() { ui.changePassphraseBtn->setEnabled(true); }); ui.changePassphraseBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::genRevokeCert() { auto cmd = new Kleo::Commands::GenRevokeCommand(key); QObject::connect(cmd, &Kleo::Commands::GenRevokeCommand::finished, q, [this]() { ui.genRevokeBtn->setEnabled(true); }); ui.genRevokeBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::refreshCertificate() { auto cmd = new Kleo::RefreshCertificateCommand{key}; QObject::connect(cmd, &Kleo::RefreshCertificateCommand::finished, q, [this]() { ui.refreshBtn->setEnabled(true); }); ui.refreshBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::certifyUserIDs() { const auto userIDs = selectedUserIDs(ui.userIDTable); auto cmd = userIDs.empty() ? new Kleo::Commands::CertifyCertificateCommand{key} // : new Kleo::Commands::CertifyCertificateCommand{userIDs}; QObject::connect(cmd, &Kleo::Commands::CertifyCertificateCommand::finished, q, [this]() { updateKey(); ui.certifyBtn->setEnabled(true); }); ui.certifyBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::revokeCertifications() { const auto userIDs = selectedUserIDs(ui.userIDTable); auto cmd = userIDs.empty() ? new Kleo::Commands::RevokeCertificationCommand{key} // : new Kleo::Commands::RevokeCertificationCommand{userIDs}; QObject::connect(cmd, &Kleo::Command::finished, q, [this]() { updateKey(); ui.revokeCertificationsBtn->setEnabled(true); }); ui.revokeCertificationsBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::webOfTrustClicked() { QScopedPointer dlg(new WebOfTrustDialog(q)); dlg->setKey(key); dlg->exec(); } void CertificateDetailsWidget::Private::exportClicked() { QScopedPointer dlg(new ExportDialog(q)); dlg->setKey(key); dlg->exec(); } void CertificateDetailsWidget::Private::addUserID() { auto cmd = new Kleo::Commands::AddUserIDCommand(key); QObject::connect(cmd, &Kleo::Commands::AddUserIDCommand::finished, q, [this]() { ui.addUserIDBtn->setEnabled(true); updateKey(); }); ui.addUserIDBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::setPrimaryUserID(const GpgME::UserID &uid) { auto userId = uid; if (userId.isNull()) { const auto userIDs = selectedUserIDs(ui.userIDTable); if (userIDs.size() != 1) { return; } userId = userIDs.front(); } auto cmd = new Kleo::Commands::SetPrimaryUserIDCommand(userId); QObject::connect(cmd, &Kleo::Commands::SetPrimaryUserIDCommand::finished, q, [this]() { ui.userIDTable->setEnabled(true); // the Flag As Primary button will be updated by the key update updateKey(); }); ui.userIDTable->setEnabled(false); ui.setPrimaryUserIDBtn->setEnabled(false); cmd->start(); } namespace { void ensureThatKeyDetailsAreLoaded(GpgME::Key &key) { if (key.userID(0).numSignatures() == 0) { key.update(); } } } void CertificateDetailsWidget::Private::keysMayHaveChanged() { auto newKey = Kleo::KeyCache::instance()->findByFingerprint(key.primaryFingerprint()); if (!newKey.isNull()) { ensureThatKeyDetailsAreLoaded(newKey); setUpdatedKey(newKey); } } void CertificateDetailsWidget::Private::showTrustChainDialog() { QScopedPointer dlg(new TrustChainDialog(q)); dlg->setKey(key); dlg->exec(); } void CertificateDetailsWidget::Private::userIDTableContextMenuRequested(const QPoint &p) { const auto userIDs = selectedUserIDs(ui.userIDTable); const auto singleUserID = (userIDs.size() == 1) ? userIDs.front() : GpgME::UserID{}; #ifdef QGPGME_SUPPORTS_SET_PRIMARY_UID const bool isPrimaryUserID = !singleUserID.isNull() && (ui.userIDTable->selectedItems().front() == ui.userIDTable->topLevelItem(0)); #endif const bool canSignUserIDs = userHasCertificationKey(); const auto isLocalKey = !isRemoteKey(key); const auto keyCanBeCertified = Kleo::canBeCertified(key); auto menu = new QMenu(q); #ifdef QGPGME_SUPPORTS_SET_PRIMARY_UID if (key.hasSecret()) { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("favorite")), i18nc("@action:inmenu", "Flag as Primary User ID"), q, [this, singleUserID]() { setPrimaryUserID(singleUserID); }); action->setEnabled(!singleUserID.isNull() // && !isPrimaryUserID // && !Kleo::isRevokedOrExpired(singleUserID) // && canBeUsedForSecretKeyOperations(key)); } #endif { const auto actionText = userIDs.empty() ? i18nc("@action:inmenu", "Certify User IDs...") : i18ncp("@action:inmenu", "Certify User ID...", "Certify User IDs...", userIDs.size()); auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-sign")), actionText, q, [this]() { certifyUserIDs(); }); action->setEnabled(isLocalKey && keyCanBeCertified && canSignUserIDs); } if (Kleo::Commands::RevokeCertificationCommand::isSupported()) { const auto actionText = userIDs.empty() ? i18nc("@action:inmenu", "Revoke Certifications...") : i18ncp("@action:inmenu", "Revoke Certification...", "Revoke Certifications...", userIDs.size()); auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-revoke")), actionText, q, [this]() { revokeCertifications(); }); action->setEnabled(isLocalKey && canSignUserIDs); } #ifdef MAILAKONADI_ENABLED if (key.hasSecret()) { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-export")), i18nc("@action:inmenu", "Publish at Mail Provider ..."), q, [this, singleUserID]() { auto cmd = new Kleo::Commands::ExportOpenPGPCertToProviderCommand(singleUserID); ui.userIDTable->setEnabled(false); connect(cmd, &Kleo::Commands::ExportOpenPGPCertToProviderCommand::finished, q, [this]() { ui.userIDTable->setEnabled(true); }); cmd->start(); }); action->setEnabled(!singleUserID.isNull()); } #endif // MAILAKONADI_ENABLED { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-revoke")), i18nc("@action:inmenu", "Revoke User ID"), q, [this, singleUserID]() { revokeUserID(singleUserID); }); action->setEnabled(!singleUserID.isNull() && canCreateCertifications(key) && canRevokeUserID(singleUserID)); } connect(menu, &QMenu::aboutToHide, menu, &QObject::deleteLater); menu->popup(ui.userIDTable->viewport()->mapToGlobal(p)); } void CertificateDetailsWidget::Private::showMoreDetails() { if (key.protocol() == GpgME::CMS) { auto cmd = new Kleo::Commands::DumpCertificateCommand(key); cmd->setParentWidget(q); cmd->setUseDialog(true); cmd->start(); } else { QScopedPointer dlg(new SubKeysDialog(q)); dlg->setKey(key); dlg->exec(); } } QString CertificateDetailsWidget::Private::tofuTooltipString(const GpgME::UserID &uid) const { const auto tofu = uid.tofuInfo(); if (tofu.isNull()) { return QString(); } QString html = QStringLiteral(""); const auto appendRow = [&html](const QString &lbl, const QString &val) { html += QStringLiteral("" "" "" "") .arg(lbl, val); }; const auto appendHeader = [this, &html](const QString &hdr) { html += QStringLiteral("") .arg(q->palette().highlight().color().name(), q->palette().highlightedText().color().name(), hdr); }; const auto dateTime = [](long ts) { QLocale l; return ts == 0 ? i18n("never") : l.toString(QDateTime::fromSecsSinceEpoch(ts), QLocale::ShortFormat); }; appendHeader(i18n("Signing")); appendRow(i18n("First message"), dateTime(tofu.signFirst())); appendRow(i18n("Last message"), dateTime(tofu.signLast())); appendRow(i18n("Message count"), QString::number(tofu.signCount())); appendHeader(i18n("Encryption")); appendRow(i18n("First message"), dateTime(tofu.encrFirst())); appendRow(i18n("Last message"), dateTime(tofu.encrLast())); appendRow(i18n("Message count"), QString::number(tofu.encrCount())); html += QStringLiteral("
%1:%2
%3
"); // Make sure the tooltip string is different for each UserID, even if the // data are the same, otherwise the tooltip is not updated and moved when // user moves mouse from one row to another. html += QStringLiteral("").arg(QString::fromUtf8(uid.id())); return html; } QIcon CertificateDetailsWidget::Private::trustLevelIcon(const GpgME::UserID &uid) const { if (updateInProgress) { return QIcon::fromTheme(QStringLiteral("emblem-question")); } switch (uid.validity()) { case GpgME::UserID::Unknown: case GpgME::UserID::Undefined: return QIcon::fromTheme(QStringLiteral("emblem-question")); case GpgME::UserID::Never: return QIcon::fromTheme(QStringLiteral("emblem-error")); case GpgME::UserID::Marginal: return QIcon::fromTheme(QStringLiteral("emblem-warning")); case GpgME::UserID::Full: case GpgME::UserID::Ultimate: return QIcon::fromTheme(QStringLiteral("emblem-success")); } return {}; } QString CertificateDetailsWidget::Private::trustLevelText(const GpgME::UserID &uid) const { return updateInProgress ? i18n("Updating...") : Formatting::validityShort(uid); } namespace { auto isGood(const GpgME::UserID::Signature &signature) { return signature.status() == GpgME::UserID::Signature::NoError && !signature.isInvalid() && 0x10 <= signature.certClass() && signature.certClass() <= 0x13; } auto accumulateTrustDomains(const std::vector &signatures) { return std::accumulate( std::begin(signatures), std::end(signatures), std::set(), [] (auto domains, const auto &signature) { if (isGood(signature) && signature.isTrustSignature()) { domains.insert(Formatting::trustSignatureDomain(signature)); } return domains; } ); } auto accumulateTrustDomains(const std::vector &userIds) { return std::accumulate( std::begin(userIds), std::end(userIds), std::set(), [] (auto domains, const auto &userID) { const auto newDomains = accumulateTrustDomains(userID.signatures()); std::copy(std::begin(newDomains), std::end(newDomains), std::inserter(domains, std::end(domains))); return domains; } ); } } void CertificateDetailsWidget::Private::setupPGPProperties() { setUpUserIDTable(); const auto trustDomains = accumulateTrustDomains(key.userIDs()); ui.trustedIntroducerField->setVisible(!trustDomains.empty()); ui.trustedIntroducerField->setValue(QStringList(std::begin(trustDomains), std::end(trustDomains)).join(u", ")); ui.refreshBtn->setToolTip(i18nc("@info:tooltip", "Update the key from external sources.")); } static QString formatDNToolTip(const Kleo::DN &dn) { QString html = QStringLiteral(""); const auto appendRow = [&html, dn](const QString &lbl, const QString &attr) { const QString val = dn[attr]; if (!val.isEmpty()) { html += QStringLiteral( "" "" "").arg(lbl, val); } }; appendRow(i18n("Common Name"), QStringLiteral("CN")); appendRow(i18n("Organization"), QStringLiteral("O")); appendRow(i18n("Street"), QStringLiteral("STREET")); appendRow(i18n("City"), QStringLiteral("L")); appendRow(i18n("State"), QStringLiteral("ST")); appendRow(i18n("Country"), QStringLiteral("C")); html += QStringLiteral("
%1:%2
"); return html; } void CertificateDetailsWidget::Private::setupSMIMEProperties() { const auto ownerId = key.userID(0); const Kleo::DN dn(ownerId.id()); for (const auto &[attributeName, field] : ui.smimeAttributeFields) { const QString attributeValue = dn[attributeName]; field->setValue(attributeValue); field->setVisible(!attributeValue.isEmpty()); } ui.smimeTrustLevelField->setIcon(trustLevelIcon(ownerId)); ui.smimeTrustLevelField->setValue(trustLevelText(ownerId)); const Kleo::DN issuerDN(key.issuerName()); const QString issuerCN = issuerDN[QStringLiteral("CN")]; const QString issuer = issuerCN.isEmpty() ? QString::fromUtf8(key.issuerName()) : issuerCN; ui.smimeIssuerField->setValue(issuer); ui.smimeIssuerField->setToolTip(formatDNToolTip(issuerDN)); ui.showIssuerCertificateAction->setEnabled(!key.isRoot()); setUpSMIMEAdressList(); ui.refreshBtn->setToolTip(i18nc("@info:tooltip", "Update the CRLs and do a full validation check of the certificate.")); } void CertificateDetailsWidget::Private::showIssuerCertificate() { // there is either one or no parent key const auto parentKeys = KeyCache::instance()->findIssuers(key, KeyCache::NoOption); if (parentKeys.empty()) { KMessageBox::error(q, i18n("The issuer certificate could not be found locally.")); return; } auto cmd = new Kleo::Commands::DetailsCommand(parentKeys.front()); cmd->setParentWidget(q); cmd->start(); } void CertificateDetailsWidget::Private::copyFingerprintToClipboard() { if (auto clipboard = QGuiApplication::clipboard()) { clipboard->setText(QString::fromLatin1(key.primaryFingerprint())); } } CertificateDetailsWidget::CertificateDetailsWidget(QWidget *parent) : QWidget{parent} , d{std::make_unique(this)} { } CertificateDetailsWidget::~CertificateDetailsWidget() = default; void CertificateDetailsWidget::Private::keyListDone(const GpgME::KeyListResult &, const std::vector &keys, const QString &, const GpgME::Error &) { updateInProgress = false; if (keys.size() != 1) { qCWarning(KLEOPATRA_LOG) << "Invalid keylist result in update."; return; } // As we listen for keysmayhavechanged we get the update // after updating the keycache. KeyCache::mutableInstance()->insert(keys); } void CertificateDetailsWidget::Private::updateKey() { key.update(); setUpdatedKey(key); } void CertificateDetailsWidget::Private::setUpdatedKey(const GpgME::Key &k) { key = k; setupCommonProperties(); if (key.protocol() == GpgME::OpenPGP) { setupPGPProperties(); } else { setupSMIMEProperties(); } } void CertificateDetailsWidget::setKey(const GpgME::Key &key) { if (key.protocol() == GpgME::CMS) { // For everything but S/MIME this should be quick // and we don't need to show another status. d->updateInProgress = true; } d->setUpdatedKey(key); // Run a keylistjob with full details (TOFU / Validate) QGpgME::KeyListJob *job = key.protocol() == GpgME::OpenPGP ? QGpgME::openpgp()->keyListJob(false, true, true) : QGpgME::smime()->keyListJob(false, true, true); auto ctx = QGpgME::Job::context(job); ctx->addKeyListMode(GpgME::WithTofu); ctx->addKeyListMode(GpgME::SignatureNotations); if (key.hasSecret()) { ctx->addKeyListMode(GpgME::WithSecret); } // Windows QGpgME new style connect problem makes this necessary. connect(job, SIGNAL(result(GpgME::KeyListResult,std::vector,QString,GpgME::Error)), this, SLOT(keyListDone(GpgME::KeyListResult,std::vector,QString,GpgME::Error))); job->start(QStringList() << QLatin1String(key.primaryFingerprint())); } GpgME::Key CertificateDetailsWidget::key() const { return d->key; } #include "moc_certificatedetailswidget.cpp" + diff --git a/src/kwatchgnupg/kwatchgnupgmainwin.cpp b/src/kwatchgnupg/kwatchgnupgmainwin.cpp index b448631ea..dcbecc0eb 100644 --- a/src/kwatchgnupg/kwatchgnupgmainwin.cpp +++ b/src/kwatchgnupg/kwatchgnupgmainwin.cpp @@ -1,293 +1,304 @@ /* kwatchgnupgmainwin.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2001, 2002, 2004 Klar �vdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "kwatchgnupgmainwin.h" #include "kwatchgnupgconfig.h" #include "kwatchgnupg.h" #include "tray.h" #include "utils/qt-cxx20-compat.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #if GPGMEPP_VERSION >= 0x11000 // 1.16.0 # define CRYPTOCONFIG_HAS_GROUPLESS_ENTRY_OVERLOAD #endif KWatchGnuPGMainWindow::KWatchGnuPGMainWindow(QWidget *parent) : KXmlGuiWindow(parent, Qt::Window), mConfig(nullptr) { createActions(); createGUI(); mCentralWidget = new QTextEdit(this); mCentralWidget->setReadOnly(true); setCentralWidget(mCentralWidget); mWatcher = new KProcess; connect(mWatcher, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotWatcherExited(int,QProcess::ExitStatus))); connect(mWatcher, &QProcess::readyReadStandardOutput, this, &KWatchGnuPGMainWindow::slotReadStdout); slotReadConfig(); mSysTray = new KWatchGnuPGTray(this); QAction *act = mSysTray->action(QStringLiteral("quit")); if (act) { connect(act, &QAction::triggered, this, &KWatchGnuPGMainWindow::slotQuit); } setAutoSaveSettings(); } KWatchGnuPGMainWindow::~KWatchGnuPGMainWindow() { delete mWatcher; } void KWatchGnuPGMainWindow::slotClear() { mCentralWidget->clear(); mCentralWidget->append(i18n("[%1] Log cleared", QDateTime::currentDateTime().toString(Qt::ISODate))); } void KWatchGnuPGMainWindow::createActions() { QAction *action = actionCollection()->addAction(QStringLiteral("clear_log")); action->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history"))); action->setText(i18n("C&lear History")); connect(action, &QAction::triggered, this, &KWatchGnuPGMainWindow::slotClear); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_L)); (void)KStandardAction::saveAs(this, &KWatchGnuPGMainWindow::slotSaveAs, actionCollection()); (void)KStandardAction::close(this, &KWatchGnuPGMainWindow::close, actionCollection()); (void)KStandardAction::quit(this, &KWatchGnuPGMainWindow::slotQuit, actionCollection()); (void)KStandardAction::preferences(this, &KWatchGnuPGMainWindow::slotConfigure, actionCollection()); (void)KStandardAction::keyBindings(this, &KWatchGnuPGMainWindow::configureShortcuts, actionCollection()); (void)KStandardAction::configureToolbars(this, &KWatchGnuPGMainWindow::slotConfigureToolbars, actionCollection()); } void KWatchGnuPGMainWindow::configureShortcuts() { KShortcutsDialog::showDialog(actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, this); } void KWatchGnuPGMainWindow::slotConfigureToolbars() { KEditToolBar dlg(factory()); dlg.exec(); } void KWatchGnuPGMainWindow::startWatcher() { disconnect(mWatcher, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotWatcherExited(int,QProcess::ExitStatus))); if (mWatcher->state() == QProcess::Running) { mWatcher->kill(); while (mWatcher->state() == QProcess::Running) { qApp->processEvents(QEventLoop::ExcludeUserInputEvents); } mCentralWidget->append(i18n("[%1] Log stopped", QDateTime::currentDateTime().toString(Qt::ISODate))); mCentralWidget->ensureCursorVisible(); } mWatcher->clearProgram(); { const KConfigGroup config(KSharedConfig::openConfig(), "WatchGnuPG"); *mWatcher << config.readEntry("Executable", WATCHGNUPGBINARY); *mWatcher << QStringLiteral("--force"); *mWatcher << config.readEntry("Socket", WATCHGNUPGSOCKET); } mWatcher->setOutputChannelMode(KProcess::OnlyStdoutChannel); mWatcher->start(); const bool ok = mWatcher->waitForStarted(); if (!ok) { KMessageBox::error(this, i18n("The watchgnupg logging process could not be started.\nPlease install watchgnupg somewhere in your $PATH.\nThis log window is unable to display any useful information.")); } else { mCentralWidget->append(i18n("[%1] Log started", QDateTime::currentDateTime().toString(Qt::ISODate))); mCentralWidget->ensureCursorVisible(); } connect(mWatcher, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotWatcherExited(int,QProcess::ExitStatus))); } namespace { QGpgME::CryptoConfigEntry *getCryptoConfigEntry(const QGpgME::CryptoConfig *config, const QString &componentName, const char *entryName) { // copied from utils/compat.cpp in libkleopatra #ifdef CRYPTOCONFIG_HAS_GROUPLESS_ENTRY_OVERLOAD return config->entry(componentName, QString::fromLatin1(entryName)); #else using namespace QGpgME; const CryptoConfigComponent *const comp = config->component(componentName); const QStringList groupNames = comp->groupList(); for (const auto &groupName : groupNames) { const CryptoConfigGroup *const group = comp ? comp->group(groupName) : nullptr; if (CryptoConfigEntry *const entry = group->entry(QString::fromLatin1(entryName))) { return entry; } } return nullptr; #endif } } void KWatchGnuPGMainWindow::setGnuPGConfig() { QStringList logclients; // Get config object QGpgME::CryptoConfig *const cconfig = QGpgME::cryptoConfig(); if (!cconfig) { return; } KConfigGroup config(KSharedConfig::openConfig(), "WatchGnuPG"); const QStringList comps = cconfig->componentList(); for (QStringList::const_iterator it = comps.constBegin(); it != comps.constEnd(); ++it) { const QGpgME::CryptoConfigComponent *const comp = cconfig->component(*it); Q_ASSERT(comp); { QGpgME::CryptoConfigEntry *const entry = getCryptoConfigEntry(cconfig, comp->name(), "log-file"); if (entry) { entry->setStringValue(QLatin1String("socket://") + config.readEntry("Socket", WATCHGNUPGSOCKET)); logclients << QStringLiteral("%1 (%2)").arg(*it, comp->description()); } } { QGpgME::CryptoConfigEntry *const entry = getCryptoConfigEntry(cconfig, comp->name(), "debug-level"); if (entry) { entry->setStringValue(config.readEntry("LogLevel", "basic")); } } } cconfig->sync(true); if (logclients.isEmpty()) { KMessageBox::error(nullptr, i18n("There are no components available that support logging.")); } } void KWatchGnuPGMainWindow::slotWatcherExited(int, QProcess::ExitStatus) { +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + if (KMessageBox::questionTwoActions(this, + i18n("The watchgnupg logging process died.\nDo you want to try to restart it?"), + QString(), + KGuiItem(i18n("Try Restart")), + KGuiItem(i18n("Do Not Try"))) + == KMessageBox::ButtonCode::PrimaryAction) { +#else if (KMessageBox::questionYesNo(this, i18n("The watchgnupg logging process died.\nDo you want to try to restart it?"), QString(), KGuiItem(i18n("Try Restart")), KGuiItem(i18n("Do Not Try"))) == KMessageBox::Yes) { + +#endif mCentralWidget->append(i18n("====== Restarting logging process =====")); mCentralWidget->ensureCursorVisible(); startWatcher(); } else { KMessageBox::error(this, i18n("The watchgnupg logging process is not running.\nThis log window is unable to display any useful information.")); } } void KWatchGnuPGMainWindow::slotReadStdout() { if (!mWatcher) { return; } while (mWatcher->canReadLine()) { QString str = QString::fromUtf8(mWatcher->readLine()); if (str.endsWith(QLatin1Char('\n'))) { str.chop(1); } if (str.endsWith(QLatin1Char('\r'))) { str.chop(1); } mCentralWidget->append(str); mCentralWidget->ensureCursorVisible(); if (!isVisible()) { // Change tray icon to show something happened // PENDING(steffen) mSysTray->setAttention(true); } } } void KWatchGnuPGMainWindow::show() { mSysTray->setAttention(false); KMainWindow::show(); } void KWatchGnuPGMainWindow::slotSaveAs() { const QString filename = QFileDialog::getSaveFileName(this, i18n("Save Log to File")); if (filename.isEmpty()) { return; } QFile file(filename); if (file.open(QIODevice::WriteOnly)) { QTextStream(&file) << mCentralWidget->document()->toRawText(); } else KMessageBox::information(this, i18n("Could not save file %1: %2", filename, file.errorString())); } void KWatchGnuPGMainWindow::slotQuit() { disconnect(mWatcher, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotWatcherExited(int,QProcess::ExitStatus))); mWatcher->kill(); qApp->quit(); } void KWatchGnuPGMainWindow::slotConfigure() { if (!mConfig) { mConfig = new KWatchGnuPGConfig(this); mConfig->setObjectName(QStringLiteral("config dialog")); connect(mConfig, &KWatchGnuPGConfig::reconfigure, this, &KWatchGnuPGMainWindow::slotReadConfig); } mConfig->loadConfig(); mConfig->exec(); } void KWatchGnuPGMainWindow::slotReadConfig() { const KConfigGroup config(KSharedConfig::openConfig(), "LogWindow"); const int maxLogLen = config.readEntry("MaxLogLen", 10000); mCentralWidget->document()->setMaximumBlockCount(maxLogLen < 1 ? -1 : maxLogLen); setGnuPGConfig(); startWatcher(); } bool KWatchGnuPGMainWindow::queryClose() { if (!qApp->isSavingSession()) { hide(); return false; } return KMainWindow::queryClose(); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 220d167a4..bb6c85a61 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,813 +1,823 @@ /* -*- mode: c++; c-basic-offset:4 -*- mainwindow.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "mainwindow.h" #include "aboutdata.h" #include "settings.h" #include #include "view/padwidget.h" #include "view/searchbar.h" #include "view/tabwidget.h" #include "view/keylistcontroller.h" #include "view/keycacheoverlay.h" #include "view/smartcardwidget.h" #include "view/welcomewidget.h" #include "commands/selftestcommand.h" #include "commands/importcrlcommand.h" #include "commands/importcertificatefromfilecommand.h" #include "commands/decryptverifyfilescommand.h" #include "commands/signencryptfilescommand.h" #include "conf/groupsconfigdialog.h" #include "utils/detail_p.h" #include #include "utils/action_data.h" #include "utils/filedialog.h" #include "utils/clipboardmenu.h" #include "utils/gui-helper.h" #include "utils/qt-cxx20-compat.h" #include "dialogs/updatenotification.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std::chrono_literals; using namespace Kleo; using namespace Kleo::Commands; using namespace GpgME; static KGuiItem KStandardGuiItem_quit() { static const QString app = KAboutData::applicationData().displayName(); KGuiItem item = KStandardGuiItem::quit(); item.setText(xi18nc("@action:button", "&Quit %1", app)); return item; } static KGuiItem KStandardGuiItem_close() { KGuiItem item = KStandardGuiItem::close(); item.setText(i18nc("@action:button", "Only &Close Window")); return item; } static bool isQuitting = false; namespace { static const std::vector mainViewActionNames = { QStringLiteral("view_certificate_overview"), QStringLiteral("manage_smartcard"), QStringLiteral("pad_view") }; class CertificateView : public QWidget, public FocusFirstChild { Q_OBJECT public: CertificateView(QWidget* parent = nullptr) : QWidget{parent} , ui{this} { } SearchBar *searchBar() const { return ui.searchBar; } TabWidget *tabWidget() const { return ui.tabWidget; } void focusFirstChild(Qt::FocusReason reason) override { ui.searchBar->lineEdit()->setFocus(reason); } private: struct UI { TabWidget *tabWidget = nullptr; SearchBar *searchBar = nullptr; explicit UI(CertificateView *q) { auto vbox = new QVBoxLayout{q}; vbox->setSpacing(0); searchBar = new SearchBar{q}; vbox->addWidget(searchBar); tabWidget = new TabWidget{q}; vbox->addWidget(tabWidget); tabWidget->connectSearchBar(searchBar); } } ui; }; } class MainWindow::Private { friend class ::MainWindow; MainWindow *const q; public: explicit Private(MainWindow *qq); ~Private(); template void createAndStart() { (new T(this->currentView(), &this->controller))->start(); } template void createAndStart(QAbstractItemView *view) { (new T(view, &this->controller))->start(); } template void createAndStart(const QStringList &a) { (new T(a, this->currentView(), &this->controller))->start(); } template void createAndStart(const QStringList &a, QAbstractItemView *view) { (new T(a, view, &this->controller))->start(); } void closeAndQuit() { const QString app = KAboutData::applicationData().displayName(); +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + const int rc = KMessageBox::questionTwoActionsCancel(q, +#else const int rc = KMessageBox::questionYesNoCancel(q, - xi18n("%1 may be used by other applications as a service." - "You may instead want to close this window without exiting %1.", app), - i18nc("@title:window", "Really Quit?"), - KStandardGuiItem_close(), - KStandardGuiItem_quit(), - KStandardGuiItem::cancel(), - QLatin1String("really-quit-") + app.toLower()); +#endif + xi18n("%1 may be used by other applications as a service." + "You may instead want to close this window without exiting %1.", + app), + i18nc("@title:window", "Really Quit?"), + KStandardGuiItem_close(), + KStandardGuiItem_quit(), + KStandardGuiItem::cancel(), + QLatin1String("really-quit-") + app.toLower()); if (rc == KMessageBox::Cancel) { return; } isQuitting = true; if (!q->close()) { return; } // WARNING: 'this' might be deleted at this point! +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + if (rc == KMessageBox::ButtonCode::SecondaryAction) { +#else if (rc == KMessageBox::No) { +#endif qApp->quit(); } } void configureToolbars() { KEditToolBar dlg(q->factory()); dlg.exec(); } void editKeybindings() { KShortcutsDialog::showDialog(q->actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, q); updateSearchBarClickMessage(); } void updateSearchBarClickMessage() { const QString shortcutStr = focusToClickSearchAction->shortcut().toString(); ui.searchTab->searchBar()->updateClickMessage(shortcutStr); } void updateStatusBar() { if (DeVSCompliance::isActive()) { auto statusBar = std::make_unique(); auto statusLbl = std::make_unique(DeVSCompliance::name()); if (!SystemInfo::isHighContrastModeActive()) { const auto color = KColorScheme(QPalette::Active, KColorScheme::View).foreground( DeVSCompliance::isCompliant() ? KColorScheme::NormalText: KColorScheme::NegativeText ).color(); const auto background = KColorScheme(QPalette::Active, KColorScheme::View).background( DeVSCompliance::isCompliant() ? KColorScheme::PositiveBackground : KColorScheme::NegativeBackground ).color(); statusLbl->setStyleSheet(QStringLiteral("QLabel { color: %1; background-color: %2; }"). arg(color.name()).arg(background.name())); } statusBar->insertPermanentWidget(0, statusLbl.release()); q->setStatusBar(statusBar.release()); // QMainWindow takes ownership } else { q->setStatusBar(nullptr); } } void selfTest() { createAndStart(); } void configureGroups() { if (KConfigDialog::showDialog(GroupsConfigDialog::dialogName())) { return; } KConfigDialog *dialog = new GroupsConfigDialog(q); dialog->show(); } void showHandbook(); void gnupgLogViewer() { const QString exec = QStandardPaths::findExecutable(QStringLiteral("kwatchgnupg")); if (exec.isEmpty()) { KMessageBox::error( q, i18n("Could not start the GnuPG Log Viewer (kwatchgnupg). " "Please check your installation."), i18n("Error Starting KWatchGnuPG")); } else { QProcess::startDetached(QStringLiteral("kwatchgnupg"), QStringList()); } } void forceUpdateCheck() { UpdateNotification::forceUpdateCheck(q); } void slotConfigCommitted(); void slotContextMenuRequested(QAbstractItemView *, const QPoint &p) { if (auto const menu = qobject_cast(q->factory()->container(QStringLiteral("listview_popup"), q))) { menu->exec(p); } else { qCDebug(KLEOPATRA_LOG) << "no \"listview_popup\" in kleopatra's ui.rc file"; } } void slotFocusQuickSearch() { ui.searchTab->searchBar()->lineEdit()->setFocus(); } void showView(const QString &actionName, QWidget *widget) { const auto coll = q->actionCollection(); if (coll) { for ( const QString &name : mainViewActionNames ) { if (auto action = coll->action(name)) { action->setChecked(name == actionName); } } } ui.stackWidget->setCurrentWidget(widget); if (auto ffci = dynamic_cast(widget)) { ffci->focusFirstChild(Qt::TabFocusReason); } } void showCertificateView() { if (KeyCache::instance()->keys().empty()) { showView(QStringLiteral("view_certificate_overview"), ui.welcomeWidget); } else { showView(QStringLiteral("view_certificate_overview"), ui.searchTab); } } void showSmartcardView() { showView(QStringLiteral("manage_smartcard"), ui.scWidget); } void showPadView() { if (!ui.padWidget) { ui.padWidget = new PadWidget; ui.stackWidget->addWidget(ui.padWidget); } showView(QStringLiteral("pad_view"), ui.padWidget); ui.stackWidget->resize(ui.padWidget->sizeHint()); } void restartDaemons() { Kleo::killDaemons(); } private: void setupActions(); QAbstractItemView *currentView() const { return ui.searchTab->tabWidget()->currentView(); } void keyListingDone() { const auto curWidget = ui.stackWidget->currentWidget(); if (curWidget == ui.scWidget || curWidget == ui.padWidget) { return; } showCertificateView(); } private: Kleo::KeyListController controller; bool firstShow : 1; struct UI { CertificateView *searchTab = nullptr; PadWidget *padWidget = nullptr; SmartCardWidget *scWidget = nullptr; WelcomeWidget *welcomeWidget = nullptr; QStackedWidget *stackWidget = nullptr; explicit UI(MainWindow *q); } ui; QAction *focusToClickSearchAction = nullptr; ClipboardMenu *clipboadMenu = nullptr; }; MainWindow::Private::UI::UI(MainWindow *q) : padWidget(nullptr) { auto mainWidget = new QWidget{q}; auto mainLayout = new QVBoxLayout(mainWidget); stackWidget = new QStackedWidget{q}; searchTab = new CertificateView{q}; stackWidget->addWidget(searchTab); new KeyCacheOverlay(mainWidget, q); scWidget = new SmartCardWidget{q}; stackWidget->addWidget(scWidget); welcomeWidget = new WelcomeWidget{q}; stackWidget->addWidget(welcomeWidget); mainLayout->addWidget(stackWidget); q->setCentralWidget(mainWidget); } MainWindow::Private::Private(MainWindow *qq) : q(qq), controller(q), firstShow(true), ui(q) { KDAB_SET_OBJECT_NAME(controller); AbstractKeyListModel *flatModel = AbstractKeyListModel::createFlatKeyListModel(q); AbstractKeyListModel *hierarchicalModel = AbstractKeyListModel::createHierarchicalKeyListModel(q); KDAB_SET_OBJECT_NAME(flatModel); KDAB_SET_OBJECT_NAME(hierarchicalModel); controller.setFlatModel(flatModel); controller.setHierarchicalModel(hierarchicalModel); controller.setTabWidget(ui.searchTab->tabWidget()); ui.searchTab->tabWidget()->setFlatModel(flatModel); ui.searchTab->tabWidget()->setHierarchicalModel(hierarchicalModel); setupActions(); ui.stackWidget->setCurrentWidget(ui.searchTab); if (auto action = q->actionCollection()->action(QStringLiteral("view_certificate_overview"))) { action->setChecked(true); } connect(&controller, SIGNAL(contextMenuRequested(QAbstractItemView*,QPoint)), q, SLOT(slotContextMenuRequested(QAbstractItemView*,QPoint))); connect(KeyCache::instance().get(), &KeyCache::keyListingDone, q, [this] () {keyListingDone();}); q->createGUI(QStringLiteral("kleopatra.rc")); // make toolbar buttons accessible by keyboard auto toolbar = q->findChild(); if (toolbar) { auto toolbarButtons = toolbar->findChildren(); for (auto b : toolbarButtons) { b->setFocusPolicy(Qt::TabFocus); } // move toolbar and its child widgets before the central widget in the tab order; // this is necessary to make Shift+Tab work as expected forceSetTabOrder(q, toolbar); auto toolbarChildren = toolbar->findChildren(); std::for_each(std::rbegin(toolbarChildren), std::rend(toolbarChildren), [toolbar](auto w) { forceSetTabOrder(toolbar, w); }); } const auto title = Kleo::brandingWindowTitle(); if (!title.isEmpty()) { QApplication::setApplicationDisplayName(title); } const auto icon = Kleo::brandingIcon(); if (!icon.isEmpty()) { const auto dir = QDir(Kleo::gpg4winInstallPath() + QStringLiteral("/../share/kleopatra/pics")); qCDebug(KLEOPATRA_LOG) << "Loading branding icon:" << dir.absoluteFilePath(icon); QPixmap brandingIcon(dir.absoluteFilePath(icon)); if (!brandingIcon.isNull()) { auto *w = new QWidget; auto *hl = new QHBoxLayout; auto *lbl = new QLabel; w->setLayout(hl); hl->addWidget(lbl); lbl->setPixmap(brandingIcon); toolbar->addSeparator(); toolbar->addWidget(w); } } q->setAcceptDrops(true); // set default window size q->resize(QSize(1024, 500)); q->setAutoSaveSettings(); updateSearchBarClickMessage(); updateStatusBar(); if (KeyCache::instance()->initialized()) { keyListingDone(); } } MainWindow::Private::~Private() {} MainWindow::MainWindow(QWidget *parent, Qt::WindowFlags flags) : KXmlGuiWindow(parent, flags), d(new Private(this)) {} MainWindow::~MainWindow() {} void MainWindow::Private::setupActions() { KActionCollection *const coll = q->actionCollection(); const std::vector action_data = { // see keylistcontroller.cpp for more actions // Tools menu #ifndef Q_OS_WIN { "tools_start_kwatchgnupg", i18n("GnuPG Log Viewer"), QString(), "kwatchgnupg", q, [this](bool) { gnupgLogViewer(); }, QString() }, #endif { "tools_restart_backend", i18nc("@action:inmenu", "Restart Background Processes"), i18nc("@info:tooltip", "Restart the background processes, e.g. after making changes to the configuration."), "view-refresh", q, [this](bool) { restartDaemons(); }, {} }, // Help menu #ifdef Q_OS_WIN { "help_check_updates", i18n("Check for updates"), QString(), "gpg4win-compact", q, [this](bool) { forceUpdateCheck(); }, QString() }, #endif // View menu { "view_certificate_overview", i18nc("@action show certificate overview", "Certificates"), i18n("Show certificate overview"), "view-certificate", q, [this](bool) { showCertificateView(); }, QString() }, { "pad_view", i18nc("@action show input / output area for encrypting/signing resp. decrypting/verifying text", "Notepad"), i18n("Show pad for encrypting/decrypting and signing/verifying text"), "note", q, [this](bool) { showPadView(); }, QString() }, { "manage_smartcard", i18nc("@action show smartcard management view", "Smartcards"), i18n("Show smartcard management"), "auth-sim-locked", q, [this](bool) { showSmartcardView(); }, QString() }, // Settings menu { "settings_self_test", i18n("Perform Self-Test"), QString(), nullptr, q, [this](bool) { selfTest(); }, QString() }, { "configure_groups", i18n("Configure Groups..."), QString(), "group", q, [this](bool) { configureGroups(); }, QString() } }; make_actions_from_data(action_data, coll); if (!Settings().groupsEnabled()) { if (auto action = coll->action(QStringLiteral("configure_groups"))) { delete action; } } for ( const QString &name : mainViewActionNames ) { if (auto action = coll->action(name)) { action->setCheckable(true); } } KStandardAction::close(q, SLOT(close()), coll); KStandardAction::quit(q, SLOT(closeAndQuit()), coll); KStandardAction::configureToolbars(q, SLOT(configureToolbars()), coll); KStandardAction::keyBindings(q, SLOT(editKeybindings()), coll); KStandardAction::preferences(qApp, SLOT(openOrRaiseConfigDialog()), coll); focusToClickSearchAction = new QAction(i18n("Set Focus to Quick Search"), q); coll->addAction(QStringLiteral("focus_to_quickseach"), focusToClickSearchAction); coll->setDefaultShortcut(focusToClickSearchAction, QKeySequence(Qt::ALT | Qt::Key_Q)); connect(focusToClickSearchAction, SIGNAL(triggered(bool)), q, SLOT(slotFocusQuickSearch())); clipboadMenu = new ClipboardMenu(q); clipboadMenu->setMainWindow(q); clipboadMenu->clipboardMenu()->setIcon(QIcon::fromTheme(QStringLiteral("edit-paste"))); clipboadMenu->clipboardMenu()->setPopupMode(QToolButton::InstantPopup); coll->addAction(QStringLiteral("clipboard_menu"), clipboadMenu->clipboardMenu()); /* Add additional help actions for documentation */ const auto compendium = new DocAction(QIcon::fromTheme(QStringLiteral("gpg4win-compact")), i18n("Gpg4win Compendium"), i18nc("The Gpg4win compendium is only available" "at this point (24.7.2017) in german and english." "Please check with Gpg4win before translating this filename.", "gpg4win-compendium-en.pdf"), QStringLiteral("../share/gpg4win")); coll->addAction(QStringLiteral("help_doc_compendium"), compendium); /* Documentation centered around the german approved VS-NfD mode for official * RESTRICTED communication. This is only available in some distributions with * the focus on official communications. */ const auto symguide = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("Password-based encryption"), i18nc("Only available in German and English. Leave to English for other languages.", "handout_symmetric_encryption_gnupg_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd")); coll->addAction(QStringLiteral("help_doc_symenc"), symguide); const auto quickguide = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("Quickguide"), i18nc("Only available in German and English. Leave to English for other languages.", "handout_sign_encrypt_gnupg_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd")); coll->addAction(QStringLiteral("help_doc_quickguide"), quickguide); const auto vsa10573 = new DocAction(QIcon::fromTheme(QStringLiteral("dvipdf")), i18n("SecOps VSA-10573"), i18nc("Only available in German and English. Leave to English for other languages.", "BSI-VSA-10573-ENG_secops-20220207.pdf"), QStringLiteral("../share/doc/gnupg-vsd")); coll->addAction(QStringLiteral("help_doc_vsa10573"), vsa10573); const auto vsa10584 = new DocAction(QIcon::fromTheme(QStringLiteral("dvipdf")), i18n("SecOps VSA-10584"), i18nc("Only available in German and English. Leave to English for other languages.", "BSI-VSA-10584-ENG_secops-20220207.pdf"), QStringLiteral("../share/doc/gnupg-vsd")); coll->addAction(QStringLiteral("help_doc_vsa10584"), vsa10584); const auto smartcard = new DocAction(QIcon::fromTheme(QStringLiteral("dvipdf")), i18n("Smartcard Management"), i18nc("Only available in German and English. Leave to English for other languages.", "smartcard_howto_de.pdf"), QStringLiteral("../share/doc/gnupg-vsd")); coll->addAction(QStringLiteral("help_doc_smartcard"), smartcard); const auto man_gpg = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("GnuPG Manual"), QStringLiteral("gnupg.pdf"), QStringLiteral("../share/doc/gnupg")); coll->addAction(QStringLiteral("help_doc_gnupg"), man_gpg); q->setStandardToolBarMenuEnabled(true); controller.createActions(coll); ui.searchTab->tabWidget()->createActions(coll); } void MainWindow::Private::slotConfigCommitted() { controller.updateConfig(); updateStatusBar(); } void MainWindow::closeEvent(QCloseEvent *e) { // KMainWindow::closeEvent() insists on quitting the application, // so do not let it touch the event... qCDebug(KLEOPATRA_LOG); if (d->controller.hasRunningCommands()) { if (d->controller.shutdownWarningRequired()) { const int ret = KMessageBox::warningContinueCancel(this, i18n("There are still some background operations ongoing. " "These will be terminated when closing the window. " "Proceed?"), i18n("Ongoing Background Tasks")); if (ret != KMessageBox::Continue) { e->ignore(); return; } } d->controller.cancelCommands(); if (d->controller.hasRunningCommands()) { // wait for them to be finished: setEnabled(false); QEventLoop ev; QTimer::singleShot(100ms, &ev, &QEventLoop::quit); connect(&d->controller, &KeyListController::commandsExecuting, &ev, &QEventLoop::quit); ev.exec(); if (d->controller.hasRunningCommands()) qCWarning(KLEOPATRA_LOG) << "controller still has commands running, this may crash now..."; setEnabled(true); } } if (isQuitting || qApp->isSavingSession()) { d->ui.searchTab->tabWidget()->saveViews(KSharedConfig::openConfig().data()); KConfigGroup grp(KConfigGroup(KSharedConfig::openConfig(), autoSaveGroup())); saveMainWindowSettings(grp); e->accept(); } else { e->ignore(); hide(); } } void MainWindow::showEvent(QShowEvent *e) { KXmlGuiWindow::showEvent(e); if (d->firstShow) { d->ui.searchTab->tabWidget()->loadViews(KSharedConfig::openConfig().data()); d->firstShow = false; } if (!savedGeometry.isEmpty()) { restoreGeometry(savedGeometry); } } void MainWindow::hideEvent(QHideEvent *e) { savedGeometry = saveGeometry(); KXmlGuiWindow::hideEvent(e); } void MainWindow::importCertificatesFromFile(const QStringList &files) { if (!files.empty()) { d->createAndStart(files); } } static QStringList extract_local_files(const QMimeData *data) { const QList urls = data->urls(); // begin workaround KDE/Qt misinterpretation of text/uri-list QList::const_iterator end = urls.end(); if (urls.size() > 1 && !urls.back().isValid()) { --end; } // end workaround QStringList result; std::transform(urls.begin(), end, std::back_inserter(result), std::mem_fn(&QUrl::toLocalFile)); result.erase(std::remove_if(result.begin(), result.end(), std::mem_fn(&QString::isEmpty)), result.end()); return result; } static bool can_decode_local_files(const QMimeData *data) { if (!data) { return false; } return !extract_local_files(data).empty(); } void MainWindow::dragEnterEvent(QDragEnterEvent *e) { qCDebug(KLEOPATRA_LOG); if (can_decode_local_files(e->mimeData())) { e->acceptProposedAction(); } } void MainWindow::dropEvent(QDropEvent *e) { qCDebug(KLEOPATRA_LOG); if (!can_decode_local_files(e->mimeData())) { return; } e->setDropAction(Qt::CopyAction); const QStringList files = extract_local_files(e->mimeData()); const unsigned int classification = classify(files); QMenu menu; QAction *const signEncrypt = menu.addAction(i18n("Sign/Encrypt...")); QAction *const decryptVerify = mayBeAnyMessageType(classification) ? menu.addAction(i18n("Decrypt/Verify...")) : nullptr; if (signEncrypt || decryptVerify) { menu.addSeparator(); } QAction *const importCerts = mayBeAnyCertStoreType(classification) ? menu.addAction(i18n("Import Certificates")) : nullptr; QAction *const importCRLs = mayBeCertificateRevocationList(classification) ? menu.addAction(i18n("Import CRLs")) : nullptr; if (importCerts || importCRLs) { menu.addSeparator(); } if (!signEncrypt && !decryptVerify && !importCerts && !importCRLs) { return; } menu.addAction(i18n("Cancel")); const QAction *const chosen = menu.exec(mapToGlobal(e->pos())); if (!chosen) { return; } if (chosen == signEncrypt) { d->createAndStart(files); } else if (chosen == decryptVerify) { d->createAndStart(files); } else if (chosen == importCerts) { d->createAndStart(files); } else if (chosen == importCRLs) { d->createAndStart(files); } e->accept(); } void MainWindow::readProperties(const KConfigGroup &cg) { qCDebug(KLEOPATRA_LOG); KXmlGuiWindow::readProperties(cg); setHidden(cg.readEntry("hidden", false)); } void MainWindow::saveProperties(KConfigGroup &cg) { qCDebug(KLEOPATRA_LOG); KXmlGuiWindow::saveProperties(cg); cg.writeEntry("hidden", isHidden()); } #include "mainwindow.moc" #include "moc_mainwindow.cpp" diff --git a/src/utils/output.cpp b/src/utils/output.cpp index eed752718..6508643b6 100644 --- a/src/utils/output.cpp +++ b/src/utils/output.cpp @@ -1,759 +1,775 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/output.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "output.h" #include "input_p.h" #include "detail_p.h" #include "kleo_assert.h" #include "kdpipeiodevice.h" #include "log.h" #include "cached.h" #include #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN # include #endif #include +#include using namespace Kleo; using namespace Kleo::_detail; static const int PROCESS_MAX_RUNTIME_TIMEOUT = -1; // no timeout static const int PROCESS_TERMINATE_TIMEOUT = 5 * 1000; // 5s class OverwritePolicy::Private { public: Private(QWidget *p, OverwritePolicy::Policy pol) : policy(pol), widget(p) {} OverwritePolicy::Policy policy; QWidget *widget; }; OverwritePolicy::OverwritePolicy(QWidget *parent, Policy initialPolicy) : d(new Private(parent, initialPolicy)) { } OverwritePolicy::~OverwritePolicy() {} OverwritePolicy::Policy OverwritePolicy::policy() const { return d->policy; } void OverwritePolicy::setPolicy(Policy policy) { d->policy = policy; } QWidget *OverwritePolicy::parentWidget() const { return d->widget; } namespace { class TemporaryFile : public QTemporaryFile { public: using QTemporaryFile::QTemporaryFile; void close() override { if (isOpen()) { m_oldFileName = fileName(); } QTemporaryFile::close(); } bool openNonInheritable() { if (!QTemporaryFile::open()) { return false; } #if defined(Q_OS_WIN) //QTemporaryFile (tested with 4.3.3) creates the file handle as inheritable. //The handle is then inherited by gpgsm, which prevents deletion of the temp file //in FileOutput::doFinalize() //There are no inheritable handles under wince return SetHandleInformation((HANDLE)_get_osfhandle(handle()), HANDLE_FLAG_INHERIT, 0); #endif return true; } QString oldFileName() const { return m_oldFileName; } private: QString m_oldFileName; }; template struct inhibit_close : T_IODevice { explicit inhibit_close() : T_IODevice() {} template explicit inhibit_close(T1 &t1) : T_IODevice(t1) {} /* reimp */ void close() override {} void reallyClose() { T_IODevice::close(); } }; template struct redirect_close : T_IODevice { explicit redirect_close() : T_IODevice(), m_closed(false) {} template explicit redirect_close(T1 &t1) : T_IODevice(t1), m_closed(false) {} /* reimp */ void close() override { this->closeWriteChannel(); m_closed = true; } bool isClosed() const { return m_closed; } private: bool m_closed; }; class FileOutput; class OutputInput : public InputImplBase { public: OutputInput(const std::shared_ptr &output); unsigned int classification() const override { return 0; } void outputFinalized() { if (!m_ioDevice->open(QIODevice::ReadOnly)) { qCCritical(KLEOPATRA_LOG) << "Failed to open file for reading"; } } std::shared_ptr ioDevice() const override { return m_ioDevice; } unsigned long long size() const override { return 0; } private: std::shared_ptr m_output; mutable std::shared_ptr m_ioDevice = nullptr; }; class OutputImplBase : public Output { public: OutputImplBase() : Output(), m_defaultLabel(), m_customLabel(), m_errorString(), m_isFinalized(false), m_isFinalizing(false), m_cancelPending(false), m_canceled(false), m_binaryOpt(false) { } QString label() const override { return m_customLabel.isEmpty() ? m_defaultLabel : m_customLabel; } void setLabel(const QString &label) override { m_customLabel = label; } void setDefaultLabel(const QString &l) { m_defaultLabel = l; } void setBinaryOpt(bool value) override { m_binaryOpt = value; } bool binaryOpt() const override { return m_binaryOpt; } QString errorString() const override { if (m_errorString.dirty()) { m_errorString = doErrorString(); } return m_errorString; } bool isFinalized() const override { return m_isFinalized; } void finalize() override { qCDebug(KLEOPATRA_LOG) << this; if (m_isFinalized || m_isFinalizing) { return; } m_isFinalizing = true; try { doFinalize(); } catch (...) { m_isFinalizing = false; throw; } m_isFinalizing = false; m_isFinalized = true; if (m_cancelPending) { cancel(); } } void cancel() override { qCDebug(KLEOPATRA_LOG) << this; if (m_isFinalizing) { m_cancelPending = true; } else if (!m_canceled) { m_isFinalizing = true; try { doCancel(); } catch (...) {} m_isFinalizing = false; m_isFinalized = false; m_canceled = true; } } private: virtual QString doErrorString() const { if (std::shared_ptr io = ioDevice()) { return io->errorString(); } else { return i18n("No output device"); } } virtual void doFinalize() = 0; virtual void doCancel() = 0; private: QString m_defaultLabel; QString m_customLabel; mutable cached m_errorString; bool m_isFinalized : 1; bool m_isFinalizing : 1; bool m_cancelPending : 1; bool m_canceled : 1; bool m_binaryOpt : 1; }; class PipeOutput : public OutputImplBase { public: explicit PipeOutput(assuan_fd_t fd); std::shared_ptr ioDevice() const override { return m_io; } void doFinalize() override { m_io->reallyClose(); } void doCancel() override { doFinalize(); } private: std::shared_ptr< inhibit_close > m_io; }; class ProcessStdInOutput : public OutputImplBase { public: explicit ProcessStdInOutput(const QString &cmd, const QStringList &args, const QDir &wd); std::shared_ptr ioDevice() const override { return m_proc; } void doFinalize() override { /* Make sure the data is written in the output here. If this is not done the output will be written in small chunks trough the eventloop causing an unnecessary delay before the process has even a chance to work and finish. This delay is mainly noticeable on Windows where it can take ~30 seconds to write out a 10MB file in the 512 byte chunks gpgme serves. */ qCDebug(KLEOPATRA_LOG) << "Waiting for " << m_proc->bytesToWrite() << " Bytes to be written"; while (m_proc->waitForBytesWritten(PROCESS_MAX_RUNTIME_TIMEOUT)); if (!m_proc->isClosed()) { m_proc->close(); } m_proc->waitForFinished(PROCESS_MAX_RUNTIME_TIMEOUT); } bool failed() const override { if (!m_proc) { return false; } return !(m_proc->exitStatus() == QProcess::NormalExit && m_proc->exitCode() == 0); } void doCancel() override { m_proc->terminate(); QTimer::singleShot(PROCESS_TERMINATE_TIMEOUT, m_proc.get(), &QProcess::kill); } QString label() const override; private: QString doErrorString() const override; private: const QString m_command; const QStringList m_arguments; const std::shared_ptr< redirect_close > m_proc; }; class FileOutput : public OutputImplBase { public: explicit FileOutput(const QString &fileName, const std::shared_ptr &policy); ~FileOutput() override { qCDebug(KLEOPATRA_LOG) << this; } QString label() const override { return QFileInfo(m_fileName).fileName(); } std::shared_ptr ioDevice() const override { return m_tmpFile; } void doFinalize() override; void doCancel() override { qCDebug(KLEOPATRA_LOG) << this; } QString fileName() const override { return m_fileName; } void attachInput(const std::shared_ptr &input) { m_attachedInput = std::weak_ptr(input); } private: bool obtainOverwritePermission(); private: const QString m_fileName; std::shared_ptr< TemporaryFile > m_tmpFile; const std::shared_ptr m_policy; std::weak_ptr m_attachedInput; }; #ifndef QT_NO_CLIPBOARD class ClipboardOutput : public OutputImplBase { public: explicit ClipboardOutput(QClipboard::Mode mode); QString label() const override; std::shared_ptr ioDevice() const override { return m_buffer; } void doFinalize() override; void doCancel() override {} private: QString doErrorString() const override { return QString(); } private: const QClipboard::Mode m_mode; std::shared_ptr m_buffer; }; #endif // QT_NO_CLIPBOARD class ByteArrayOutput: public OutputImplBase { public: explicit ByteArrayOutput(QByteArray *data): m_buffer(std::shared_ptr(new QBuffer(data))) { if (!m_buffer->open(QIODevice::WriteOnly)) throw Exception(gpg_error(GPG_ERR_EIO), QStringLiteral("Could not open bytearray for writing?!")); } QString label() const override { return m_label; } void setLabel(const QString &label) override { m_label = label; } std::shared_ptr ioDevice() const override { return m_buffer; } void doFinalize() override { m_buffer->close(); } void doCancel() override { m_buffer->close(); } private: QString doErrorString() const override { return QString(); } private: QString m_label; std::shared_ptr m_buffer; }; } std::shared_ptr Output::createFromPipeDevice(assuan_fd_t fd, const QString &label) { std::shared_ptr po(new PipeOutput(fd)); po->setDefaultLabel(label); return po; } PipeOutput::PipeOutput(assuan_fd_t fd) : OutputImplBase(), m_io(new inhibit_close) { errno = 0; if (!m_io->open(fd, QIODevice::WriteOnly)) throw Exception(errno ? gpg_error_from_errno(errno) : gpg_error(GPG_ERR_EIO), i18n("Could not open FD %1 for writing", assuanFD2int(fd))); } std::shared_ptr Output::createFromFile(const QString &fileName, bool forceOverwrite) { return createFromFile(fileName, std::shared_ptr(new OverwritePolicy(nullptr, forceOverwrite ? OverwritePolicy::Allow : OverwritePolicy::Deny))); } std::shared_ptr Output::createFromFile(const QString &fileName, const std::shared_ptr &policy) { std::shared_ptr fo(new FileOutput(fileName, policy)); qCDebug(KLEOPATRA_LOG) << fo.get(); return fo; } FileOutput::FileOutput(const QString &fileName, const std::shared_ptr &policy) : OutputImplBase(), m_fileName(fileName), m_tmpFile(new TemporaryFile(fileName)), m_policy(policy) { Q_ASSERT(m_policy); errno = 0; if (!m_tmpFile->openNonInheritable()) throw Exception(errno ? gpg_error_from_errno(errno) : gpg_error(GPG_ERR_EIO), i18n("Could not create temporary file for output \"%1\"", fileName)); } bool FileOutput::obtainOverwritePermission() { if (m_policy->policy() != OverwritePolicy::Ask) { return m_policy->policy() == OverwritePolicy::Allow; } - const int sel = KMessageBox::questionYesNoCancel(m_policy->parentWidget(), i18n("The file %1 already exists.\n" - "Overwrite?", m_fileName), - i18n("Overwrite Existing File?"), - KStandardGuiItem::overwrite(), - KGuiItem(i18n("Overwrite All")), - KStandardGuiItem::cancel()); +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + const int sel = KMessageBox::questionTwoActionsCancel(m_policy->parentWidget(), + i18n("The file %1 already exists.\n" +#else + const int sel = KMessageBox::questionYesNoCancel(m_policy->parentWidget(), + i18n("The file %1 already exists.\n" +#endif + "Overwrite?", + m_fileName), + i18n("Overwrite Existing File?"), + KStandardGuiItem::overwrite(), + KGuiItem(i18n("Overwrite All")), + KStandardGuiItem::cancel()); +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + if (sel == KMessageBox::ButtonCode::SecondaryAction) { // Overwrite All +#else if (sel == KMessageBox::No) { //Overwrite All +#endif m_policy->setPolicy(OverwritePolicy::Allow); } +#if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) + return sel == KMessageBox::ButtonCode::PrimaryAction || sel == KMessageBox::ButtonCode::SecondaryAction; +#else return sel == KMessageBox::Yes || sel == KMessageBox::No; +#endif } void FileOutput::doFinalize() { qCDebug(KLEOPATRA_LOG) << this; struct Remover { QString file; ~Remover() { if (QFile::exists(file)) { QFile::remove(file); } } } remover; kleo_assert(m_tmpFile); if (m_tmpFile->isOpen()) { m_tmpFile->close(); } QString tmpFileName = remover.file = m_tmpFile->oldFileName(); m_tmpFile->setAutoRemove(false); QPointer guard = m_tmpFile.get(); m_tmpFile.reset(); // really close the file - needed on Windows for renaming :/ kleo_assert(!guard); // if this triggers, we need to audit for holder of std::shared_ptrs. const QFileInfo fi(tmpFileName); bool qtbug83365_workaround = false; if (!fi.exists()) { /* QT Bug 83365 since qt 5.13 causes the filename of temporary files * in UNC path name directories (unmounted samba shares) to start with * UNC/ instead of the // that Qt can actually handle for things like * rename and remove. So if we can't find our temporary file we try * to workaround that bug. */ qCDebug(KLEOPATRA_LOG) << "failure to find " << tmpFileName; if (tmpFileName.startsWith(QLatin1String("UNC"))) { tmpFileName.replace(0, strlen("UNC"), QLatin1Char('/')); qtbug83365_workaround = true; } const QFileInfo fi2(tmpFileName); if (!fi2.exists()) { throw Exception(gpg_error(GPG_ERR_EIO), QStringLiteral("Could not find temporary file \"%1\".").arg(tmpFileName)); } } qCDebug(KLEOPATRA_LOG) << this << " renaming " << tmpFileName << "->" << m_fileName; if (QFile::rename(tmpFileName, m_fileName)) { qCDebug(KLEOPATRA_LOG) << this << "succeeded"; if (!m_attachedInput.expired()) { m_attachedInput.lock()->outputFinalized(); } return; } qCDebug(KLEOPATRA_LOG) << this << "failed"; if (!obtainOverwritePermission()) throw Exception(gpg_error(GPG_ERR_CANCELED), i18n("Overwriting declined")); qCDebug(KLEOPATRA_LOG) << this << "going to overwrite" << m_fileName; if (!QFile::remove(m_fileName)) throw Exception(errno ? gpg_error_from_errno(errno) : gpg_error(GPG_ERR_EIO), i18n("Could not remove file \"%1\" for overwriting.", m_fileName)); qCDebug(KLEOPATRA_LOG) << this << "succeeded, renaming " << tmpFileName << "->" << m_fileName; if (QFile::rename(tmpFileName, m_fileName)) { qCDebug(KLEOPATRA_LOG) << this << "succeeded"; if (!m_attachedInput.expired()) { m_attachedInput.lock()->outputFinalized(); } if (qtbug83365_workaround) { QFile::remove(tmpFileName); } return; } qCDebug(KLEOPATRA_LOG) << this << "failed"; throw Exception(errno ? gpg_error_from_errno(errno) : gpg_error(GPG_ERR_EIO), i18n(R"(Could not rename file "%1" to "%2")", tmpFileName, m_fileName)); } std::shared_ptr Output::createFromProcessStdIn(const QString &command) { return std::shared_ptr(new ProcessStdInOutput(command, QStringList(), QDir::current())); } std::shared_ptr Output::createFromProcessStdIn(const QString &command, const QStringList &args) { return std::shared_ptr(new ProcessStdInOutput(command, args, QDir::current())); } std::shared_ptr Output::createFromProcessStdIn(const QString &command, const QStringList &args, const QDir &wd) { return std::shared_ptr(new ProcessStdInOutput(command, args, wd)); } ProcessStdInOutput::ProcessStdInOutput(const QString &cmd, const QStringList &args, const QDir &wd) : OutputImplBase(), m_command(cmd), m_arguments(args), m_proc(new redirect_close) { qCDebug(KLEOPATRA_LOG) << "cd" << wd.absolutePath() << '\n' << cmd << args; if (cmd.isEmpty()) throw Exception(gpg_error(GPG_ERR_INV_ARG), i18n("Command not specified")); m_proc->setWorkingDirectory(wd.absolutePath()); m_proc->start(cmd, args); m_proc->setReadChannel(QProcess::StandardError); if (!m_proc->waitForStarted()) throw Exception(gpg_error(GPG_ERR_EIO), i18n("Could not start %1 process: %2", cmd, m_proc->errorString())); } QString ProcessStdInOutput::label() const { if (!m_proc) { return OutputImplBase::label(); } // output max. 3 arguments const QString cmdline = (QStringList(m_command) + m_arguments.mid(0, 3)).join(QLatin1Char(' ')); if (m_arguments.size() > 3) { return i18nc("e.g. \"Input to tar xf - file1 ...\"", "Input to %1 ...", cmdline); } else { return i18nc("e.g. \"Input to tar xf - file\"", "Input to %1", cmdline); } } QString ProcessStdInOutput::doErrorString() const { kleo_assert(m_proc); if (m_proc->exitStatus() == QProcess::NormalExit && m_proc->exitCode() == 0) { return QString(); } if (m_proc->error() == QProcess::UnknownError) return i18n("Error while running %1: %2", m_command, QString::fromLocal8Bit(m_proc->readAllStandardError().trimmed().constData())); else { return i18n("Failed to execute %1: %2", m_command, m_proc->errorString()); } } #ifndef QT_NO_CLIPBOARD std::shared_ptr Output::createFromClipboard() { return std::shared_ptr(new ClipboardOutput(QClipboard::Clipboard)); } ClipboardOutput::ClipboardOutput(QClipboard::Mode mode) : OutputImplBase(), m_mode(mode), m_buffer(new QBuffer) { errno = 0; if (!m_buffer->open(QIODevice::WriteOnly)) throw Exception(errno ? gpg_error_from_errno(errno) : gpg_error(GPG_ERR_EIO), i18n("Could not write to clipboard")); } QString ClipboardOutput::label() const { switch (m_mode) { case QClipboard::Clipboard: return i18n("Clipboard"); case QClipboard::FindBuffer: return i18n("Find buffer"); case QClipboard::Selection: return i18n("Selection"); } return QString(); } void ClipboardOutput::doFinalize() { if (m_buffer->isOpen()) { m_buffer->close(); } if (QClipboard *const cb = QApplication::clipboard()) { cb->setText(QString::fromUtf8(m_buffer->data())); } else throw Exception(gpg_error(GPG_ERR_EIO), i18n("Could not find clipboard")); } #endif // QT_NO_CLIPBOARD Output::~Output() {} OutputInput::OutputInput(const std::shared_ptr &output) : m_output(output) , m_ioDevice(new QFile(output->fileName())) { } std::shared_ptr Input::createFromOutput(const std::shared_ptr &output) { if (auto fo = std::dynamic_pointer_cast(output)) { auto input = std::shared_ptr(new OutputInput(fo)); fo->attachInput(input); return input; } else { return {}; } } std::shared_ptr Output::createFromByteArray(QByteArray *data, const QString &label) { auto ret = std::shared_ptr(new ByteArrayOutput(data)); ret->setLabel(label); return ret; }