diff --git a/NEWS b/NEWS index 796e3c9e..c317d2f3 100644 --- a/NEWS +++ b/NEWS @@ -24,6 +24,8 @@ Noteworthy changes in version 1.24.0 (unrelease) * qt: Allow specifying import options when importing keys. [T7152] + * qt: Allow appending a detached signature to an existing file. [T6867] + * Interface changes relative to the 1.23.2 release: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GPGME_ENCRYPT_FILE NEW. @@ -67,6 +69,8 @@ Noteworthy changes in version 1.24.0 (unrelease) qt: SignJob::outputFile NEW. qt: SignJob::setSigningFlags NEW. qt: SignJob::signingFlags NEW. + qt: SignJob::setAppendSignature NEW. + qt: SignJob::appendSignatureEnabled NEW. qt: VerifyDetachedJob::setSignatureFile NEW. qt: VerifyDetachedJob::signatureFile NEW. qt: VerifyDetachedJob::setSignedFile NEW. diff --git a/lang/qt/src/qgpgmesignjob.cpp b/lang/qt/src/qgpgmesignjob.cpp index 76e60e72..7ad71767 100644 --- a/lang/qt/src/qgpgmesignjob.cpp +++ b/lang/qt/src/qgpgmesignjob.cpp @@ -170,7 +170,8 @@ static QGpgMESignJob::result_type sign_to_filename(Context *ctx, const std::vector &signers, const QString &inputFilePath, const QString &outputFilePath, - SignatureMode flags) + SignatureMode flags, + bool appendSignature) { Data indata; #ifdef Q_OS_WIN @@ -206,13 +207,42 @@ static QGpgMESignJob::result_type sign_to_filename(Context *ctx, flags = static_cast(flags | SignFile); const auto signingResult = ctx->sign(indata, outdata, flags); - if (!signingResult.error().code()) { - // the operation succeeded -> save the result under the requested file name - partFileGuard.commit(); - } - Error ae; const QString log = _detail::audit_log_as_html(ctx, ae); + + if (!signingResult.error().code()) { + // the operation succeeded + const bool appendSignatureToExistingFile = appendSignature && (flags & Detached) && QFile::exists(outputFilePath); + if (appendSignatureToExistingFile) { + // append the result to the existing file + QFile newSignatureFile{partFileGuard.tempFileName()}; + if (!newSignatureFile.open(QIODevice::ReadOnly)) { + qCDebug(QGPGME_LOG) << "Failed to open detached signature file" << newSignatureFile.fileName() << "(" << newSignatureFile.errorString() << ")"; + return std::make_tuple(SigningResult{Error::fromCode(GPG_ERR_GENERAL)}, QByteArray{}, log, ae); + } + const QByteArray newSigData = newSignatureFile.readAll(); + if (newSigData.isEmpty()) { + qCDebug(QGPGME_LOG) << "Failed to read detached signature from file" << newSignatureFile.fileName() << "(" << newSignatureFile.errorString() << ")"; + return std::make_tuple(SigningResult{Error::fromCode(GPG_ERR_GENERAL)}, QByteArray{}, log, ae); + } + newSignatureFile.close(); + + QFile existingSignatureFile{outputFilePath}; + if (!existingSignatureFile.open(QIODevice::WriteOnly | QIODevice::Append)) { + qCDebug(QGPGME_LOG) << "Failed to open existing detached signature file for appending" << existingSignatureFile.fileName() << "(" << existingSignatureFile.errorString() << ")"; + return std::make_tuple(SigningResult{Error::fromCode(GPG_ERR_GENERAL)}, QByteArray{}, log, ae); + } + const auto bytesWritten = existingSignatureFile.write(newSigData); + if (bytesWritten != newSigData.size()) { + qCDebug(QGPGME_LOG) << "Failed to write new signature to existing detached signature file" << existingSignatureFile.fileName() << "(" << existingSignatureFile.errorString() << ")"; + return std::make_tuple(SigningResult{Error::fromCode(GPG_ERR_GENERAL)}, QByteArray{}, log, ae); + } + } else { + // save the result under the requested file name + partFileGuard.commit(); + } + } + return std::make_tuple(signingResult, QByteArray{}, log, ae); } @@ -241,7 +271,7 @@ GpgME::Error QGpgMESignJobPrivate::startIt() } q->run([=](Context *ctx) { - return sign_to_filename(ctx, m_signers, m_inputFilePath, m_outputFilePath, m_signingFlags); + return sign_to_filename(ctx, m_signers, m_inputFilePath, m_outputFilePath, m_signingFlags, m_appendSignature); }); return {}; diff --git a/lang/qt/src/signjob.cpp b/lang/qt/src/signjob.cpp index 0a9c8651..be15051b 100644 --- a/lang/qt/src/signjob.cpp +++ b/lang/qt/src/signjob.cpp @@ -95,4 +95,16 @@ GpgME::SignatureMode SignJob::signingFlags() const return d->m_signingFlags; } +void SignJob::setAppendSignature(bool append) +{ + auto d = jobPrivate(this); + d->m_appendSignature = append; +} + +bool SignJob::appendSignatureEnabled() const +{ + auto d = jobPrivate(this); + return d->m_appendSignature; +} + #include "signjob.moc" diff --git a/lang/qt/src/signjob.h b/lang/qt/src/signjob.h index 273277b5..085f8498 100644 --- a/lang/qt/src/signjob.h +++ b/lang/qt/src/signjob.h @@ -113,7 +113,8 @@ public: * * \note If a file with this path exists, then the job will fail, i.e. you * need to delete an existing file that shall be overwritten before you - * start the job. + * start the job. If you create a detached signature then you can tell + * the job to append the new detached signature to an existing file. */ void setOutputFile(const QString &path); QString outputFile() const; @@ -129,6 +130,17 @@ public: void setSigningFlags(GpgME::SignatureMode flags); GpgME::SignatureMode signingFlags() const; + /** + * If @c true then a new detached signature is appended to an already + * existing detached signature. + * + * Defaults to \c false. + * + * Used if the job is started with startIt(). + */ + void setAppendSignature(bool append); + bool appendSignatureEnabled() const; + /** Starts the signing operation. \a signers is the list of keys to sign \a plainText with. Empty (null) keys are ignored. diff --git a/lang/qt/src/signjob_p.h b/lang/qt/src/signjob_p.h index 75309782..e1eae6b8 100644 --- a/lang/qt/src/signjob_p.h +++ b/lang/qt/src/signjob_p.h @@ -48,6 +48,7 @@ struct SignJobPrivate : public JobPrivate QString m_inputFilePath; QString m_outputFilePath; GpgME::SignatureMode m_signingFlags = GpgME::SignFile; + bool m_appendSignature = false; }; } diff --git a/lang/qt/tests/run-signjob.cpp b/lang/qt/tests/run-signjob.cpp index 14b0a406..c1342b66 100644 --- a/lang/qt/tests/run-signjob.cpp +++ b/lang/qt/tests/run-signjob.cpp @@ -56,7 +56,9 @@ std::ostream &operator<<(std::ostream &os, const QString &s) } struct CommandLineOptions { - bool armor; + GpgME::SignatureMode signingFlags = GpgME::NormalSignatureMode; + bool armor = false; + bool appendSignature = false; QString inputFile; QString outputFile; std::chrono::seconds cancelTimeout{0}; @@ -72,6 +74,8 @@ CommandLineOptions parseCommandLine(const QStringList &arguments) parser.addOptions({ {{"o", "output"}, "Write output to FILE.", "FILE"}, {{"a", "armor"}, "Create ASCII armored output."}, + {{"b", "detach-sign"}, "Create a detached signature."}, + {"append", "Append new (detached) signature to existing file."}, {"cancel-after", "Cancel the running job after SECONDS seconds.", "SECONDS"}, }); parser.addPositionalArgument("file", "File to sign", "FILE"); @@ -84,6 +88,10 @@ CommandLineOptions parseCommandLine(const QStringList &arguments) } options.armor = parser.isSet("armor"); + if (parser.isSet("detach-sign")) { + options.signingFlags = GpgME::Detached; + options.appendSignature = parser.isSet("append"); + } options.inputFile = args.front(); options.outputFile = parser.value("output"); if (parser.isSet("cancel-after")) { @@ -114,7 +122,7 @@ int main(int argc, char **argv) output.reset(new QFile); output->open(stdout, QIODevice::WriteOnly); } else { - if (QFile::exists(options.outputFile)) { + if (QFile::exists(options.outputFile) && !options.appendSignature) { qCritical() << "File" << options.outputFile << "exists. Bailing out."; return 1; } @@ -146,6 +154,8 @@ int main(int argc, char **argv) } else { job->setInputFile(options.inputFile); job->setOutputFile(options.outputFile); + job->setSigningFlags(options.signingFlags); + job->setAppendSignature(options.appendSignature); err = job->startIt(); } if (err) {