diff --git a/src/crypto/autodecryptverifyfilescontroller.cpp b/src/crypto/autodecryptverifyfilescontroller.cpp --- a/src/crypto/autodecryptverifyfilescontroller.cpp +++ b/src/crypto/autodecryptverifyfilescontroller.cpp @@ -84,6 +84,15 @@ void exec(); std::vector > buildTasks(const QStringList &, QStringList &); + struct CryptoFile { + QString baseName; + QString fileName; + GpgME::Protocol protocol = GpgME::UnknownProtocol; + int classification = 0; + std::shared_ptr output; + }; + QVector classifyAndSortFiles(const QStringList &files); + void reportError(int err, const QString &details) { q->setLastError(err, details); @@ -210,77 +219,175 @@ return; } +QVector AutoDecryptVerifyFilesController::Private::classifyAndSortFiles(const QStringList &files) +{ + const auto isSignature = [](int classification) -> bool { + return mayBeDetachedSignature(classification) + || mayBeOpaqueSignature(classification) + || (classification & Class::TypeMask) == Class::ClearsignedMessage; + }; + + QVector out; + for (const auto &file : files) { + CryptoFile cFile; + cFile.fileName = file; + cFile.baseName = file.left(file.length() - 4); + cFile.classification = classify(file); + cFile.protocol = findProtocol(cFile.classification); + + auto it = std::find_if(out.begin(), out.end(), + [&cFile](const CryptoFile &other) { + return other.protocol == cFile.protocol + && other.baseName == cFile.baseName; + }); + if (it != out.end()) { + // If we found a file with the same basename, make sure that encrypted + // file is before the signature file, so that we first decrypt and then + // verify + if (isSignature(cFile.classification) && isCipherText(it->classification)) { + out.insert(it + 1, cFile); + } else if (isCipherText(cFile.classification) && isSignature(it->classification)) { + out.insert(it, cFile); + } else { + // both are signatures or both are encrypted files, in which + // case order does not matter + out.insert(it, cFile); + } + } else { + out.push_back(cFile); + } + } + + return out; +} + + std::vector< std::shared_ptr > AutoDecryptVerifyFilesController::Private::buildTasks(const QStringList &fileNames, QStringList &undetected) { - std::vector > tasks; - for (const QString &fName : fileNames) { - const auto classification = classify(fName); - const auto proto = findProtocol(classification); + // sort files so that we make sure we first decrypt and then verify + QVector cryptoFiles = classifyAndSortFiles(fileNames); - QFileInfo fi(fName); - qCDebug(KLEOPATRA_LOG) << "classified" << fName << "as" << printableClassification(classification); + 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.", fName)); - } else if (mayBeAnyCertStoreType(classification)) { + 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.", fName)); + xi18n("The file %1 contains certificates and can't be decrypted or verified.", cFile.fileName)); qCDebug(KLEOPATRA_LOG) << "reported error"; - } else if (isDetachedSignature(classification)) { + 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 = findSignedData(fName); + QString signedDataFileName = cFile.baseName; if (signedDataFileName.isEmpty()) { signedDataFileName = QFileDialog::getOpenFileName(nullptr, xi18n("Select the file to verify with \"%1\"", fi.fileName()), fi.dir().dirName()); } if (signedDataFileName.isEmpty()) { qCDebug(KLEOPATRA_LOG) << "No signed data selected. Verify abortet."; - continue; + } else { + qCDebug(KLEOPATRA_LOG) << "Detached verify: " << cFile.fileName << " Data: " << signedDataFileName; + std::shared_ptr t(new VerifyDetachedTask); + t->setInput(Input::createFromFile(cFile.fileName)); + t->setSignedData(Input::createFromFile(signedDataFileName)); + t->setProtocol(cFile.protocol); + tasks.push_back(t); } - qCDebug(KLEOPATRA_LOG) << "Detached verify: " << fName << " Data: " << signedDataFileName; - std::shared_ptr t(new VerifyDetachedTask); - t->setInput(Input::createFromFile(fName)); - t->setSignedData(Input::createFromFile(signedDataFileName)); - t->setProtocol(proto); - tasks.push_back(t); - } else if (!mayBeAnyMessageType(classification)) { + continue; + } + + if (!mayBeAnyMessageType(cFile.classification)) { // Not a Message? Maybe there is a signature for this file? - const auto signatures = findSignatures(fName); + const auto signatures = findSignatures(cFile.fileName); if (!signatures.empty()) { for (const QString &sig : signatures) { - qCDebug(KLEOPATRA_LOG) << "Guessing: " << sig << " is a signature for: " << fName; + qCDebug(KLEOPATRA_LOG) << "Guessing: " << sig << " is a signature for: " << cFile.fileName; std::shared_ptr t(new VerifyDetachedTask); t->setInput(Input::createFromFile(sig)); - t->setSignedData(Input::createFromFile(fName)); - t->setProtocol(proto); + t->setSignedData(Input::createFromFile(cFile.fileName)); + t->setProtocol(cFile.protocol); tasks.push_back(t); } } else { - undetected << fName; - qCDebug(KLEOPATRA_LOG) << "Failed detection for: " << fName << " adding to undetected."; + undetected << cFile.fileName; + qCDebug(KLEOPATRA_LOG) << "Failed detection for: " << cFile.fileName << " adding to undetected."; } } else { // Any Message type so we have input and output. - const auto input = Input::createFromFile(fName); + const auto input = Input::createFromFile(cFile.fileName); const auto archiveDefinitions = ArchiveDefinition::getArchiveDefinitions(); - const auto ad = q->pick_archive_definition(proto, archiveDefinitions, fName); + const auto ad = q->pick_archive_definition(cFile.protocol, archiveDefinitions, cFile.fileName); const auto wd = QDir(m_workDir.path()); const auto output = - ad ? ad->createOutputFromUnpackCommand(proto, fName, wd) : + ad ? ad->createOutputFromUnpackCommand(cFile.protocol, cFile.fileName, wd) : /*else*/ Output::createFromFile(wd.absoluteFilePath(outputFileName(fi.fileName())), false); - if (isOpaqueSignature(classification)) { + // If this might be opaque CMS signature, then try that. We already handled + // detached CMS signature above + const auto isCMSOpaqueSignature = cFile.protocol == GpgME::CMS && mayBeOpaqueSignature(cFile.classification); + + if (isOpaqueSignature(cFile.classification) || isCMSOpaqueSignature) { qCDebug(KLEOPATRA_LOG) << "creating a VerifyOpaqueTask"; std::shared_ptr t(new VerifyOpaqueTask); t->setInput(input); t->setOutput(output); - t->setProtocol(proto); + t->setProtocol(cFile.protocol); tasks.push_back(t); } else { // Any message. That is not an opaque signature needs to be @@ -290,7 +397,8 @@ std::shared_ptr t(new DecryptVerifyTask); t->setInput(input); t->setOutput(output); - t->setProtocol(proto); + t->setProtocol(cFile.protocol); + cFile.output = output; tasks.push_back(t); } } diff --git a/src/utils/input.h b/src/utils/input.h --- a/src/utils/input.h +++ b/src/utils/input.h @@ -46,7 +46,7 @@ namespace Kleo { - +class Output; class Input { public: @@ -63,6 +63,7 @@ static std::shared_ptr createFromPipeDevice(assuan_fd_t fd, const QString &label); static std::shared_ptr createFromFile(const QString &filename, bool dummy = false); static std::shared_ptr createFromFile(const std::shared_ptr &file); + static std::shared_ptr createFromOutput(const std::shared_ptr &output); // implemented in output.cpp static std::shared_ptr createFromProcessStdOut(const QString &command); static std::shared_ptr createFromProcessStdOut(const QString &command, const QStringList &args); static std::shared_ptr createFromProcessStdOut(const QString &command, const QStringList &args, const QDir &workingDirectory); diff --git a/src/utils/input.cpp b/src/utils/input.cpp --- a/src/utils/input.cpp +++ b/src/utils/input.cpp @@ -33,6 +33,7 @@ #include #include "input.h" +#include "input_p.h" #include "detail_p.h" #include "kdpipeiodevice.h" @@ -77,46 +78,6 @@ namespace { -class InputImplBase : public Input -{ -public: - InputImplBase() : Input(), m_customLabel(), m_defaultLabel() {} - - QString label() const override - { - return m_customLabel.isEmpty() ? m_defaultLabel : m_customLabel; - } - void setDefaultLabel(const QString &l) - { - m_defaultLabel = l; - } - void setLabel(const QString &l) override { - m_customLabel = l; - } - QString errorString() const override - { - if (m_errorString.dirty()) { - m_errorString = doErrorString(); - } - return m_errorString; - } - -private: - virtual QString doErrorString() const - { - if (const std::shared_ptr io = ioDevice()) { - return io->errorString(); - } else { - return i18n("No input device"); - } - } - -private: - QString m_customLabel; - QString m_defaultLabel; - mutable cached m_errorString; -}; - class PipeInput : public InputImplBase { public: diff --git a/src/utils/input_p.h b/src/utils/input_p.h new file mode 100644 --- /dev/null +++ b/src/utils/input_p.h @@ -0,0 +1,91 @@ +/* -*- mode: c++; c-basic-offset:4 -*- + utils/input_p.h + + This file is part of Kleopatra, the KDE keymanager + Copyright (c) 2007 Klarälvdalens Datakonsult AB + + Kleopatra is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + Kleopatra is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#ifndef __KLEOPATRA_UTILS_INPUT_P_H__ +#define __KLEOPATRA_UTILS_INPUT_P_H__ + +#include "input.h" +#include "cached.h" + +#include +#include +#include + +namespace Kleo { + +class InputImplBase : public Input +{ +public: + InputImplBase() : Input(), m_customLabel(), m_defaultLabel() {} + + QString label() const override + { + return m_customLabel.isEmpty() ? m_defaultLabel : m_customLabel; + } + + void setDefaultLabel(const QString &l) + { + m_defaultLabel = l; + } + + void setLabel(const QString &l) override + { + m_customLabel = l; + } + + QString errorString() const override + { + if (m_errorString.dirty()) { + m_errorString = doErrorString(); + } + return m_errorString; + } + +private: + virtual QString doErrorString() const + { + if (const std::shared_ptr io = ioDevice()) { + return io->errorString(); + } else { + return i18n("No input device"); + } + } + +private: + QString m_customLabel; + QString m_defaultLabel; + mutable cached m_errorString; +}; + +} + +#endif diff --git a/src/utils/output.cpp b/src/utils/output.cpp --- a/src/utils/output.cpp +++ b/src/utils/output.cpp @@ -33,7 +33,7 @@ #include #include "output.h" - +#include "input_p.h" #include "detail_p.h" #include "kleo_assert.h" #include "kdpipeiodevice.h" @@ -175,6 +175,39 @@ bool m_closed; }; +class FileOutput; +class OutputInput : public InputImplBase +{ +public: + OutputInput(const std::shared_ptr &output); + + unsigned int classification() const override + { + return 0; + } + + void outputFinalized() + { + if (!m_ioDevice->open(QIODevice::ReadOnly)) { + qCCritical(KLEOPATRA_LOG) << "Failed to open file for reading"; + } + } + + std::shared_ptr ioDevice() const override + { + return m_ioDevice; + } + + unsigned long long size() const override + { + return 0; + } + +private: + std::shared_ptr m_output; + mutable std::shared_ptr m_ioDevice = nullptr; +}; + class OutputImplBase : public Output { public: @@ -366,13 +399,24 @@ void doCancel() override { qCDebug(KLEOPATRA_LOG) << this; } + QString fileName() const + { + return m_fileName; + } + + void attachInput(const std::shared_ptr &input) + { + m_attachedInput = std::weak_ptr(input); + } + private: bool obtainOverwritePermission(); private: const QString m_fileName; std::shared_ptr< TemporaryFile > m_tmpFile; const std::shared_ptr m_policy; + std::weak_ptr m_attachedInput; }; #ifndef QT_NO_CLIPBOARD @@ -493,6 +537,10 @@ if (QFile::rename(tmpFileName, m_fileName)) { qCDebug(KLEOPATRA_LOG) << this << "succeeded"; + + if (!m_attachedInput.expired()) { + m_attachedInput.lock()->outputFinalized(); + } return; } @@ -512,6 +560,10 @@ if (QFile::rename(tmpFileName, m_fileName)) { qCDebug(KLEOPATRA_LOG) << this << "succeeded"; + + if (!m_attachedInput.expired()) { + m_attachedInput.lock()->outputFinalized(); + } return; } @@ -627,3 +679,23 @@ #endif // QT_NO_CLIPBOARD Output::~Output() {} + + +OutputInput::OutputInput(const std::shared_ptr &output) + : m_output(output) + , m_ioDevice(new QFile(output->fileName())) +{ +} + + + +std::shared_ptr Input::createFromOutput(const std::shared_ptr &output) +{ + if (auto fo = std::dynamic_pointer_cast(output)) { + auto input = std::shared_ptr(new OutputInput(fo)); + fo->attachInput(input); + return input; + } else { + return {}; + } +}