diff --git a/src/commands/keytocardcommand.cpp b/src/commands/keytocardcommand.cpp index 19fb89044..4dc3c1023 100644 --- a/src/commands/keytocardcommand.cpp +++ b/src/commands/keytocardcommand.cpp @@ -1,744 +1,782 @@ /* 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,2022 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 "authenticatepivcardapplicationcommand.h" #include "smartcard/algorithminfo.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 #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" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::SmartCard; using namespace GpgME; namespace { QString cardDisplayName(const std::shared_ptr &card) { return i18nc("smartcard application - serial number of smartcard", "%1 - %2", displayAppName(card->appName()), card->displaySerialNumber()); } } 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: + enum Confirmation { + AskForConfirmation, + SkipConfirmation, + }; + void start(); void startKeyToOpenPGPCard(); Subkey getSubkeyToTransferToPIVCard(const std::string &cardSlot, const std::shared_ptr &card); void startKeyToPIVCard(); void authenticate(); void authenticationFinished(); void authenticationCanceled(); void keyToCardDone(const GpgME::Error &err); void keyToPIVCardDone(const GpgME::Error &err); void updateDone(); void keyHasBeenCopiedToCard(); - bool backupKey(); + void backupHasBeenCreated(const QString &backupFilename); + + QString backupKey(); std::vector readSecretKeyFile(); bool writeSecretKeyBackup(const QString &filename, const std::vector &keydata); - void startDeleteSecretKeyLocally(); + void startDeleteSecretKeyLocally(Confirmation confirmation); void deleteSecretKeyLocallyFinished(const GpgME::Error &err); private: std::string appName; GpgME::Subkey subkey; std::string cardSlot; bool overwriteExistingAlreadyApproved = false; bool hasBeenCanceled = false; QMetaObject::Connection updateConnection; }; 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) { } 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(cardDisplayName(card)); } 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(xi18nc("@info", "Sorry! Writing keys to the card %1 is not supported.", cardDisplayName(card))); finished(); return; } } namespace { static std::string 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 OpenPGPCard::pgpSigKeyRef(); } if (subKey.canEncrypt() && !(subKey.canSign() || subKey.canCertify()) && !subKey.canAuthenticate()) { // Encrypt only return OpenPGPCard::pgpEncKeyRef(); } if (subKey.canAuthenticate() && !(subKey.canSign() || subKey.canCertify()) && !subKey.canEncrypt()) { // Auth only return OpenPGPCard::pgpAuthKeyRef(); } // Multiple uses, ask user. QStringList options; std::vector cardSlots; if (subKey.canSign() || subKey.canCertify()) { options.push_back(i18nc("@item:inlistbox", "Signature")); cardSlots.push_back(OpenPGPCard::pgpSigKeyRef()); } if (subKey.canEncrypt()) { options.push_back(i18nc("@item:inlistbox", "Encryption")); cardSlots.push_back(OpenPGPCard::pgpEncKeyRef()); } if (subKey.canAuthenticate()) { options.push_back(i18nc("@item:inlistbox", "Authentication")); cardSlots.push_back(OpenPGPCard::pgpAuthKeyRef()); } 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 choiceIndex = options.indexOf(choice); if (ok && choiceIndex >= 0) { return cardSlots[choiceIndex]; } else { return {}; } } } 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; } cardSlot = getOpenPGPCardSlotForKey(subkey, parentWidgetOrView()); if (cardSlot.empty()) { finished(); return; } // Check if we need to do the overwrite warning. const std::string existingKey = pgpCard->keyFingerprint(cardSlot); if (!existingKey.empty()) { const auto encKeyWarning = (cardSlot == OpenPGPCard::pgpEncKeyRef()) ? i18n("It will no longer be possible to decrypt past communication encrypted for the existing key.") : QString{}; const QString message = i18nc("@info", "

The card %1 already contains a key in this slot. Continuing will overwrite that key.

" "

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

", cardDisplayName(pgpCard)) + i18n("The existing key has the fingerprint:") + QStringLiteral("
%1
").arg(Formatting::prettyID(existingKey.c_str())) + encKeyWarning; const auto choice = KMessageBox::warningContinueCancel(parentWidgetOrView(), message, i18nc("@title:window", "Overwrite existing key"), KGuiItem{i18nc("@action:button", "Overwrite Existing Key")}, 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 %3 %4") .arg(QString::fromLatin1(subkey.keyGrip()), QString::fromStdString(serialNumber()), QString::fromStdString(cardSlot), timestamp); ReaderStatus::mutableInstance()->startSimpleTransaction(pgpCard, cmd.toUtf8(), q_func(), [this](const GpgME::Error &err) { keyToCardDone(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", "

The card %1 already contains a key in this slot. Continuing will overwrite that key.

" "

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

", cardDisplayName(pivCard)) + 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"), KGuiItem{i18nc("@action:button", "Overwrite Existing Key")}, 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) { keyToPIVCardDone(err); }); } void KeyToCardCommand::Private::authenticate() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::authenticate()"; auto cmd = new AuthenticatePIVCardApplicationCommand(serialNumber(), parentWidgetOrView()); cmd->setAutoResetCardToOpenPGP(false); 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(); } void KeyToCardCommand::Private::updateDone() { disconnect(updateConnection); 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; } const std::string keyGripOnCard = card->keyInfo(cardSlot).grip; if (keyGripOnCard != subkey.keyGrip()) { qCWarning(KLEOPATRA_LOG) << q << __func__ << "KEYTOCARD succeeded, but key on card doesn't match copied key"; error(i18nc("@info", "Copying the key to the card failed.")); finished(); return; } keyHasBeenCopiedToCard(); } void KeyToCardCommand::Private::keyHasBeenCopiedToCard() { const auto answer = KMessageBox::questionTwoActionsCancel(parentWidgetOrView(), xi18nc("@info", "The key has been copied to the card." - "Do you want to delete the copy of the key stored on this computer?"), + "You can now delete the copy of the key stored on this computer. " + "Optionally, you can first create a backup of the key."), i18nc("@title:window", "Success"), - KGuiItem{i18nc("@action:button", "Create Backup and Delete Key")}, - KGuiItem{i18nc("@action:button", "Delete Key")}, - KGuiItem{i18nc("@action:button", "Keep Key")}); - if (answer == KMessageBox::ButtonCode::Cancel) { + KGuiItem{i18nc("@action:button", "Create backup")}, + KGuiItem{i18nc("@action:button", "Delete copy on disk")}, + KGuiItem{i18nc("@action:button", "Keep copy on disk")}); + if (answer == KMessageBox::ButtonCode::PrimaryAction) { + const QString backupFilename = backupKey(); + if (backupFilename.isEmpty()) { + // user canceled the backup or there was an error; repeat above question + QMetaObject::invokeMethod(q, [this]() { + keyHasBeenCopiedToCard(); + }); + } + backupHasBeenCreated(backupFilename); + } else if (answer == KMessageBox::ButtonCode::SecondaryAction) { + startDeleteSecretKeyLocally(AskForConfirmation); + } else { finished(); - return; } +} + +void KeyToCardCommand::Private::backupHasBeenCreated(const QString &backupFilename) +{ + const auto answer = + KMessageBox::questionTwoActions(parentWidgetOrView(), + xi18nc("@info", + "The key has been copied to the card and a backup has been written to %1." + "Do you want to delete the copy of the key stored on this computer?", + backupFilename), + i18nc("@title:window", "Success"), + KGuiItem{i18nc("@action:button", "Delete copy on disk")}, + KGuiItem{i18nc("@action:button", "Keep copy on disk")}); if (answer == KMessageBox::ButtonCode::PrimaryAction) { - if (!backupKey()) { - finished(); - return; - } + // the user has created a backup; don't ask again for confirmation before deleting the copy on disk + startDeleteSecretKeyLocally(SkipConfirmation); + } else { + finished(); } - startDeleteSecretKeyLocally(); } namespace { QString gnupgPrivateKeyBackupExtension() { return QStringLiteral(".gpgsk"); } QString proposeFilename(const Subkey &subkey) { QString filename; const auto key = subkey.parent(); auto name = Formatting::prettyName(key); if (name.isEmpty()) { name = Formatting::prettyEMail(key); } const auto shortKeyID = Formatting::prettyKeyID(key.shortKeyID()); const auto shortSubkeyID = Formatting::prettyKeyID(QByteArray{subkey.keyID()}.right(8).constData()); const auto usage = Formatting::usageString(subkey).replace(QLatin1String{", "}, QLatin1String{"_"}); /* Not translated so it's better to use in tutorials etc. */ filename = ((shortKeyID == shortSubkeyID) // ? QStringView{u"%1_%2_SECRET_KEY_BACKUP_%3"}.arg(name, shortKeyID, usage) : QStringView{u"%1_%2_SECRET_KEY_BACKUP_%3_%4"}.arg(name, shortKeyID, shortSubkeyID, usage)); filename.replace(u'/', u'_'); return QDir{ApplicationState::lastUsedExportDirectory()}.filePath(filename + gnupgPrivateKeyBackupExtension()); } QString requestPrivateKeyBackupFilename(const QString &proposedFilename, QWidget *parent) { auto filename = FileDialog::getSaveFileNameEx(parent, i18nc("@title:window", "Backup Secret Key"), QStringLiteral("imp"), proposedFilename, i18nc("description of filename filter", "Secret Key Backup Files") + QLatin1String{" (*.gpgsk)"}); if (!filename.isEmpty()) { const QFileInfo fi{filename}; if (fi.suffix().isEmpty()) { filename += gnupgPrivateKeyBackupExtension(); } ApplicationState::setLastUsedExportDirectory(filename); } return filename; } } -bool KeyToCardCommand::Private::backupKey() +QString KeyToCardCommand::Private::backupKey() { static const QByteArray backupInfoName = "Backup-info:"; auto keydata = readSecretKeyFile(); if (keydata.empty()) { - return false; + return {}; } const auto filename = requestPrivateKeyBackupFilename(proposeFilename(subkey), parentWidgetOrView()); if (filename.isEmpty()) { - return false; + return {}; } // remove old backup info Kleo::erase_if(keydata, [](const auto &line) { return line.startsWith(backupInfoName); }); // prepend new backup info const QByteArrayList backupInfo = { backupInfoName, subkey.keyGrip(), QDateTime::currentDateTimeUtc().toString(Qt::ISODate).toUtf8(), "Kleopatra", Formatting::prettyNameAndEMail(subkey.parent()).toUtf8(), }; keydata.insert(keydata.begin(), backupInfo.join(' ') + '\n'); - return writeSecretKeyBackup(filename, keydata); + if (writeSecretKeyBackup(filename, keydata)) { + return filename; + } else { + return {}; + } } std::vector KeyToCardCommand::Private::readSecretKeyFile() { const auto filename = QString::fromLatin1(subkey.keyGrip()) + QLatin1String{".key"}; const auto path = QDir{Kleo::gnupgPrivateKeysDirectory()}.filePath(filename); QFile file{path}; if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { error(xi18n("Cannot open the private key file %1 for reading.", path)); return {}; } std::vector lines; while (!file.atEnd()) { lines.push_back(file.readLine()); } if (lines.empty()) { error(xi18n("The private key file %1 is empty.", path)); } return lines; } bool KeyToCardCommand::Private::writeSecretKeyBackup(const QString &filename, const std::vector &keydata) { QSaveFile file{filename}; // open the file in binary format because we want to write Unix line endings if (!file.open(QIODevice::WriteOnly)) { error(xi18n("Cannot open the file %1 for writing.", filename)); return false; } for (const auto &line : keydata) { file.write(line); } if (!file.commit()) { error(xi18n("Writing the backup of the secret key to %1 failed.", filename)); return false; }; return true; } -void KeyToCardCommand::Private::startDeleteSecretKeyLocally() +void KeyToCardCommand::Private::startDeleteSecretKeyLocally(Confirmation confirmation) { 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; } - const auto answer = KMessageBox::questionTwoActions(parentWidgetOrView(), - xi18n("Do you really want to delete the local copy of the secret key?"), - i18nc("@title:window", "Confirm Deletion"), - KStandardGuiItem::del(), - KStandardGuiItem::cancel(), - {}, - KMessageBox::Notify | KMessageBox::Dangerous); - if (answer != KMessageBox::ButtonCode::PrimaryAction) { - finished(); - return; + if (confirmation == AskForConfirmation) { + const auto answer = KMessageBox::questionTwoActions(parentWidgetOrView(), + xi18nc("@info", "Do you really want to delete the copy of the key stored on this computer?"), + i18nc("@title:window", "Confirm Deletion"), + KStandardGuiItem::del(), + KStandardGuiItem::cancel(), + {}, + KMessageBox::Notify | KMessageBox::Dangerous); + if (answer != KMessageBox::ButtonCode::PrimaryAction) { + finished(); + return; + } } const auto cmd = QByteArray{"DELETE_KEY --force "} + subkey.keyGrip(); ReaderStatus::mutableInstance()->startSimpleTransaction(card, cmd, q, [this](const GpgME::Error &err) { deleteSecretKeyLocallyFinished(err); }); } void KeyToCardCommand::Private::deleteSecretKeyLocallyFinished(const GpgME::Error &err) { if (err) { - error(xi18nc("@info", "Failed to delete the key:%1", Formatting::errorAsString(err))); + error(xi18nc("@info", + "Failed to delete the copy of the key stored on this computer:%1", + Formatting::errorAsString(err))); } ReaderStatus::mutableInstance()->updateStatus(); - success(i18nc("@info", "Successfully copied the key to the card.")); + success(i18nc("@info", "Successfully deleted the copy of the key stored on this computer.")); finished(); } 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()"; } namespace { bool cardSupportsKeyAlgorithm(const std::shared_ptr &card, const std::string &keyAlgo) { if (card->appName() == OpenPGPCard::AppName) { const auto pgpCard = static_cast(card.get()); const auto cardAlgos = pgpCard->supportedAlgorithms(); return Kleo::any_of(cardAlgos, [keyAlgo](const auto &algo) { return (keyAlgo == algo.id) // || (keyAlgo == OpenPGPCard::getAlgorithmName(algo.id, OpenPGPCard::pgpEncKeyRef())) || (keyAlgo == OpenPGPCard::getAlgorithmName(algo.id, OpenPGPCard::pgpSigKeyRef())); }); } return false; } } // static std::vector> KeyToCardCommand::getSuitableCards(const GpgME::Subkey &subkey) { std::vector> suitableCards; if (subkey.isNull() || subkey.parent().protocol() != GpgME::OpenPGP) { return suitableCards; } const auto keyAlgo = subkey.algoName(); Kleo::copy_if(ReaderStatus::instance()->getCards(), std::back_inserter(suitableCards), [keyAlgo](const auto &card) { return cardSupportsKeyAlgorithm(card, keyAlgo); }); return suitableCards; } void KeyToCardCommand::Private::keyToCardDone(const GpgME::Error &err) { if (!err && !err.isCanceled()) { updateConnection = connect(ReaderStatus::instance(), &ReaderStatus::updateFinished, q, [this]() { updateDone(); }); ReaderStatus::mutableInstance()->updateCard(serialNumber(), appName); return; } if (err) { error(xi18nc("@info", "Copying the key to the card failed:%1", Formatting::errorAsString(err))); } finished(); } void KeyToCardCommand::Private::keyToPIVCardDone(const GpgME::Error &err) { qCDebug(KLEOPATRA_LOG) << q << __func__ << Formatting::errorAsString(err) << "(" << err.code() << ")"; #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) { authenticate(); return; } #endif keyToCardDone(err); } void KeyToCardCommand::doStart() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::doStart()"; d->start(); } void KeyToCardCommand::doCancel() { } #undef q_func #undef d_func #include "moc_keytocardcommand.cpp" diff --git a/src/crypto/autodecryptverifyfilescontroller.cpp b/src/crypto/autodecryptverifyfilescontroller.cpp index 5e2f62c09..eb5676855 100644 --- a/src/crypto/autodecryptverifyfilescontroller.cpp +++ b/src/crypto/autodecryptverifyfilescontroller.cpp @@ -1,664 +1,666 @@ /* -*- 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 #ifndef Q_OS_WIN #include #include #endif #include "kleopatra_debug.h" #include #include #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 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; }; QList 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; QPointer m_dialog; std::unique_ptr m_workDir; }; AutoDecryptVerifyFilesController::Private::Private(AutoDecryptVerifyFilesController *qq) : q(qq) { qRegisterMetaType(); } 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); DecryptVerifyFilesDialog dialog{coll}; m_dialog = &dialog; m_dialog->setOutputLocation(heuristicBaseDirectory(m_passedFiles)); QTimer::singleShot(0, q, SLOT(schedule())); const auto result = m_dialog->exec(); if (result == QDialog::Rejected) { q->cancel(); } else if (result == 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 filesAndFoldersToMove = workdir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); + const auto fileCount = Kleo::count_if(filesAndFoldersToMove, [](const auto &fi) { + return !fi.isDir(); + }); + OverwritePolicy overwritePolicy{m_dialog, fileCount > 1 ? OverwritePolicy::MultipleFiles : OverwritePolicy::Options{}}; + for (const QFileInfo &fi : filesAndFoldersToMove) { 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.fileName(); 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); const auto destPath = ofi.absoluteFilePath(); #ifndef Q_OS_WIN auto job = KIO::moveAs(QUrl::fromLocalFile(inpath), QUrl::fromLocalFile(destPath)); qCDebug(KLEOPATRA_LOG) << "Moving" << job->srcUrls().front().toLocalFile() << "to" << job->destUrl().toLocalFile(); if (!job->exec()) { if (job->error() == KIO::ERR_USER_CANCELED) { break; } reportError(makeGnuPGError(GPG_ERR_GENERAL), xi18nc("@info", "Failed to move %1 to %2." "%3", inpath, destPath, job->errorString())); } #else // On Windows, KIO::move does not work for folders when crossing partition boundaries if (!moveDir(inpath, destPath)) { reportError(makeGnuPGError(GPG_ERR_GENERAL), xi18nc("@info", "Failed to move %1 to %2.", inpath, destPath)); } #endif 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::questionTwoActionsCancel( m_dialog, xi18n("Shall the file be saved with the original file name %1?", embeddedFileName), i18nc("@title:window", "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; } else if (answer == KMessageBox::ButtonCode::SecondaryAction) { outFileName = embeddedFileName; } } - const auto outpath = outDir.absoluteFilePath(outFileName); + auto outpath = outDir.absoluteFilePath(outFileName); qCDebug(KLEOPATRA_LOG) << "Moving " << inpath << " to " << outpath; const QFileInfo ofi(outpath); if (ofi.exists()) { - int sel = KMessageBox::Cancel; - if (!overWriteAll) { - sel = KMessageBox::questionTwoActionsCancel(m_dialog, - i18n("The file %1 already exists.\n" - "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 (sel == KMessageBox::ButtonCode::SecondaryAction) { // Overwrite All - overWriteAll = true; - } - if (!QFile::remove(outpath)) { - reportError(makeGnuPGError(GPG_ERR_GENERAL), xi18n("Failed to delete %1.", outpath)); + const auto newPath = overwritePolicy.obtainOverwritePermission(outpath); + if (newPath.isEmpty()) { + if (overwritePolicy.policy() == OverwritePolicy::Cancel) { + qCDebug(KLEOPATRA_LOG) << "Overwriting canceled for: " << outpath; + break; + } + // else Skip continue; + } else if (newPath == outpath) { + // overwrite existing file + if (!QFile::remove(outpath)) { + reportError(makeGnuPGError(GPG_ERR_GENERAL), xi18n("Failed to delete %1.", outpath)); + continue; + } + } else { + // use new name for file + outpath = newPath; } } if (!QFile::rename(inpath, outpath)) { reportError(makeGnuPGError(GPG_ERR_GENERAL), xi18n("Failed to move %1 to %2.", inpath, outpath)); } } } q->emitDoneOrError(); } QList AutoDecryptVerifyFilesController::Private::classifyAndSortFiles(const QStringList &files) { const auto isSignature = [](int classification) -> bool { return mayBeDetachedSignature(classification) // || mayBeOpaqueSignature(classification) // || (classification & Class::TypeMask) == Class::ClearsignedMessage; }; QList 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; } static bool archiveJobsCanBeUsed([[maybe_unused]] GpgME::Protocol protocol) { return (protocol == GpgME::OpenPGP) && QGpgME::DecryptVerifyArchiveJob::isSupported(); } std::vector> AutoDecryptVerifyFilesController::Private::buildTasks(const QStringList &fileNames, QStringList &undetected) { // sort files so that we make sure we first decrypt and then verify QList 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); #if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO if (cFile.protocol == GpgME::OpenPGP) { t->setSignatureFile(cFile.fileName); t->setSignedFile(signedDataFileName); } else { t->setInput(Input::createFromFile(cFile.fileName)); t->setSignedData(Input::createFromFile(signedDataFileName)); } #else t->setInput(Input::createFromFile(cFile.fileName)); t->setSignedData(Input::createFromFile(signedDataFileName)); #endif 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); #if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO if (cFile.protocol == GpgME::OpenPGP) { t->setSignatureFile(sig); t->setSignedFile(cFile.fileName); } else { t->setInput(Input::createFromFile(sig)); t->setSignedData(Input::createFromFile(cFile.fileName)); } #else t->setInput(Input::createFromFile(sig)); t->setSignedData(Input::createFromFile(cFile.fileName)); #endif 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. std::shared_ptr input; #if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO if (cFile.protocol != GpgME::OpenPGP) { input = Input::createFromFile(cFile.fileName); } #else input = Input::createFromFile(cFile.fileName); #endif 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()); std::shared_ptr output; QString outputFilePath; if (ad) { if ((ad->id() == QLatin1String{"tar"}) && archiveJobsCanBeUsed(cFile.protocol)) { // we don't need an output } else { output = ad->createOutputFromUnpackCommand(cFile.protocol, ad->stripExtension(cFile.protocol, cFile.baseName), wd); } } else { outputFilePath = wd.absoluteFilePath(outputFileName(fi.fileName())); #if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO if (cFile.protocol != GpgME::OpenPGP) { output = Output::createFromFile(outputFilePath, false); } #else output = Output::createFromFile(outputFilePath, false); #endif } // 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); if (input) { t->setInput(input); } if (output) { t->setOutput(output); } t->setProtocol(cFile.protocol); if (ad) { t->setExtractArchive(true); t->setInputFile(cFile.fileName); if (output) { t->setOutputDirectory(m_workDir->path()); } else { // make gpgtar extract to a subfolder of the work directory based on the input file name const auto baseFileName = QFileInfo{ad->stripExtension(cFile.protocol, cFile.baseName)}.fileName(); t->setOutputDirectory(QDir{m_workDir->path()}.filePath(baseFileName)); } #if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO } else if (cFile.protocol == GpgME::OpenPGP) { t->setInputFile(cFile.fileName); t->setOutputFile(outputFilePath); #endif } 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); if (input) { t->setInput(input); } if (output) { t->setOutput(output); } t->setProtocol(cFile.protocol); if (ad) { t->setExtractArchive(true); t->setInputFile(cFile.fileName); if (output) { t->setOutputDirectory(m_workDir->path()); } else { // make gpgtar extract to a subfolder of the work directory based on the input file name const auto baseFileName = QFileInfo{ad->stripExtension(cFile.protocol, cFile.baseName)}.fileName(); t->setOutputDirectory(QDir{m_workDir->path()}.filePath(baseFileName)); } #if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO } else if (cFile.protocol == GpgME::OpenPGP) { t->setInputFile(cFile.fileName); t->setOutputFile(outputFilePath); #endif } 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) << this << __func__; 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/crypto/decryptverifytask.cpp b/src/crypto/decryptverifytask.cpp index eb761c749..2a1796c6d 100644 --- a/src/crypto/decryptverifytask.cpp +++ b/src/crypto/decryptverifytask.cpp @@ -1,1926 +1,1936 @@ /* -*- mode: c++; c-basic-offset:4 -*- decryptverifytask.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "decryptverifytask.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 "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include // Qt::escape #include #include using namespace Kleo::Crypto; using namespace Kleo; using namespace GpgME; using namespace KMime::Types; namespace { static AuditLogEntry auditLogFromSender(QObject *sender) { return AuditLogEntry::fromJob(qobject_cast(sender)); } static bool addrspec_equal(const AddrSpec &lhs, const AddrSpec &rhs, Qt::CaseSensitivity cs) { return lhs.localPart.compare(rhs.localPart, cs) == 0 && lhs.domain.compare(rhs.domain, Qt::CaseInsensitive) == 0; } static bool mailbox_equal(const Mailbox &lhs, const Mailbox &rhs, Qt::CaseSensitivity cs) { return addrspec_equal(lhs.addrSpec(), rhs.addrSpec(), cs); } static std::string stripAngleBrackets(const std::string &str) { if (str.empty()) { return str; } if (str[0] == '<' && str[str.size() - 1] == '>') { return str.substr(1, str.size() - 2); } return str; } static std::string email(const UserID &uid) { if (uid.parent().protocol() == OpenPGP) { if (const char *const email = uid.email()) { return stripAngleBrackets(email); } else { return std::string(); } } Q_ASSERT(uid.parent().protocol() == CMS); if (const char *const id = uid.id()) if (*id == '<') { return stripAngleBrackets(id); } else { return DN(id)[QStringLiteral("EMAIL")].trimmed().toUtf8().constData(); } else { return std::string(); } } static Mailbox mailbox(const UserID &uid) { const std::string e = email(uid); Mailbox mbox; if (!e.empty()) { mbox.setAddress(e.c_str()); } return mbox; } static std::vector extractMailboxes(const Key &key) { std::vector res; const auto userIDs{key.userIDs()}; for (const UserID &id : userIDs) { const Mailbox mbox = mailbox(id); if (!mbox.addrSpec().isEmpty()) { res.push_back(mbox); } } return res; } static std::vector extractMailboxes(const std::vector &signers) { std::vector res; for (const Key &i : signers) { const std::vector bxs = extractMailboxes(i); res.insert(res.end(), bxs.begin(), bxs.end()); } return res; } static bool keyContainsMailbox(const Key &key, const Mailbox &mbox) { const std::vector mbxs = extractMailboxes(key); return std::find_if(mbxs.cbegin(), mbxs.cend(), [mbox](const Mailbox &m) { return mailbox_equal(mbox, m, Qt::CaseInsensitive); }) != mbxs.cend(); } static bool keysContainMailbox(const std::vector &keys, const Mailbox &mbox) { return std::find_if(keys.cbegin(), keys.cend(), [mbox](const Key &key) { return keyContainsMailbox(key, mbox); }) != keys.cend(); } static bool relevantInDecryptVerifyContext(const VerificationResult &r) { // for D/V operations, we ignore verification results which are not errors and contain // no signatures (which means that the data was just not signed) return (r.error() && r.error().code() != GPG_ERR_DECRYPT_FAILED) || r.numSignatures() > 0; } static QString signatureSummaryToString(int summary) { if (summary & Signature::None) { return i18n("Error: Signature not verified"); } else if (summary & Signature::Valid || summary & Signature::Green) { return i18n("Good signature"); } else if (summary & Signature::KeyRevoked) { return i18n("Signing certificate was revoked"); } else if (summary & Signature::KeyExpired) { return i18n("Signing certificate is expired"); } else if (summary & Signature::KeyMissing) { return i18n("Certificate is not available"); } else if (summary & Signature::SigExpired) { return i18n("Signature expired"); } else if (summary & Signature::CrlMissing) { return i18n("CRL missing"); } else if (summary & Signature::CrlTooOld) { return i18n("CRL too old"); } else if (summary & Signature::BadPolicy) { return i18n("Bad policy"); } else if (summary & Signature::SysError) { return i18n("System error"); // ### retrieve system error details? } else if (summary & Signature::Red) { return i18n("Bad signature"); } return QString(); } static QString formatValidSignatureWithTrustLevel(const UserID &id) { if (id.isNull()) { return QString(); } switch (id.validity()) { case UserID::Marginal: return i18n("The signature is valid but the trust in the certificate's validity is only marginal."); case UserID::Full: return i18n("The signature is valid and the certificate's validity is fully trusted."); case UserID::Ultimate: return i18n("The signature is valid and the certificate's validity is ultimately trusted."); case UserID::Never: return i18n("The signature is valid but the certificate's validity is not trusted."); case UserID::Unknown: return i18n("The signature is valid but the certificate's validity is unknown."); case UserID::Undefined: default: return i18n("The signature is valid but the certificate's validity is undefined."); } } static QString renderKeyLink(const QString &fpr, const QString &text) { return QStringLiteral("%2").arg(fpr, text); } static QString renderKey(const Key &key) { if (key.isNull()) { return i18n("Unknown certificate"); } if (key.primaryFingerprint() && strlen(key.primaryFingerprint()) > 16 && key.numUserIDs()) { const QString text = QStringLiteral("%1 (%2)") .arg(Formatting::prettyNameAndEMail(key).toHtmlEscaped()) .arg(Formatting::prettyID(QString::fromLocal8Bit(key.primaryFingerprint()).right(16).toLatin1().constData())); return renderKeyLink(QLatin1String(key.primaryFingerprint()), text); } return renderKeyLink(QLatin1String(key.primaryFingerprint()), Formatting::prettyID(key.primaryFingerprint())); } static QString renderKeyEMailOnlyNameAsFallback(const Key &key) { if (key.isNull()) { return i18n("Unknown certificate"); } const QString email = Formatting::prettyEMail(key); const QString user = !email.isEmpty() ? email : Formatting::prettyName(key); return renderKeyLink(QLatin1String(key.primaryFingerprint()), user); } static QString formatDate(const QDateTime &dt) { return QLocale().toString(dt); } static QString formatSigningInformation(const Signature &sig) { if (sig.isNull()) { return QString(); } const QDateTime dt = sig.creationTime() != 0 ? QDateTime::fromSecsSinceEpoch(quint32(sig.creationTime())) : QDateTime(); QString text; Key key = sig.key(); if (dt.isValid()) { text = i18nc("1 is a date", "Signature created on %1", formatDate(dt)) + QStringLiteral("
"); } if (key.isNull()) { return text += i18n("With unavailable certificate:") + QStringLiteral("
ID: 0x%1").arg(QString::fromLatin1(sig.fingerprint()).toUpper()); } text += i18n("With certificate:") + QStringLiteral("
") + renderKey(key); if (DeVSCompliance::isCompliant()) { text += (QStringLiteral("
") + (sig.isDeVs() ? i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "The signature is %1", DeVSCompliance::name(true)) : i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "The signature is not %1.", DeVSCompliance::name(true)))); } return text; } static QString strikeOut(const QString &str, bool strike) { return QString(strike ? QStringLiteral("%1") : QStringLiteral("%1")).arg(str.toHtmlEscaped()); } static QString formatInputOutputLabel(const QString &input, const QString &output, bool inputDeleted, bool outputDeleted) { if (output.isEmpty()) { return strikeOut(input, inputDeleted); } return i18nc("Input file --> Output file (rarr is arrow", "%1 → %2", strikeOut(input, inputDeleted), strikeOut(output, outputDeleted)); } static bool IsErrorOrCanceled(const GpgME::Error &err) { return err || err.isCanceled(); } static bool IsErrorOrCanceled(const Result &res) { return IsErrorOrCanceled(res.error()); } static bool IsBad(const Signature &sig) { return sig.summary() & Signature::Red; } static bool IsGoodOrValid(const Signature &sig) { return (sig.summary() & Signature::Valid) || (sig.summary() & Signature::Green); } static UserID findUserIDByMailbox(const Key &key, const Mailbox &mbox) { const auto userIDs{key.userIDs()}; for (const UserID &id : userIDs) if (mailbox_equal(mailbox(id), mbox, Qt::CaseInsensitive)) { return id; } return UserID(); } static void updateKeys(const VerificationResult &result) { // This little hack works around the problem that GnuPG / GpgME does not // provide Key information in a verification result. The Key object is // a dummy just holding the KeyID. This hack ensures that all available // keys are fetched from the backend and are populated for (const auto &sig : result.signatures()) { // Update key information sig.key(true, true); } } static QString ensureUniqueDirectory(const QString &path) { // make sure that we don't use an existing directory QString uniquePath = path; const QFileInfo outputInfo{path}; if (outputInfo.exists()) { const auto uniqueName = KFileUtils::suggestName(QUrl::fromLocalFile(outputInfo.absolutePath()), outputInfo.fileName()); uniquePath = outputInfo.dir().filePath(uniqueName); } if (!QDir{}.mkpath(uniquePath)) { return {}; } return uniquePath; } static bool mimeTypeInherits(const QMimeType &mimeType, const QString &mimeTypeName) { // inherits is expensive on an invalid mimeType return mimeType.isValid() && mimeType.inherits(mimeTypeName); } } class DecryptVerifyResult::SenderInfo { public: explicit SenderInfo(const Mailbox &infSender, const std::vector &signers_) : informativeSender(infSender) , signers(signers_) { } const Mailbox informativeSender; const std::vector signers; bool hasInformativeSender() const { return !informativeSender.addrSpec().isEmpty(); } bool conflicts() const { return hasInformativeSender() && hasKeys() && !keysContainMailbox(signers, informativeSender); } bool hasKeys() const { return std::any_of(signers.cbegin(), signers.cend(), [](const Key &key) { return !key.isNull(); }); } std::vector signerMailboxes() const { return extractMailboxes(signers); } }; namespace { static Task::Result::VisualCode codeForVerificationResult(const VerificationResult &res) { if (res.isNull()) { return Task::Result::NeutralSuccess; } const std::vector sigs = res.signatures(); if (sigs.empty()) { return Task::Result::Warning; } if (std::find_if(sigs.begin(), sigs.end(), IsBad) != sigs.end()) { return Task::Result::Danger; } if ((size_t)std::count_if(sigs.begin(), sigs.end(), IsGoodOrValid) == sigs.size()) { return Task::Result::AllGood; } return Task::Result::Warning; } static QString formatVerificationResultOverview(const VerificationResult &res, const DecryptVerifyResult::SenderInfo &info) { if (res.isNull()) { return QString(); } const Error err = res.error(); if (err.isCanceled()) { return i18n("Verification canceled."); } else if (err) { return i18n("Verification failed: %1.", Formatting::errorAsString(err).toHtmlEscaped()); } const std::vector sigs = res.signatures(); if (sigs.empty()) { return i18n("No signatures found."); } const uint bad = std::count_if(sigs.cbegin(), sigs.cend(), IsBad); if (bad > 0) { return i18np("Invalid signature.", "%1 invalid signatures.", bad); } const uint warn = std::count_if(sigs.cbegin(), sigs.cend(), [](const Signature &sig) { return !IsGoodOrValid(sig); }); if (warn == sigs.size()) { return i18np("The data could not be verified.", "%1 signatures could not be verified.", warn); } // Good signature: QString text; if (sigs.size() == 1) { text = i18n("Valid signature by %1", renderKeyEMailOnlyNameAsFallback(sigs[0].key())); if (info.conflicts()) text += i18n("
Warning: The sender's mail address is not stored in the %1 used for signing.", renderKeyLink(QLatin1String(sigs[0].key().primaryFingerprint()), i18n("certificate"))); } else { text = i18np("Valid signature.", "%1 valid signatures.", sigs.size()); if (info.conflicts()) { text += i18n("
Warning: The sender's mail address is not stored in the certificates used for signing."); } } return text; } static QString formatDecryptionResultOverview(const DecryptionResult &result, const QString &errorString = QString()) { const Error err = result.error(); if (err.isCanceled()) { return i18n("Decryption canceled."); } else if (result.isLegacyCipherNoMDC()) { return i18n("Decryption failed: %1.", i18n("No integrity protection (MDC).")); } else if (!errorString.isEmpty()) { return i18n("Decryption failed: %1.", errorString.toHtmlEscaped()); } else if (err) { return i18n("Decryption failed: %1.", Formatting::errorAsString(err).toHtmlEscaped()); } return i18n("Decryption succeeded."); } static QString formatSignature(const Signature &sig, const DecryptVerifyResult::SenderInfo &info) { if (sig.isNull()) { return QString(); } const QString text = formatSigningInformation(sig) + QLatin1String("
"); const Key key = sig.key(); // Green if (sig.summary() & Signature::Valid) { const UserID id = findUserIDByMailbox(key, info.informativeSender); return text + formatValidSignatureWithTrustLevel(!id.isNull() ? id : key.userID(0)); } // Red if ((sig.summary() & Signature::Red)) { const QString ret = text + i18n("The signature is invalid: %1", signatureSummaryToString(sig.summary())); if (sig.summary() & Signature::SysError) { return ret + QStringLiteral(" (%1)").arg(Formatting::errorAsString(sig.status())); } return ret; } // Key missing if ((sig.summary() & Signature::KeyMissing)) { return text + i18n("You can search the certificate on a keyserver or import it from a file."); } // Yellow if ((sig.validity() & Signature::Validity::Undefined) // || (sig.validity() & Signature::Validity::Unknown) // || (sig.summary() == Signature::Summary::None)) { return text + (key.protocol() == OpenPGP ? i18n("The used key is not certified by you or any trusted person.") : i18n("The used certificate is not certified by a trustworthy Certificate Authority or the Certificate Authority is unknown.")); } // Catch all fall through const QString ret = text + i18n("The signature is invalid: %1", signatureSummaryToString(sig.summary())); if (sig.summary() & Signature::SysError) { return ret + QStringLiteral(" (%1)").arg(Formatting::errorAsString(sig.status())); } return ret; } static QStringList format(const std::vector &mbxs) { QStringList res; std::transform(mbxs.cbegin(), mbxs.cend(), std::back_inserter(res), [](const Mailbox &mbox) { return mbox.prettyAddress(); }); return res; } static QString formatVerificationResultDetails(const VerificationResult &res, const DecryptVerifyResult::SenderInfo &info, const QString &errorString) { if ((res.error().code() == GPG_ERR_EIO || res.error().code() == GPG_ERR_NO_DATA) && !errorString.isEmpty()) { return i18n("Input error: %1", errorString); } const std::vector sigs = res.signatures(); QString details; for (const Signature &sig : sigs) { details += formatSignature(sig, info) + QLatin1Char('\n'); } details = details.trimmed(); details.replace(QLatin1Char('\n'), QStringLiteral("

")); if (info.conflicts()) { details += i18n("

The sender's address %1 is not stored in the certificate. Stored: %2

", info.informativeSender.prettyAddress(), format(info.signerMailboxes()).join(i18nc("separator for a list of e-mail addresses", ", "))); } return details; } static QString formatRecipientsDetails(const std::vector &knownRecipients, unsigned int numRecipients) { if (numRecipients == 0) { return {}; } if (knownRecipients.empty()) { return QLatin1String("") + i18np("One unknown recipient.", "%1 unknown recipients.", numRecipients) + QLatin1String(""); } QString details = i18np("Recipient:", "Recipients:", numRecipients); if (numRecipients == 1) { details += QLatin1Char(' ') + renderKey(knownRecipients.front()); } else { details += QLatin1String("
    "); for (const Key &key : knownRecipients) { details += QLatin1String("
  • ") + renderKey(key) + QLatin1String("
  • "); } if (knownRecipients.size() < numRecipients) { details += QLatin1String("
  • ") + i18np("One unknown recipient", "%1 unknown recipients", numRecipients - knownRecipients.size()) + QLatin1String("
  • "); } details += QLatin1String("
"); } return details; } static QString formatDecryptionResultDetails(const DecryptionResult &res, const std::vector &recipients, const QString &errorString, bool isSigned, const QPointer &task) { if ((res.error().code() == GPG_ERR_EIO || res.error().code() == GPG_ERR_NO_DATA) && !errorString.isEmpty()) { return i18n("Input error: %1", errorString); } if (res.isNull() || res.error() || res.error().isCanceled()) { return formatRecipientsDetails(recipients, res.numRecipients()); } QString details; if (DeVSCompliance::isCompliant()) { details += ((res.isDeVs() ? i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "The decryption is %1.", DeVSCompliance::name(true)) : i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "The decryption is not %1.", DeVSCompliance::name(true))) + QStringLiteral("
")); } if (res.fileName()) { const auto decVerifyTask = qobject_cast(task.data()); if (decVerifyTask) { const auto embedFileName = QString::fromUtf8(res.fileName()).toHtmlEscaped(); if (embedFileName != decVerifyTask->outputLabel()) { details += i18n("Embedded file name: '%1'", embedFileName); details += QStringLiteral("
"); } } } if (!isSigned) { details += i18n("Note: You cannot be sure who encrypted this message as it is not signed.") + QLatin1String("
"); } if (res.isLegacyCipherNoMDC()) { details += i18nc("Integrity protection was missing because an old cipher was used.", "Hint: If this file was encrypted before the year 2003 it is " "likely that the file is legitimate. This is because back " "then integrity protection was not widely used.") + QStringLiteral("

") + i18nc("The user is offered to force decrypt a non integrity protected message. With the strong advice to re-encrypt it.", "If you are confident that the file was not manipulated you should re-encrypt it after you have forced the decryption.") + QStringLiteral("

"); } details += formatRecipientsDetails(recipients, res.numRecipients()); return details; } static QString formatDecryptVerifyResultOverview(const DecryptionResult &dr, const VerificationResult &vr, const DecryptVerifyResult::SenderInfo &info) { if (IsErrorOrCanceled(dr) || !relevantInDecryptVerifyContext(vr)) { return formatDecryptionResultOverview(dr); } return formatVerificationResultOverview(vr, info); } static QString formatDecryptVerifyResultDetails(const DecryptionResult &dr, const VerificationResult &vr, const std::vector &recipients, const DecryptVerifyResult::SenderInfo &info, const QString &errorString, const QPointer &task) { const QString drDetails = formatDecryptionResultDetails(dr, recipients, errorString, relevantInDecryptVerifyContext(vr), task); if (IsErrorOrCanceled(dr) || !relevantInDecryptVerifyContext(vr)) { return drDetails; } return drDetails + (drDetails.isEmpty() ? QString() : QStringLiteral("
")) + formatVerificationResultDetails(vr, info, errorString); } } // anon namespace class DecryptVerifyResult::Private { DecryptVerifyResult *const q; public: Private(DecryptVerifyOperation type, const VerificationResult &vr, const DecryptionResult &dr, const QByteArray &stuff, const QString &fileName, const GpgME::Error &error, const QString &errString, const QString &input, const QString &output, const AuditLogEntry &auditLog, Task *parentTask, const Mailbox &informativeSender, DecryptVerifyResult *qq) : q(qq) , m_type(type) , m_verificationResult(vr) , m_decryptionResult(dr) , m_stuff(stuff) , m_fileName(fileName) , m_error(error) , m_errorString(errString) , m_inputLabel(input) , m_outputLabel(output) , m_auditLog(auditLog) , m_parentTask(QPointer(parentTask)) , m_informativeSender(informativeSender) { } QString label() const { return formatInputOutputLabel(m_inputLabel, m_outputLabel, false, q->hasError()); } DecryptVerifyResult::SenderInfo makeSenderInfo() const; bool isDecryptOnly() const { return m_type == Decrypt; } bool isVerifyOnly() const { return m_type == Verify; } bool isDecryptVerify() const { return m_type == DecryptVerify; } DecryptVerifyOperation m_type; VerificationResult m_verificationResult; DecryptionResult m_decryptionResult; QByteArray m_stuff; QString m_fileName; GpgME::Error m_error; QString m_errorString; QString m_inputLabel; QString m_outputLabel; const AuditLogEntry m_auditLog; QPointer m_parentTask; const Mailbox m_informativeSender; }; DecryptVerifyResult::SenderInfo DecryptVerifyResult::Private::makeSenderInfo() const { return SenderInfo(m_informativeSender, KeyCache::instance()->findSigners(m_verificationResult)); } std::shared_ptr AbstractDecryptVerifyTask::fromDecryptResult(const DecryptionResult &dr, const QByteArray &plaintext, const AuditLogEntry &auditLog) { return std::shared_ptr(new DecryptVerifyResult(Decrypt, // VerificationResult(), dr, plaintext, {}, {}, QString(), inputLabel(), outputLabel(), auditLog, this, informativeSender())); } std::shared_ptr AbstractDecryptVerifyTask::fromDecryptResult(const GpgME::Error &err, const QString &what, const AuditLogEntry &auditLog) { return std::shared_ptr(new DecryptVerifyResult(Decrypt, // VerificationResult(), DecryptionResult(err), QByteArray(), {}, err, what, inputLabel(), outputLabel(), auditLog, this, informativeSender())); } std::shared_ptr AbstractDecryptVerifyTask::fromDecryptVerifyResult(const DecryptionResult &dr, const VerificationResult &vr, const QByteArray &plaintext, const QString &fileName, const AuditLogEntry &auditLog) { const auto err = dr.error().code() ? dr.error() : vr.error(); return std::shared_ptr(new DecryptVerifyResult(DecryptVerify, // vr, dr, plaintext, fileName, err, QString(), inputLabel(), outputLabel(), auditLog, this, informativeSender())); } std::shared_ptr AbstractDecryptVerifyTask::fromDecryptVerifyResult(const GpgME::Error &err, const QString &details, const AuditLogEntry &auditLog) { return std::shared_ptr(new DecryptVerifyResult(DecryptVerify, // VerificationResult(), DecryptionResult(err), QByteArray(), {}, err, details, inputLabel(), outputLabel(), auditLog, this, informativeSender())); } std::shared_ptr AbstractDecryptVerifyTask::fromVerifyOpaqueResult(const VerificationResult &vr, const QByteArray &plaintext, const AuditLogEntry &auditLog) { return std::shared_ptr(new DecryptVerifyResult(Verify, // vr, DecryptionResult(), plaintext, {}, {}, QString(), inputLabel(), outputLabel(), auditLog, this, informativeSender())); } std::shared_ptr AbstractDecryptVerifyTask::fromVerifyOpaqueResult(const GpgME::Error &err, const QString &details, const AuditLogEntry &auditLog) { return std::shared_ptr(new DecryptVerifyResult(Verify, // VerificationResult(err), DecryptionResult(), QByteArray(), {}, err, details, inputLabel(), outputLabel(), auditLog, this, informativeSender())); } std::shared_ptr AbstractDecryptVerifyTask::fromVerifyDetachedResult(const VerificationResult &vr, const AuditLogEntry &auditLog) { return std::shared_ptr(new DecryptVerifyResult(Verify, // vr, DecryptionResult(), QByteArray(), {}, {}, QString(), inputLabel(), outputLabel(), auditLog, this, informativeSender())); } std::shared_ptr AbstractDecryptVerifyTask::fromVerifyDetachedResult(const GpgME::Error &err, const QString &details, const AuditLogEntry &auditLog) { return std::shared_ptr(new DecryptVerifyResult(Verify, // VerificationResult(err), DecryptionResult(), QByteArray(), {}, err, details, inputLabel(), outputLabel(), auditLog, this, informativeSender())); } DecryptVerifyResult::DecryptVerifyResult(DecryptVerifyOperation type, const VerificationResult &vr, const DecryptionResult &dr, const QByteArray &stuff, const QString &fileName, const GpgME::Error &error, const QString &errString, const QString &inputLabel, const QString &outputLabel, const AuditLogEntry &auditLog, Task *parentTask, const Mailbox &informativeSender) : Task::Result() , d(new Private(type, vr, dr, stuff, fileName, error, errString, inputLabel, outputLabel, auditLog, parentTask, informativeSender, this)) { } Task::Result::ContentType DecryptVerifyResult::viewableContentType() const { #if QGPGME_SUPPORTS_IS_MIME if (decryptionResult().isMime()) { return Task::Result::ContentType::Mime; } #endif if (fileName().endsWith(QStringLiteral("openpgp-encrypted-message"))) { return Task::Result::ContentType::Mime; } QMimeDatabase mimeDatabase; const auto mimeType = mimeDatabase.mimeTypeForFile(fileName()); if (mimeTypeInherits(mimeType, QStringLiteral("message/rfc822"))) { return Task::Result::ContentType::Mime; } if (mimeTypeInherits(mimeType, QStringLiteral("application/mbox"))) { return Task::Result::ContentType::Mbox; } return Task::Result::ContentType::None; } QString DecryptVerifyResult::overview() const { QString ov; if (d->isDecryptOnly()) { ov += formatDecryptionResultOverview(d->m_decryptionResult); } else if (d->isVerifyOnly()) { ov += formatVerificationResultOverview(d->m_verificationResult, d->makeSenderInfo()); } else { ov += formatDecryptVerifyResultOverview(d->m_decryptionResult, d->m_verificationResult, d->makeSenderInfo()); } if (ov.size() + d->label().size() > 120) { // Avoid ugly breaks ov = QStringLiteral("
") + ov; } return i18nc("label: result example: foo.sig: Verification failed. ", "%1: %2", d->label(), ov); } QString DecryptVerifyResult::details() const { if (d->isDecryptOnly()) { return formatDecryptionResultDetails(d->m_decryptionResult, KeyCache::instance()->findRecipients(d->m_decryptionResult), errorString(), false, d->m_parentTask); } if (d->isVerifyOnly()) { return formatVerificationResultDetails(d->m_verificationResult, d->makeSenderInfo(), errorString()); } return formatDecryptVerifyResultDetails(d->m_decryptionResult, d->m_verificationResult, KeyCache::instance()->findRecipients(d->m_decryptionResult), d->makeSenderInfo(), errorString(), d->m_parentTask); } GpgME::Error DecryptVerifyResult::error() const { return d->m_error; } QString DecryptVerifyResult::errorString() const { return d->m_errorString; } AuditLogEntry DecryptVerifyResult::auditLog() const { return d->m_auditLog; } QPointer DecryptVerifyResult::parentTask() const { return d->m_parentTask; } Task::Result::VisualCode DecryptVerifyResult::code() const { if ((d->m_type == DecryptVerify || d->m_type == Verify) && relevantInDecryptVerifyContext(verificationResult())) { return codeForVerificationResult(verificationResult()); } return hasError() ? NeutralError : NeutralSuccess; } GpgME::VerificationResult DecryptVerifyResult::verificationResult() const { return d->m_verificationResult; } GpgME::DecryptionResult DecryptVerifyResult::decryptionResult() const { return d->m_decryptionResult; } QString DecryptVerifyResult::fileName() const { return d->m_fileName; } class AbstractDecryptVerifyTask::Private { public: Mailbox informativeSender; QPointer job; }; AbstractDecryptVerifyTask::AbstractDecryptVerifyTask(QObject *parent) : Task(parent) , d(new Private) { } AbstractDecryptVerifyTask::~AbstractDecryptVerifyTask() { } void AbstractDecryptVerifyTask::cancel() { qCDebug(KLEOPATRA_LOG) << this << __func__; if (d->job) { d->job->slotCancel(); } } Mailbox AbstractDecryptVerifyTask::informativeSender() const { return d->informativeSender; } void AbstractDecryptVerifyTask::setInformativeSender(const Mailbox &sender) { d->informativeSender = sender; } QGpgME::Job *AbstractDecryptVerifyTask::job() const { return d->job; } void AbstractDecryptVerifyTask::setJob(QGpgME::Job *job) { d->job = job; } class DecryptVerifyTask::Private { DecryptVerifyTask *const q; public: explicit Private(DecryptVerifyTask *qq) : q{qq} { } void startDecryptVerifyJob(); void startDecryptVerifyArchiveJob(); void slotResult(const DecryptionResult &, const VerificationResult &, const QByteArray & = {}); std::shared_ptr m_input; std::shared_ptr m_output; const QGpgME::Protocol *m_backend = nullptr; Protocol m_protocol = UnknownProtocol; bool m_ignoreMDCError = false; bool m_extractArchive = false; QString m_inputFilePath; QString m_outputFilePath; QString m_outputDirectory; }; void DecryptVerifyTask::Private::slotResult(const DecryptionResult &dr, const VerificationResult &vr, const QByteArray &plainText) { updateKeys(vr); { std::stringstream ss; ss << dr << '\n' << vr; qCDebug(KLEOPATRA_LOG) << ss.str().c_str(); } const AuditLogEntry auditLog = auditLogFromSender(q->sender()); if (m_output) { if (dr.error().code() || vr.error().code()) { m_output->cancel(); } else { try { kleo_assert(!dr.isNull() || !vr.isNull()); m_output->finalize(); } catch (const GpgME::Exception &e) { q->emitResult(q->fromDecryptResult(e.error(), QString::fromLocal8Bit(e.what()), auditLog)); return; } catch (const std::exception &e) { q->emitResult( q->fromDecryptResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), auditLog)); return; } catch (...) { q->emitResult(q->fromDecryptResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), auditLog)); return; } } } const int drErr = dr.error().code(); const QString errorString = m_output ? m_output->errorString() : QString{}; if (((drErr == GPG_ERR_EIO || drErr == GPG_ERR_NO_DATA) && !errorString.isEmpty()) || (m_output && m_output->failed())) { q->emitResult(q->fromDecryptResult(drErr ? dr.error() : Error::fromCode(GPG_ERR_EIO), errorString, auditLog)); return; } q->emitResult(q->fromDecryptVerifyResult(dr, vr, plainText, m_output ? m_output->fileName() : QString{}, auditLog)); } DecryptVerifyTask::DecryptVerifyTask(QObject *parent) : AbstractDecryptVerifyTask(parent) , d(new Private(this)) { } DecryptVerifyTask::~DecryptVerifyTask() { } void DecryptVerifyTask::setInput(const std::shared_ptr &input) { d->m_input = input; kleo_assert(d->m_input && d->m_input->ioDevice()); } void DecryptVerifyTask::setOutput(const std::shared_ptr &output) { d->m_output = output; kleo_assert(d->m_output && d->m_output->ioDevice()); } void DecryptVerifyTask::setProtocol(Protocol prot) { kleo_assert(prot != UnknownProtocol); d->m_protocol = prot; d->m_backend = prot == GpgME::OpenPGP ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(d->m_backend); } void DecryptVerifyTask::autodetectProtocolFromInput() { if (!d->m_input) { return; } const Protocol p = findProtocol(d->m_input->classification()); if (p == UnknownProtocol) { throw Exception( gpg_error(GPG_ERR_NOTHING_FOUND), i18n("Could not determine whether this is an S/MIME or an OpenPGP signature/ciphertext - maybe it is neither ciphertext nor a signature?"), Exception::MessageOnly); } setProtocol(p); } QString DecryptVerifyTask::label() const { return i18n("Decrypting %1...", inputLabel()); } unsigned long long DecryptVerifyTask::inputSize() const { return d->m_input ? d->m_input->size() : 0; } QString DecryptVerifyTask::inputLabel() const { return d->m_input ? d->m_input->label() : QFileInfo{d->m_inputFilePath}.fileName(); } QString DecryptVerifyTask::outputLabel() const { if (d->m_output) { return d->m_output->label(); } else if (!d->m_outputFilePath.isEmpty()) { return QFileInfo{d->m_outputFilePath}.fileName(); } else { return d->m_outputDirectory; } } Protocol DecryptVerifyTask::protocol() const { return d->m_protocol; } static void ensureIOOpen(QIODevice *input, QIODevice *output) { if (input && !input->isOpen()) { input->open(QIODevice::ReadOnly); } if (output && !output->isOpen()) { output->open(QIODevice::WriteOnly); } } void DecryptVerifyTask::setIgnoreMDCError(bool value) { d->m_ignoreMDCError = value; } void DecryptVerifyTask::setExtractArchive(bool extract) { d->m_extractArchive = extract; } void DecryptVerifyTask::setInputFile(const QString &path) { d->m_inputFilePath = path; } void DecryptVerifyTask::setOutputFile(const QString &path) { d->m_outputFilePath = path; } void DecryptVerifyTask::setOutputDirectory(const QString &directory) { d->m_outputDirectory = directory; } static bool archiveJobsCanBeUsed(GpgME::Protocol protocol) { return (protocol == GpgME::OpenPGP) && QGpgME::DecryptVerifyArchiveJob::isSupported(); } void DecryptVerifyTask::doStart() { kleo_assert(d->m_backend); if (d->m_extractArchive && archiveJobsCanBeUsed(d->m_protocol)) { d->startDecryptVerifyArchiveJob(); } else { d->startDecryptVerifyJob(); } } static void setIgnoreMDCErrorFlag(QGpgME::Job *job, bool ignoreMDCError) { if (ignoreMDCError) { qCDebug(KLEOPATRA_LOG) << "Modifying job to ignore MDC errors."; auto ctx = QGpgME::Job::context(job); if (!ctx) { qCWarning(KLEOPATRA_LOG) << "Failed to get context for job"; } else { const auto err = ctx->setFlag("ignore-mdc-error", "1"); if (err) { qCWarning(KLEOPATRA_LOG) << "Failed to set ignore mdc errors" << Formatting::errorAsString(err); } } } } void DecryptVerifyTask::Private::startDecryptVerifyJob() { #if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO - if (QFile::exists(m_outputFilePath)) { + if (!m_outputFilePath.isEmpty() && QFile::exists(m_outputFilePath)) { // The output files are always written to a temporary location. Therefore, this can only occur // if two signed/encrypted files with the same name in different folders are verified/decrypted // because they would be written to the same temporary location. QMetaObject::invokeMethod( q, [this]() { slotResult(DecryptionResult{Error::fromCode(GPG_ERR_EEXIST)}, VerificationResult{}); }, Qt::QueuedConnection); return; } #endif try { std::unique_ptr job{m_backend->decryptVerifyJob()}; kleo_assert(job); setIgnoreMDCErrorFlag(job.get(), m_ignoreMDCError); QObject::connect(job.get(), &QGpgME::DecryptVerifyJob::result, q, [this](const GpgME::DecryptionResult &decryptResult, const GpgME::VerificationResult &verifyResult, const QByteArray &plainText) { slotResult(decryptResult, verifyResult, plainText); }); connect(job.get(), &QGpgME::Job::jobProgress, q, &DecryptVerifyTask::setProgress); #if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO - job->setInputFile(m_inputFilePath); - job->setOutputFile(m_outputFilePath); - const auto err = job->startIt(); + if (!m_inputFilePath.isEmpty() && !m_outputFilePath.isEmpty()) { + job->setInputFile(m_inputFilePath); + job->setOutputFile(m_outputFilePath); + const auto err = job->startIt(); + } else { + ensureIOOpen(m_input->ioDevice().get(), m_output->ioDevice().get()); + job->start(m_input->ioDevice(), m_output->ioDevice()); + } #else ensureIOOpen(m_input->ioDevice().get(), m_output->ioDevice().get()); job->start(m_input->ioDevice(), m_output->ioDevice()); #endif q->setJob(job.release()); } catch (const GpgME::Exception &e) { q->emitResult(q->fromDecryptVerifyResult(e.error(), QString::fromLocal8Bit(e.what()), AuditLogEntry())); } catch (const std::exception &e) { q->emitResult( q->fromDecryptVerifyResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), AuditLogEntry())); } catch (...) { q->emitResult(q->fromDecryptVerifyResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), AuditLogEntry())); } } void DecryptVerifyTask::Private::startDecryptVerifyArchiveJob() { std::unique_ptr job{m_backend->decryptVerifyArchiveJob()}; kleo_assert(job); setIgnoreMDCErrorFlag(job.get(), m_ignoreMDCError); connect(job.get(), &QGpgME::DecryptVerifyArchiveJob::result, q, [this](const GpgME::DecryptionResult &decryptResult, const GpgME::VerificationResult &verifyResult) { slotResult(decryptResult, verifyResult); }); connect(job.get(), &QGpgME::Job::jobProgress, q, &DecryptVerifyTask::setProgress); #if QGPGME_ARCHIVE_JOBS_SUPPORT_INPUT_FILENAME // make sure that we don't use an existing output directory const auto outputDirectory = ensureUniqueDirectory(m_outputDirectory); if (outputDirectory.isEmpty()) { q->emitResult(q->fromDecryptVerifyResult(Error::fromCode(GPG_ERR_GENERAL), {}, {})); return; } m_outputDirectory = outputDirectory; job->setInputFile(m_inputFilePath); job->setOutputDirectory(m_outputDirectory); const auto err = job->startIt(); #else ensureIOOpen(m_input->ioDevice().get(), nullptr); job->setOutputDirectory(m_outputDirectory); const auto err = job->start(m_input->ioDevice()); #endif q->setJob(job.release()); if (err) { q->emitResult(q->fromDecryptVerifyResult(err, {}, {})); } } class DecryptTask::Private { DecryptTask *const q; public: explicit Private(DecryptTask *qq) : q{qq} { } void slotResult(const DecryptionResult &, const QByteArray &); void registerJob(QGpgME::DecryptJob *job) { q->connect(job, SIGNAL(result(GpgME::DecryptionResult, QByteArray)), q, SLOT(slotResult(GpgME::DecryptionResult, QByteArray))); q->connect(job, &QGpgME::Job::jobProgress, q, &DecryptTask::setProgress); } std::shared_ptr m_input; std::shared_ptr m_output; const QGpgME::Protocol *m_backend = nullptr; Protocol m_protocol = UnknownProtocol; }; void DecryptTask::Private::slotResult(const DecryptionResult &result, const QByteArray &plainText) { { std::stringstream ss; ss << result; qCDebug(KLEOPATRA_LOG) << ss.str().c_str(); } const AuditLogEntry auditLog = auditLogFromSender(q->sender()); if (result.error().code()) { m_output->cancel(); } else { try { kleo_assert(!result.isNull()); m_output->finalize(); } catch (const GpgME::Exception &e) { q->emitResult(q->fromDecryptResult(e.error(), QString::fromLocal8Bit(e.what()), auditLog)); return; } catch (const std::exception &e) { q->emitResult(q->fromDecryptResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), auditLog)); return; } catch (...) { q->emitResult(q->fromDecryptResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), auditLog)); return; } } const int drErr = result.error().code(); const QString errorString = m_output->errorString(); if (((drErr == GPG_ERR_EIO || drErr == GPG_ERR_NO_DATA) && !errorString.isEmpty()) || m_output->failed()) { q->emitResult(q->fromDecryptResult(result.error() ? result.error() : Error::fromCode(GPG_ERR_EIO), errorString, auditLog)); return; } q->emitResult(q->fromDecryptResult(result, plainText, auditLog)); } DecryptTask::DecryptTask(QObject *parent) : AbstractDecryptVerifyTask(parent) , d(new Private(this)) { } DecryptTask::~DecryptTask() { } void DecryptTask::setInput(const std::shared_ptr &input) { d->m_input = input; kleo_assert(d->m_input && d->m_input->ioDevice()); } void DecryptTask::setOutput(const std::shared_ptr &output) { d->m_output = output; kleo_assert(d->m_output && d->m_output->ioDevice()); } void DecryptTask::setProtocol(Protocol prot) { kleo_assert(prot != UnknownProtocol); d->m_protocol = prot; d->m_backend = (prot == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(d->m_backend); } void DecryptTask::autodetectProtocolFromInput() { if (!d->m_input) { return; } const Protocol p = findProtocol(d->m_input->classification()); if (p == UnknownProtocol) { throw Exception(gpg_error(GPG_ERR_NOTHING_FOUND), i18n("Could not determine whether this was S/MIME- or OpenPGP-encrypted - maybe it is not ciphertext at all?"), Exception::MessageOnly); } setProtocol(p); } QString DecryptTask::label() const { return i18n("Decrypting: %1...", d->m_input->label()); } unsigned long long DecryptTask::inputSize() const { return d->m_input ? d->m_input->size() : 0; } QString DecryptTask::inputLabel() const { return d->m_input ? d->m_input->label() : QString(); } QString DecryptTask::outputLabel() const { return d->m_output ? d->m_output->label() : QString(); } Protocol DecryptTask::protocol() const { return d->m_protocol; } void DecryptTask::doStart() { kleo_assert(d->m_backend); try { std::unique_ptr job{d->m_backend->decryptJob()}; kleo_assert(job); d->registerJob(job.get()); ensureIOOpen(d->m_input->ioDevice().get(), d->m_output->ioDevice().get()); job->start(d->m_input->ioDevice(), d->m_output->ioDevice()); setJob(job.release()); } catch (const GpgME::Exception &e) { emitResult(fromDecryptResult(e.error(), QString::fromLocal8Bit(e.what()), AuditLogEntry())); } catch (const std::exception &e) { emitResult(fromDecryptResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), AuditLogEntry())); } catch (...) { emitResult(fromDecryptResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), AuditLogEntry())); } } class VerifyOpaqueTask::Private { VerifyOpaqueTask *const q; public: explicit Private(VerifyOpaqueTask *qq) : q{qq} { } void startVerifyOpaqueJob(); void startDecryptVerifyArchiveJob(); void slotResult(const VerificationResult &, const QByteArray & = {}); std::shared_ptr m_input; std::shared_ptr m_output; const QGpgME::Protocol *m_backend = nullptr; Protocol m_protocol = UnknownProtocol; bool m_extractArchive = false; QString m_inputFilePath; QString m_outputFilePath; QString m_outputDirectory; }; void VerifyOpaqueTask::Private::slotResult(const VerificationResult &result, const QByteArray &plainText) { updateKeys(result); { std::stringstream ss; ss << result; qCDebug(KLEOPATRA_LOG) << ss.str().c_str(); } const AuditLogEntry auditLog = auditLogFromSender(q->sender()); if (m_output) { if (result.error().code()) { m_output->cancel(); } else { try { kleo_assert(!result.isNull()); m_output->finalize(); } catch (const GpgME::Exception &e) { q->emitResult(q->fromVerifyOpaqueResult(e.error(), QString::fromLocal8Bit(e.what()), auditLog)); return; } catch (const std::exception &e) { q->emitResult( q->fromVerifyOpaqueResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), auditLog)); return; } catch (...) { q->emitResult(q->fromVerifyOpaqueResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), auditLog)); return; } } } const int drErr = result.error().code(); const QString errorString = m_output ? m_output->errorString() : QString{}; if (((drErr == GPG_ERR_EIO || drErr == GPG_ERR_NO_DATA) && !errorString.isEmpty()) || (m_output && m_output->failed())) { q->emitResult(q->fromVerifyOpaqueResult(result.error() ? result.error() : Error::fromCode(GPG_ERR_EIO), errorString, auditLog)); return; } q->emitResult(q->fromVerifyOpaqueResult(result, plainText, auditLog)); } VerifyOpaqueTask::VerifyOpaqueTask(QObject *parent) : AbstractDecryptVerifyTask(parent) , d(new Private(this)) { } VerifyOpaqueTask::~VerifyOpaqueTask() { } void VerifyOpaqueTask::setInput(const std::shared_ptr &input) { d->m_input = input; kleo_assert(d->m_input && d->m_input->ioDevice()); } void VerifyOpaqueTask::setOutput(const std::shared_ptr &output) { d->m_output = output; kleo_assert(d->m_output && d->m_output->ioDevice()); } void VerifyOpaqueTask::setProtocol(Protocol prot) { kleo_assert(prot != UnknownProtocol); d->m_protocol = prot; d->m_backend = (prot == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(d->m_backend); } void VerifyOpaqueTask::autodetectProtocolFromInput() { if (!d->m_input) { return; } const Protocol p = findProtocol(d->m_input->classification()); if (p == UnknownProtocol) { throw Exception(gpg_error(GPG_ERR_NOTHING_FOUND), i18n("Could not determine whether this is an S/MIME or an OpenPGP signature - maybe it is not a signature at all?"), Exception::MessageOnly); } setProtocol(p); } QString VerifyOpaqueTask::label() const { return i18n("Verifying %1...", inputLabel()); } unsigned long long VerifyOpaqueTask::inputSize() const { return d->m_input ? d->m_input->size() : 0; } QString VerifyOpaqueTask::inputLabel() const { return d->m_input ? d->m_input->label() : QFileInfo{d->m_inputFilePath}.fileName(); } QString VerifyOpaqueTask::outputLabel() const { if (d->m_output) { return d->m_output->label(); } else if (!d->m_outputFilePath.isEmpty()) { return QFileInfo{d->m_outputFilePath}.fileName(); } else { return d->m_outputDirectory; } } Protocol VerifyOpaqueTask::protocol() const { return d->m_protocol; } void VerifyOpaqueTask::setExtractArchive(bool extract) { d->m_extractArchive = extract; } void VerifyOpaqueTask::setInputFile(const QString &path) { d->m_inputFilePath = path; } void VerifyOpaqueTask::setOutputFile(const QString &path) { d->m_outputFilePath = path; } void VerifyOpaqueTask::setOutputDirectory(const QString &directory) { d->m_outputDirectory = directory; } void VerifyOpaqueTask::doStart() { kleo_assert(d->m_backend); if (d->m_extractArchive && archiveJobsCanBeUsed(d->m_protocol)) { d->startDecryptVerifyArchiveJob(); } else { d->startVerifyOpaqueJob(); } } void VerifyOpaqueTask::Private::startVerifyOpaqueJob() { #if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO - if (QFile::exists(m_outputFilePath)) { + if (!m_outputFilePath.isEmpty() && QFile::exists(m_outputFilePath)) { // The output files are always written to a temporary location. Therefore, this can only occur // if two signed/encrypted files with the same name in different folders are verified/decrypted // because they would be written to the same temporary location. QMetaObject::invokeMethod( q, [this]() { slotResult(VerificationResult{Error::fromCode(GPG_ERR_EEXIST)}); }, Qt::QueuedConnection); return; } #endif try { std::unique_ptr job{m_backend->verifyOpaqueJob()}; kleo_assert(job); connect(job.get(), &QGpgME::VerifyOpaqueJob::result, q, [this](const GpgME::VerificationResult &result, const QByteArray &plainText) { slotResult(result, plainText); }); connect(job.get(), &QGpgME::Job::jobProgress, q, &VerifyOpaqueTask::setProgress); #if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO - job->setInputFile(m_inputFilePath); - job->setOutputFile(m_outputFilePath); - const auto err = job->startIt(); + if (!m_inputFilePath.isEmpty() && !m_outputFilePath.isEmpty()) { + job->setInputFile(m_inputFilePath); + job->setOutputFile(m_outputFilePath); + const auto err = job->startIt(); + } else { + ensureIOOpen(m_input->ioDevice().get(), m_output ? m_output->ioDevice().get() : nullptr); + job->start(m_input->ioDevice(), m_output ? m_output->ioDevice() : std::shared_ptr()); + } #else ensureIOOpen(m_input->ioDevice().get(), m_output ? m_output->ioDevice().get() : nullptr); job->start(m_input->ioDevice(), m_output ? m_output->ioDevice() : std::shared_ptr()); #endif q->setJob(job.release()); } catch (const GpgME::Exception &e) { q->emitResult(q->fromVerifyOpaqueResult(e.error(), QString::fromLocal8Bit(e.what()), AuditLogEntry())); } catch (const std::exception &e) { q->emitResult( q->fromVerifyOpaqueResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), AuditLogEntry())); } catch (...) { q->emitResult(q->fromVerifyOpaqueResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), AuditLogEntry())); } } void VerifyOpaqueTask::Private::startDecryptVerifyArchiveJob() { std::unique_ptr job{m_backend->decryptVerifyArchiveJob()}; kleo_assert(job); connect(job.get(), &QGpgME::DecryptVerifyArchiveJob::result, q, [this](const DecryptionResult &, const VerificationResult &verifyResult) { slotResult(verifyResult); }); connect(job.get(), &QGpgME::DecryptVerifyArchiveJob::dataProgress, q, &VerifyOpaqueTask::setProgress); #if QGPGME_ARCHIVE_JOBS_SUPPORT_INPUT_FILENAME // make sure that we don't use an existing output directory const auto outputDirectory = ensureUniqueDirectory(m_outputDirectory); if (outputDirectory.isEmpty()) { q->emitResult(q->fromDecryptVerifyResult(Error::fromCode(GPG_ERR_GENERAL), {}, {})); return; } m_outputDirectory = outputDirectory; job->setInputFile(m_inputFilePath); job->setOutputDirectory(m_outputDirectory); const auto err = job->startIt(); #else ensureIOOpen(m_input->ioDevice().get(), nullptr); job->setOutputDirectory(m_outputDirectory); const auto err = job->start(m_input->ioDevice()); #endif q->setJob(job.release()); if (err) { q->emitResult(q->fromVerifyOpaqueResult(err, {}, {})); } } class VerifyDetachedTask::Private { VerifyDetachedTask *const q; public: explicit Private(VerifyDetachedTask *qq) : q{qq} { } void slotResult(const VerificationResult &); void registerJob(QGpgME::VerifyDetachedJob *job) { q->connect(job, SIGNAL(result(GpgME::VerificationResult)), q, SLOT(slotResult(GpgME::VerificationResult))); q->connect(job, &QGpgME::Job::jobProgress, q, &VerifyDetachedTask::setProgress); } QString signatureLabel() const; QString signedDataLabel() const; std::shared_ptr m_input, m_signedData; const QGpgME::Protocol *m_backend = nullptr; Protocol m_protocol = UnknownProtocol; QString m_signatureFilePath; QString m_signedFilePath; }; void VerifyDetachedTask::Private::slotResult(const VerificationResult &result) { updateKeys(result); { std::stringstream ss; ss << result; qCDebug(KLEOPATRA_LOG) << ss.str().c_str(); } const AuditLogEntry auditLog = auditLogFromSender(q->sender()); try { kleo_assert(!result.isNull()); q->emitResult(q->fromVerifyDetachedResult(result, auditLog)); } catch (const GpgME::Exception &e) { q->emitResult(q->fromVerifyDetachedResult(e.error(), QString::fromLocal8Bit(e.what()), auditLog)); } catch (const std::exception &e) { q->emitResult(q->fromVerifyDetachedResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), auditLog)); } catch (...) { q->emitResult(q->fromVerifyDetachedResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), auditLog)); } } QString VerifyDetachedTask::Private::signatureLabel() const { return m_input ? m_input->label() : m_signatureFilePath; } QString VerifyDetachedTask::Private::signedDataLabel() const { return m_signedData ? m_signedData->label() : m_signedFilePath; } VerifyDetachedTask::VerifyDetachedTask(QObject *parent) : AbstractDecryptVerifyTask(parent) , d(new Private(this)) { } VerifyDetachedTask::~VerifyDetachedTask() { } void VerifyDetachedTask::setInput(const std::shared_ptr &input) { d->m_input = input; kleo_assert(d->m_input && d->m_input->ioDevice()); } void VerifyDetachedTask::setSignedData(const std::shared_ptr &signedData) { d->m_signedData = signedData; kleo_assert(d->m_signedData && d->m_signedData->ioDevice()); } void VerifyDetachedTask::setSignatureFile(const QString &path) { d->m_signatureFilePath = path; } void VerifyDetachedTask::setSignedFile(const QString &path) { d->m_signedFilePath = path; } void VerifyDetachedTask::setProtocol(Protocol prot) { kleo_assert(prot != UnknownProtocol); d->m_protocol = prot; d->m_backend = (prot == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(d->m_backend); } void VerifyDetachedTask::autodetectProtocolFromInput() { if (!d->m_input) { return; } const Protocol p = findProtocol(d->m_input->classification()); if (p == UnknownProtocol) { throw Exception(gpg_error(GPG_ERR_NOTHING_FOUND), i18n("Could not determine whether this is an S/MIME or an OpenPGP signature - maybe it is not a signature at all?"), Exception::MessageOnly); } setProtocol(p); } unsigned long long VerifyDetachedTask::inputSize() const { return d->m_signedData ? d->m_signedData->size() : 0; } QString VerifyDetachedTask::label() const { const QString signedDataLabel = d->signedDataLabel(); if (!signedDataLabel.isEmpty()) { return xi18nc( "Verification of a detached signature in progress. The first file contains the data." "The second file is the signature file.", "Verifying %1 with %2...", signedDataLabel, d->signatureLabel()); } return i18n("Verifying signature %1...", d->signatureLabel()); } QString VerifyDetachedTask::inputLabel() const { const QString signatureLabel = d->signatureLabel(); const QString signedDataLabel = d->signedDataLabel(); if (!signedDataLabel.isEmpty() && !signatureLabel.isEmpty()) { return xi18nc( "Verification of a detached signature summary. The first file contains the data." "The second file is signature.", "Verified %1 with %2", signedDataLabel, signatureLabel); } return signatureLabel; } QString VerifyDetachedTask::outputLabel() const { return QString(); } Protocol VerifyDetachedTask::protocol() const { return d->m_protocol; } void VerifyDetachedTask::doStart() { kleo_assert(d->m_backend); try { std::unique_ptr job{d->m_backend->verifyDetachedJob()}; kleo_assert(job); d->registerJob(job.get()); #if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO - if (d->m_protocol == GpgME::OpenPGP) { + if (d->m_protocol == GpgME::OpenPGP && !d->m_signatureFilePath.isEmpty() && !d->m_signedFilePath.isEmpty()) { job->setSignatureFile(d->m_signatureFilePath); job->setSignedFile(d->m_signedFilePath); job->startIt(); } else { ensureIOOpen(d->m_input->ioDevice().get(), nullptr); ensureIOOpen(d->m_signedData->ioDevice().get(), nullptr); job->start(d->m_input->ioDevice(), d->m_signedData->ioDevice()); } #else ensureIOOpen(d->m_input->ioDevice().get(), nullptr); ensureIOOpen(d->m_signedData->ioDevice().get(), nullptr); job->start(d->m_input->ioDevice(), d->m_signedData->ioDevice()); #endif setJob(job.release()); } catch (const GpgME::Exception &e) { emitResult(fromVerifyDetachedResult(e.error(), QString::fromLocal8Bit(e.what()), AuditLogEntry())); } catch (const std::exception &e) { emitResult( fromVerifyDetachedResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), AuditLogEntry())); } catch (...) { emitResult(fromVerifyDetachedResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), AuditLogEntry())); } } #include "moc_decryptverifytask.cpp" diff --git a/src/crypto/signencrypttask.cpp b/src/crypto/signencrypttask.cpp index 62ff33f71..f332e654e 100644 --- a/src/crypto/signencrypttask.cpp +++ b/src/crypto/signencrypttask.cpp @@ -1,972 +1,973 @@ /* -*- mode: c++; c-basic-offset:4 -*- crypto/signencrypttask.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 "signencrypttask.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" #include #include #include // for Qt::escape using namespace Kleo; using namespace Kleo::Crypto; using namespace GpgME; namespace { QString formatInputOutputLabel(const QString &input, const QString &output, bool outputDeleted) { return i18nc("Input file --> Output file (rarr is arrow", "%1 → %2", input.toHtmlEscaped(), outputDeleted ? QStringLiteral("%1").arg(output.toHtmlEscaped()) : output.toHtmlEscaped()); } class ErrorResult : public Task::Result { public: ErrorResult(bool sign, bool encrypt, const Error &err, const QString &errStr, const QString &input, const QString &output, const AuditLogEntry &auditLog) : Task::Result() , m_sign(sign) , m_encrypt(encrypt) , m_error(err) , m_errString(errStr) , m_inputLabel(input) , m_outputLabel(output) , m_auditLog(auditLog) { } QString overview() const override; QString details() const override; GpgME::Error error() const override { return m_error; } QString errorString() const override { return m_errString; } VisualCode code() const override { return NeutralError; } AuditLogEntry auditLog() const override { return m_auditLog; } private: const bool m_sign; const bool m_encrypt; const Error m_error; const QString m_errString; const QString m_inputLabel; const QString m_outputLabel; const AuditLogEntry m_auditLog; }; namespace { struct LabelAndError { QString label; QString errorString; }; } class SignEncryptFilesResult : public Task::Result { public: SignEncryptFilesResult(const SigningResult &sr, const LabelAndError &input, const LabelAndError &output, bool outputCreated, const AuditLogEntry &auditLog) : Task::Result() , m_sresult(sr) , m_input{input} , m_output{output} , m_outputCreated(outputCreated) , m_auditLog(auditLog) { qCDebug(KLEOPATRA_LOG) << "\ninputError :" << m_input.errorString << "\noutputError:" << m_output.errorString; Q_ASSERT(!m_sresult.isNull()); } SignEncryptFilesResult(const EncryptionResult &er, const LabelAndError &input, const LabelAndError &output, bool outputCreated, const AuditLogEntry &auditLog) : Task::Result() , m_eresult(er) , m_input{input} , m_output{output} , m_outputCreated(outputCreated) , m_auditLog(auditLog) { qCDebug(KLEOPATRA_LOG) << "\ninputError :" << m_input.errorString << "\noutputError:" << m_output.errorString; Q_ASSERT(!m_eresult.isNull()); } SignEncryptFilesResult(const SigningResult &sr, const EncryptionResult &er, const LabelAndError &input, const LabelAndError &output, bool outputCreated, const AuditLogEntry &auditLog) : Task::Result() , m_sresult(sr) , m_eresult(er) , m_input{input} , m_output{output} , m_outputCreated(outputCreated) , m_auditLog(auditLog) { qCDebug(KLEOPATRA_LOG) << "\ninputError :" << m_input.errorString << "\noutputError:" << m_output.errorString; Q_ASSERT(!m_sresult.isNull() || !m_eresult.isNull()); } QString overview() const override; QString details() const override; GpgME::Error error() const override; QString errorString() const override; VisualCode code() const override; AuditLogEntry auditLog() const override; private: const SigningResult m_sresult; const EncryptionResult m_eresult; const LabelAndError m_input; const LabelAndError m_output; const bool m_outputCreated; const AuditLogEntry m_auditLog; }; static QString makeSigningOverview(const Error &err) { if (err.isCanceled()) { return i18n("Signing canceled."); } if (err) { return i18n("Signing failed."); } return i18n("Signing succeeded."); } static QString makeResultOverview(const SigningResult &result) { return makeSigningOverview(result.error()); } static QString makeEncryptionOverview(const Error &err) { if (err.isCanceled()) { return i18n("Encryption canceled."); } if (err) { return i18n("Encryption failed."); } return i18n("Encryption succeeded."); } static QString makeResultOverview(const EncryptionResult &result) { return makeEncryptionOverview(result.error()); } static QString makeResultOverview(const SigningResult &sr, const EncryptionResult &er) { if (er.isNull() && sr.isNull()) { return QString(); } if (er.isNull()) { return makeResultOverview(sr); } if (sr.isNull()) { return makeResultOverview(er); } if (sr.error().isCanceled() || sr.error()) { return makeResultOverview(sr); } if (er.error().isCanceled() || er.error()) { return makeResultOverview(er); } return i18n("Signing and encryption succeeded."); } static QString escape(QString s) { s = s.toHtmlEscaped(); s.replace(QLatin1Char('\n'), QStringLiteral("
")); return s; } static QString makeResultDetails(const SigningResult &result, const QString &inputError, const QString &outputError) { const Error err = result.error(); if (err.code() == GPG_ERR_EIO) { if (!inputError.isEmpty()) { return i18n("Input error: %1", escape(inputError)); } else if (!outputError.isEmpty()) { return i18n("Output error: %1", escape(outputError)); } } if (err || err.isCanceled()) { return Formatting::errorAsString(err).toHtmlEscaped(); } return QString(); } static QString makeResultDetails(const EncryptionResult &result, const QString &inputError, const QString &outputError) { const Error err = result.error(); if (err.code() == GPG_ERR_EIO) { if (!inputError.isEmpty()) { return i18n("Input error: %1", escape(inputError)); } else if (!outputError.isEmpty()) { return i18n("Output error: %1", escape(outputError)); } } if (err || err.isCanceled()) { return Formatting::errorAsString(err).toHtmlEscaped(); } return i18n(" Encryption succeeded."); } } QString ErrorResult::overview() const { Q_ASSERT(m_error || m_error.isCanceled()); Q_ASSERT(m_sign || m_encrypt); const QString label = formatInputOutputLabel(m_inputLabel, m_outputLabel, true); const bool canceled = m_error.isCanceled(); if (m_sign && m_encrypt) { return canceled ? i18n("%1: Sign/encrypt canceled.", label) : i18n(" %1: Sign/encrypt failed.", label); } return i18nc("label: result. Example: foo -> foo.gpg: Encryption failed.", "%1: %2", label, m_sign ? makeSigningOverview(m_error) : makeEncryptionOverview(m_error)); } QString ErrorResult::details() const { return m_errString; } class SignEncryptTask::Private { friend class ::Kleo::Crypto::SignEncryptTask; SignEncryptTask *const q; public: explicit Private(SignEncryptTask *qq); private: QString inputLabel() const; QString outputLabel() const; bool removeExistingOutputFile(); void startSignEncryptJob(GpgME::Protocol proto); std::unique_ptr createSignJob(GpgME::Protocol proto); std::unique_ptr createSignEncryptJob(GpgME::Protocol proto); std::unique_ptr createEncryptJob(GpgME::Protocol proto); void startSignEncryptArchiveJob(GpgME::Protocol proto); std::unique_ptr createSignArchiveJob(GpgME::Protocol proto); std::unique_ptr createSignEncryptArchiveJob(GpgME::Protocol proto); std::unique_ptr createEncryptArchiveJob(GpgME::Protocol proto); std::shared_ptr makeErrorResult(const Error &err, const QString &errStr, const AuditLogEntry &auditLog); private: void slotResult(const SigningResult &); void slotResult(const SigningResult &, const EncryptionResult &); void slotResult(const EncryptionResult &); void slotResult(const QGpgME::Job *, const SigningResult &, const EncryptionResult &); private: std::shared_ptr input; std::shared_ptr output; QStringList inputFileNames; QString outputFileName; std::vector signers; std::vector recipients; bool sign : 1; bool encrypt : 1; bool detached : 1; bool symmetric : 1; bool clearsign : 1; bool archive : 1; QPointer job; QString labelText; std::shared_ptr m_overwritePolicy; }; SignEncryptTask::Private::Private(SignEncryptTask *qq) : q{qq} , sign{true} , encrypt{true} , detached{false} , clearsign{false} , archive{false} , m_overwritePolicy{new OverwritePolicy{OverwritePolicy::Ask}} { q->setAsciiArmor(true); } std::shared_ptr SignEncryptTask::Private::makeErrorResult(const Error &err, const QString &errStr, const AuditLogEntry &auditLog) { return std::shared_ptr(new ErrorResult(sign, encrypt, err, errStr, inputLabel(), outputLabel(), auditLog)); } SignEncryptTask::SignEncryptTask(QObject *p) : Task(p) , d(new Private(this)) { } SignEncryptTask::~SignEncryptTask() { } void SignEncryptTask::setInputFileName(const QString &fileName) { kleo_assert(!d->job); kleo_assert(!fileName.isEmpty()); d->inputFileNames = QStringList(fileName); } void SignEncryptTask::setInputFileNames(const QStringList &fileNames) { kleo_assert(!d->job); kleo_assert(!fileNames.empty()); d->inputFileNames = fileNames; } void SignEncryptTask::setInput(const std::shared_ptr &input) { kleo_assert(!d->job); kleo_assert(input); d->input = input; } void SignEncryptTask::setOutput(const std::shared_ptr &output) { kleo_assert(!d->job); kleo_assert(output); d->output = output; } void SignEncryptTask::setOutputFileName(const QString &fileName) { kleo_assert(!d->job); kleo_assert(!fileName.isEmpty()); d->outputFileName = fileName; } QString SignEncryptTask::outputFileName() const { return d->outputFileName; } void SignEncryptTask::setSigners(const std::vector &signers) { kleo_assert(!d->job); d->signers = signers; } void SignEncryptTask::setRecipients(const std::vector &recipients) { kleo_assert(!d->job); d->recipients = recipients; } void SignEncryptTask::setOverwritePolicy(const std::shared_ptr &policy) { kleo_assert(!d->job); d->m_overwritePolicy = policy; } void SignEncryptTask::setSign(bool sign) { kleo_assert(!d->job); d->sign = sign; } void SignEncryptTask::setEncrypt(bool encrypt) { kleo_assert(!d->job); d->encrypt = encrypt; } void SignEncryptTask::setDetachedSignature(bool detached) { kleo_assert(!d->job); d->detached = detached; } void SignEncryptTask::setEncryptSymmetric(bool symmetric) { kleo_assert(!d->job); d->symmetric = symmetric; } void SignEncryptTask::setClearsign(bool clearsign) { kleo_assert(!d->job); d->clearsign = clearsign; } void SignEncryptTask::setCreateArchive(bool archive) { kleo_assert(!d->job); d->archive = archive; } Protocol SignEncryptTask::protocol() const { if (d->sign && !d->signers.empty()) { return d->signers.front().protocol(); } if (d->encrypt || d->symmetric) { if (!d->recipients.empty()) { return d->recipients.front().protocol(); } else { return GpgME::OpenPGP; // symmetric OpenPGP encryption } } throw Kleo::Exception(gpg_error(GPG_ERR_INTERNAL), i18n("Cannot determine protocol for task")); } QString SignEncryptTask::label() const { if (!d->labelText.isEmpty()) { return d->labelText; } return d->inputLabel(); } QString SignEncryptTask::tag() const { return Formatting::displayName(protocol()); } unsigned long long SignEncryptTask::inputSize() const { return d->input ? d->input->size() : 0U; } static bool archiveJobsCanBeUsed(GpgME::Protocol protocol) { return (protocol == GpgME::OpenPGP) && QGpgME::SignEncryptArchiveJob::isSupported(); } void SignEncryptTask::doStart() { kleo_assert(!d->job); if (d->sign) { kleo_assert(!d->signers.empty()); if (d->archive) { kleo_assert(!d->detached && !d->clearsign); } } const auto proto = protocol(); if (d->archive && archiveJobsCanBeUsed(proto)) { d->startSignEncryptArchiveJob(proto); } else { d->startSignEncryptJob(proto); } } QString SignEncryptTask::Private::inputLabel() const { if (input) { return input->label(); } if (!inputFileNames.empty()) { const auto firstFile = QFileInfo{inputFileNames.front()}.fileName(); return inputFileNames.size() == 1 ? firstFile : i18nc(", ...", "%1, ...", firstFile); } return {}; } QString SignEncryptTask::Private::outputLabel() const { return output ? output->label() : QFileInfo{outputFileName}.fileName(); } bool SignEncryptTask::Private::removeExistingOutputFile() { if (QFile::exists(outputFileName)) { bool fileRemoved = false; // we should already have asked the user for overwrite permission if (m_overwritePolicy && (m_overwritePolicy->policy() == OverwritePolicy::Overwrite)) { qCDebug(KLEOPATRA_LOG) << __func__ << "going to remove file for overwriting" << outputFileName; fileRemoved = QFile::remove(outputFileName); if (!fileRemoved) { qCDebug(KLEOPATRA_LOG) << __func__ << "removing file to overwrite failed"; } } else { qCDebug(KLEOPATRA_LOG) << __func__ << "we have no permission to overwrite" << outputFileName; } if (!fileRemoved) { QMetaObject::invokeMethod( q, [this]() { slotResult(nullptr, SigningResult{}, EncryptionResult{Error::fromCode(GPG_ERR_EEXIST)}); }, Qt::QueuedConnection); return false; } } return true; } void SignEncryptTask::Private::startSignEncryptJob(GpgME::Protocol proto) { #if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO if (proto == GpgME::OpenPGP) { - kleo_assert(!input); - kleo_assert(!output); + // either input and output are both set (e.g. when encrypting the notepad), + // or they are both unset (when encrypting files) + kleo_assert((!input && !output) || (input && output)); } else { kleo_assert(input); if (!output) { output = Output::createFromFile(outputFileName, m_overwritePolicy); } } #else kleo_assert(input); if (!output) { output = Output::createFromFile(outputFileName, m_overwritePolicy); } #endif if (encrypt || symmetric) { Context::EncryptionFlags flags{Context::None}; if (proto == GpgME::OpenPGP) { flags = static_cast(flags | Context::AlwaysTrust); } if (symmetric) { flags = static_cast(flags | Context::Symmetric); qCDebug(KLEOPATRA_LOG) << "Adding symmetric flag"; } if (sign) { std::unique_ptr job = createSignEncryptJob(proto); kleo_assert(job.get()); #if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO - if (proto == GpgME::OpenPGP) { + if (proto == GpgME::OpenPGP && !input && !output) { kleo_assert(inputFileNames.size() == 1); job->setSigners(signers); job->setRecipients(recipients); job->setInputFile(inputFileNames.front()); job->setOutputFile(outputFileName); job->setEncryptionFlags(flags); if (!removeExistingOutputFile()) { return; } job->startIt(); } else { if (inputFileNames.size() == 1) { job->setFileName(inputFileNames.front()); } job->start(signers, recipients, input->ioDevice(), output->ioDevice(), flags); } #else if (inputFileNames.size() == 1) { job->setFileName(inputFileNames.front()); } job->start(signers, recipients, input->ioDevice(), output->ioDevice(), flags); #endif this->job = job.release(); } else { std::unique_ptr job = createEncryptJob(proto); kleo_assert(job.get()); #if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO - if (proto == GpgME::OpenPGP) { + if (proto == GpgME::OpenPGP && !input && !output) { kleo_assert(inputFileNames.size() == 1); job->setRecipients(recipients); job->setInputFile(inputFileNames.front()); job->setOutputFile(outputFileName); job->setEncryptionFlags(flags); if (!removeExistingOutputFile()) { return; } job->startIt(); } else { if (inputFileNames.size() == 1) { job->setFileName(inputFileNames.front()); } job->start(recipients, input->ioDevice(), output->ioDevice(), flags); } #else if (inputFileNames.size() == 1) { job->setFileName(inputFileNames.front()); } job->start(recipients, input->ioDevice(), output->ioDevice(), flags); #endif this->job = job.release(); } } else if (sign) { std::unique_ptr job = createSignJob(proto); kleo_assert(job.get()); kleo_assert(!(detached && clearsign)); const GpgME::SignatureMode sigMode = detached ? GpgME::Detached : clearsign ? GpgME::Clearsigned : GpgME::NormalSignatureMode; #if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO - if (proto == GpgME::OpenPGP) { + if (proto == GpgME::OpenPGP && !input && !output) { kleo_assert(inputFileNames.size() == 1); job->setSigners(signers); job->setInputFile(inputFileNames.front()); job->setOutputFile(outputFileName); job->setSigningFlags(sigMode); if (!removeExistingOutputFile()) { return; } job->startIt(); } else { job->start(signers, input->ioDevice(), output->ioDevice(), sigMode); } #else job->start(signers, input->ioDevice(), output->ioDevice(), sigMode); #endif this->job = job.release(); } else { kleo_assert(!"Either 'sign' or 'encrypt' or 'symmetric' must be set!"); } } void SignEncryptTask::cancel() { qCDebug(KLEOPATRA_LOG) << this << __func__; if (d->job) { d->job->slotCancel(); } } std::unique_ptr SignEncryptTask::Private::createSignJob(GpgME::Protocol proto) { const QGpgME::Protocol *const backend = (proto == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(backend); std::unique_ptr signJob(backend->signJob(q->asciiArmor(), /*textmode=*/false)); kleo_assert(signJob.get()); connect(signJob.get(), &QGpgME::Job::jobProgress, q, &SignEncryptTask::setProgress); connect(signJob.get(), SIGNAL(result(GpgME::SigningResult, QByteArray)), q, SLOT(slotResult(GpgME::SigningResult))); return signJob; } std::unique_ptr SignEncryptTask::Private::createSignEncryptJob(GpgME::Protocol proto) { const QGpgME::Protocol *const backend = (proto == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(backend); std::unique_ptr signEncryptJob(backend->signEncryptJob(q->asciiArmor(), /*textmode=*/false)); kleo_assert(signEncryptJob.get()); connect(signEncryptJob.get(), &QGpgME::Job::jobProgress, q, &SignEncryptTask::setProgress); connect(signEncryptJob.get(), SIGNAL(result(GpgME::SigningResult, GpgME::EncryptionResult, QByteArray)), q, SLOT(slotResult(GpgME::SigningResult, GpgME::EncryptionResult))); return signEncryptJob; } std::unique_ptr SignEncryptTask::Private::createEncryptJob(GpgME::Protocol proto) { const QGpgME::Protocol *const backend = (proto == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(backend); std::unique_ptr encryptJob(backend->encryptJob(q->asciiArmor(), /*textmode=*/false)); kleo_assert(encryptJob.get()); connect(encryptJob.get(), &QGpgME::Job::jobProgress, q, &SignEncryptTask::setProgress); connect(encryptJob.get(), SIGNAL(result(GpgME::EncryptionResult, QByteArray)), q, SLOT(slotResult(GpgME::EncryptionResult))); return encryptJob; } void SignEncryptTask::Private::startSignEncryptArchiveJob(GpgME::Protocol proto) { kleo_assert(!input); kleo_assert(!output); #if !QGPGME_ARCHIVE_JOBS_SUPPORT_OUTPUT_FILENAME output = Output::createFromFile(outputFileName, m_overwritePolicy); #endif const auto baseDirectory = heuristicBaseDirectory(inputFileNames); if (baseDirectory.isEmpty()) { throw Kleo::Exception(GPG_ERR_CONFLICT, i18n("Cannot find common base directory for these files:\n%1", inputFileNames.join(QLatin1Char('\n')))); } qCDebug(KLEOPATRA_LOG) << "heuristicBaseDirectory(" << inputFileNames << ") ->" << baseDirectory; const auto tempPaths = makeRelativeTo(baseDirectory, inputFileNames); const auto relativePaths = std::vector{tempPaths.begin(), tempPaths.end()}; qCDebug(KLEOPATRA_LOG) << "relative paths:" << relativePaths; if (encrypt || symmetric) { Context::EncryptionFlags flags{Context::None}; if (proto == GpgME::OpenPGP) { flags = static_cast(flags | Context::AlwaysTrust); } if (symmetric) { flags = static_cast(flags | Context::Symmetric); qCDebug(KLEOPATRA_LOG) << "Adding symmetric flag"; } if (sign) { labelText = i18nc("@info", "Creating signed and encrypted archive ..."); std::unique_ptr job = createSignEncryptArchiveJob(proto); kleo_assert(job.get()); job->setBaseDirectory(baseDirectory); #if QGPGME_ARCHIVE_JOBS_SUPPORT_OUTPUT_FILENAME job->setSigners(signers); job->setRecipients(recipients); job->setInputPaths(relativePaths); job->setOutputFile(outputFileName); job->setEncryptionFlags(flags); if (!removeExistingOutputFile()) { return; } job->startIt(); #else job->start(signers, recipients, relativePaths, output->ioDevice(), flags); #endif this->job = job.release(); } else { labelText = i18nc("@info", "Creating encrypted archive ..."); std::unique_ptr job = createEncryptArchiveJob(proto); kleo_assert(job.get()); job->setBaseDirectory(baseDirectory); #if QGPGME_ARCHIVE_JOBS_SUPPORT_OUTPUT_FILENAME job->setRecipients(recipients); job->setInputPaths(relativePaths); job->setOutputFile(outputFileName); job->setEncryptionFlags(flags); if (!removeExistingOutputFile()) { return; } job->startIt(); #else job->start(recipients, relativePaths, output->ioDevice(), flags); #endif this->job = job.release(); } } else if (sign) { labelText = i18nc("@info", "Creating signed archive ..."); std::unique_ptr job = createSignArchiveJob(proto); kleo_assert(job.get()); job->setBaseDirectory(baseDirectory); #if QGPGME_ARCHIVE_JOBS_SUPPORT_OUTPUT_FILENAME job->setSigners(signers); job->setInputPaths(relativePaths); job->setOutputFile(outputFileName); if (!removeExistingOutputFile()) { return; } job->startIt(); #else job->start(signers, relativePaths, output->ioDevice()); #endif this->job = job.release(); } else { kleo_assert(!"Either 'sign' or 'encrypt' or 'symmetric' must be set!"); } } std::unique_ptr SignEncryptTask::Private::createSignArchiveJob(GpgME::Protocol proto) { const QGpgME::Protocol *const backend = (proto == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(backend); std::unique_ptr signJob(backend->signArchiveJob(q->asciiArmor())); auto job = signJob.get(); kleo_assert(job); connect(job, &QGpgME::SignArchiveJob::dataProgress, q, &SignEncryptTask::setProgress); connect(job, &QGpgME::SignArchiveJob::result, q, [this, job](const GpgME::SigningResult &signResult) { slotResult(job, signResult, EncryptionResult{}); }); return signJob; } std::unique_ptr SignEncryptTask::Private::createSignEncryptArchiveJob(GpgME::Protocol proto) { const QGpgME::Protocol *const backend = (proto == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(backend); std::unique_ptr signEncryptJob(backend->signEncryptArchiveJob(q->asciiArmor())); auto job = signEncryptJob.get(); kleo_assert(job); connect(job, &QGpgME::SignEncryptArchiveJob::dataProgress, q, &SignEncryptTask::setProgress); connect(job, &QGpgME::SignEncryptArchiveJob::result, q, [this, job](const GpgME::SigningResult &signResult, const GpgME::EncryptionResult &encryptResult) { slotResult(job, signResult, encryptResult); }); return signEncryptJob; } std::unique_ptr SignEncryptTask::Private::createEncryptArchiveJob(GpgME::Protocol proto) { const QGpgME::Protocol *const backend = (proto == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(backend); std::unique_ptr encryptJob(backend->encryptArchiveJob(q->asciiArmor())); auto job = encryptJob.get(); kleo_assert(job); connect(job, &QGpgME::EncryptArchiveJob::dataProgress, q, &SignEncryptTask::setProgress); connect(job, &QGpgME::EncryptArchiveJob::result, q, [this, job](const GpgME::EncryptionResult &encryptResult) { slotResult(job, SigningResult{}, encryptResult); }); return encryptJob; } void SignEncryptTask::Private::slotResult(const SigningResult &result) { slotResult(qobject_cast(q->sender()), result, EncryptionResult{}); } void SignEncryptTask::Private::slotResult(const SigningResult &sresult, const EncryptionResult &eresult) { slotResult(qobject_cast(q->sender()), sresult, eresult); } void SignEncryptTask::Private::slotResult(const EncryptionResult &result) { slotResult(qobject_cast(q->sender()), SigningResult{}, result); } void SignEncryptTask::Private::slotResult(const QGpgME::Job *job, const SigningResult &sresult, const EncryptionResult &eresult) { qCDebug(KLEOPATRA_LOG) << q << __func__ << "job:" << job << "signing result:" << QGpgME::toLogString(sresult) << "encryption result:" << QGpgME::toLogString(eresult); const AuditLogEntry auditLog = AuditLogEntry::fromJob(job); bool outputCreated = false; if (input && input->failed()) { if (output) { output->cancel(); } q->emitResult(makeErrorResult(Error::fromCode(GPG_ERR_EIO), i18n("Input error: %1", escape(input->errorString())), auditLog)); return; } else if (sresult.error().code() || eresult.error().code()) { if (output) { output->cancel(); } if (!outputFileName.isEmpty() && eresult.error().code() != GPG_ERR_EEXIST) { // ensure that the output file is removed if the task was canceled or an error occurred; // unless a "file exists" error occurred because this means that the file with the name // of outputFileName wasn't created as result of this task if (QFile::exists(outputFileName)) { qCDebug(KLEOPATRA_LOG) << __func__ << "Removing output file" << outputFileName << "after error or cancel"; if (!QFile::remove(outputFileName)) { qCDebug(KLEOPATRA_LOG) << __func__ << "Removing output file" << outputFileName << "failed"; } } } } else { try { kleo_assert(!sresult.isNull() || !eresult.isNull()); if (output) { output->finalize(); } outputCreated = true; if (input) { input->finalize(); } } catch (const GpgME::Exception &e) { q->emitResult(makeErrorResult(e.error(), QString::fromLocal8Bit(e.what()), auditLog)); return; } } const LabelAndError inputInfo{inputLabel(), input ? input->errorString() : QString{}}; const LabelAndError outputInfo{outputLabel(), output ? output->errorString() : QString{}}; q->emitResult(std::shared_ptr(new SignEncryptFilesResult(sresult, eresult, inputInfo, outputInfo, outputCreated, auditLog))); } QString SignEncryptFilesResult::overview() const { const QString files = formatInputOutputLabel(m_input.label, m_output.label, !m_outputCreated); return files + QLatin1String(": ") + makeOverview(makeResultOverview(m_sresult, m_eresult)); } QString SignEncryptFilesResult::details() const { return errorString(); } GpgME::Error SignEncryptFilesResult::error() const { if (m_sresult.error().code()) { return m_sresult.error(); } if (m_eresult.error().code()) { return m_eresult.error(); } return {}; } QString SignEncryptFilesResult::errorString() const { const bool sign = !m_sresult.isNull(); const bool encrypt = !m_eresult.isNull(); kleo_assert(sign || encrypt); if (sign && encrypt) { return m_sresult.error().code() ? makeResultDetails(m_sresult, m_input.errorString, m_output.errorString) : m_eresult.error().code() ? makeResultDetails(m_eresult, m_input.errorString, m_output.errorString) : QString(); } return sign ? makeResultDetails(m_sresult, m_input.errorString, m_output.errorString) // : makeResultDetails(m_eresult, m_input.errorString, m_output.errorString); } Task::Result::VisualCode SignEncryptFilesResult::code() const { if (m_sresult.error().isCanceled() || m_eresult.error().isCanceled()) { return Warning; } return (m_sresult.error().code() || m_eresult.error().code()) ? NeutralError : NeutralSuccess; } AuditLogEntry SignEncryptFilesResult::auditLog() const { return m_auditLog; } #include "moc_signencrypttask.cpp"