From 46f5d5eeb3b1d0586106b33cecf600ab66170b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingo=20Kl=C3=B6cker?= Date: Fri, 27 Oct 2023 16:07:16 +0200 Subject: [PATCH] qt: Use temporary .part file names when creating archives * lang/qt/src/util.h, lang/qt/src/util.cpp (class PartialFileGuard): New. * lang/qt/src/util.cpp (getRandomCharacters, createPartFileName): New. * lang/qt/src/qgpgmeencryptarchivejob.cpp (encrypt_to_filename): Use PartialFileGuard. * lang/qt/src/qgpgmesignarchivejob.cpp (sign_to_filename): Ditto. * lang/qt/src/qgpgmesignencryptarchivejob.cpp (sign_encrypt_to_filename): Ditto. -- When creating signed and/or encrypted archives, gpgtar now writes the result to a temporary file name. On success, the archive is renamed to the final file name. Otherwise, the (partially written) temporary file is removed (if possible). GnuPG-bug-id: 6721 --- lang/qt/src/qgpgmeencryptarchivejob.cpp | 15 ++- lang/qt/src/qgpgmesignarchivejob.cpp | 15 ++- lang/qt/src/qgpgmesignencryptarchivejob.cpp | 18 +++- lang/qt/src/util.cpp | 105 ++++++++++++++++++++ lang/qt/src/util.h | 22 ++++ 5 files changed, 160 insertions(+), 15 deletions(-) diff --git a/lang/qt/src/qgpgmeencryptarchivejob.cpp b/lang/qt/src/qgpgmeencryptarchivejob.cpp index b8bb4c42..b3586d80 100644 --- a/lang/qt/src/qgpgmeencryptarchivejob.cpp +++ b/lang/qt/src/qgpgmeencryptarchivejob.cpp @@ -133,18 +133,23 @@ static QGpgMEEncryptArchiveJob::result_type encrypt_to_filename(Context *ctx, Context::EncryptionFlags flags, const QString &baseDirectory) { + PartialFileGuard partFileGuard{outputFileName}; + if (partFileGuard.tempFileName().isEmpty()) { + return std::make_tuple(EncryptionResult{Error::fromCode(GPG_ERR_EEXIST)}, QString{}, Error{}); + } + Data outdata; #ifdef Q_OS_WIN - outdata.setFileName(outputFileName.toUtf8().constData()); + outdata.setFileName(partFileGuard.tempFileName().toUtf8().constData()); #else - outdata.setFileName(QFile::encodeName(outputFileName).constData()); + outdata.setFileName(QFile::encodeName(partFileGuard.tempFileName()).constData()); #endif const auto result = encrypt(ctx, recipients, paths, outdata, flags, baseDirectory); const auto &encryptionResult = std::get<0>(result); - if (encryptionResult.error().code()) { - // ensure that the output file is removed if the operation was canceled or failed - removeFile(outputFileName); + if (!encryptionResult.error().code()) { + // the operation succeeded -> save the result under the requested file name + partFileGuard.commit(); } return result; diff --git a/lang/qt/src/qgpgmesignarchivejob.cpp b/lang/qt/src/qgpgmesignarchivejob.cpp index fc36d886..4a0e1f7f 100644 --- a/lang/qt/src/qgpgmesignarchivejob.cpp +++ b/lang/qt/src/qgpgmesignarchivejob.cpp @@ -138,18 +138,23 @@ static QGpgMESignArchiveJob::result_type sign_to_filename(Context *ctx, const QString &outputFileName, const QString &baseDirectory) { + PartialFileGuard partFileGuard{outputFileName}; + if (partFileGuard.tempFileName().isEmpty()) { + return std::make_tuple(SigningResult{Error::fromCode(GPG_ERR_EEXIST)}, QString{}, Error{}); + } + Data outdata; #ifdef Q_OS_WIN - outdata.setFileName(outputFileName.toUtf8().constData()); + outdata.setFileName(partFileGuard.tempFileName().toUtf8().constData()); #else - outdata.setFileName(QFile::encodeName(outputFileName).constData()); + outdata.setFileName(QFile::encodeName(partFileGuard.tempFileName()).constData()); #endif const auto result = sign(ctx, signers, paths, outdata, baseDirectory); const auto &signingResult = std::get<0>(result); - if (signingResult.error().code()) { - // ensure that the output file is removed if the operation was canceled or failed - removeFile(outputFileName); + if (!signingResult.error().code()) { + // the operation succeeded -> save the result under the requested file name + partFileGuard.commit(); } return result; diff --git a/lang/qt/src/qgpgmesignencryptarchivejob.cpp b/lang/qt/src/qgpgmesignencryptarchivejob.cpp index 3403ad57..c156bcba 100644 --- a/lang/qt/src/qgpgmesignencryptarchivejob.cpp +++ b/lang/qt/src/qgpgmesignencryptarchivejob.cpp @@ -147,19 +147,27 @@ static QGpgMESignEncryptArchiveJob::result_type sign_encrypt_to_filename(Context Context::EncryptionFlags encryptionFlags, const QString &baseDirectory) { + PartialFileGuard partFileGuard{outputFileName}; + if (partFileGuard.tempFileName().isEmpty()) { + return std::make_tuple(SigningResult{Error::fromCode(GPG_ERR_EEXIST)}, + EncryptionResult{Error::fromCode(GPG_ERR_EEXIST)}, + QString{}, + Error{}); + } + Data outdata; #ifdef Q_OS_WIN - outdata.setFileName(outputFileName.toUtf8().constData()); + outdata.setFileName(partFileGuard.tempFileName().toUtf8().constData()); #else - outdata.setFileName(QFile::encodeName(outputFileName).constData()); + outdata.setFileName(QFile::encodeName(partFileGuard.tempFileName()).constData()); #endif const auto result = sign_encrypt(ctx, signers, recipients, paths, outdata, encryptionFlags, baseDirectory); const auto &signingResult = std::get<0>(result); const auto &encryptionResult = std::get<1>(result); - if (signingResult.error().code() || encryptionResult.error().code()) { - // ensure that the output file is removed if the operation was canceled or failed - removeFile(outputFileName); + if (!signingResult.error().code() && !encryptionResult.error().code()) { + // the operation succeeded -> save the result under the requested file name + partFileGuard.commit(); } return result; diff --git a/lang/qt/src/util.cpp b/lang/qt/src/util.cpp index a9a70290..297c76a6 100644 --- a/lang/qt/src/util.cpp +++ b/lang/qt/src/util.cpp @@ -40,6 +40,8 @@ #include "qgpgme_debug.h" #include +#include +#include #include @@ -76,3 +78,106 @@ void removeFile(const QString &fileName) } } } + +/** + * Generates a string of random characters for the file names of temporary files. + * Never use this for generating passwords or similar use cases requiring highly + * secure random data. + */ +static QString getRandomCharacters(const int count) +{ + if (count < 0) { + return {}; + } + + QString randomChars; + randomChars.reserve(count); + + do { + // get a 32-bit random number to generate up to 5 random characters from + // the set {A-Z, a-z, 0-9}; set the highest bit for the break condition + for (quint32 rnd = QRandomGenerator::global()->generate() | (1 << 31); rnd > 3; rnd = rnd >> 6) + { + // take the last 6 bits; ignore 62 and 63 + const char ch = rnd & ((1 << 6) - 1); + if (ch < 26) { + randomChars += QLatin1Char(ch + 'A'); + } else if (ch < 26 + 26) { + randomChars += QLatin1Char(ch - 26 + 'a'); + } else if (ch < 26 + 26 + 10) { + randomChars += QLatin1Char(ch - 26 - 26 + '0'); + } + if (randomChars.size() >= count) { + break; + } + } + } while (randomChars.size() < count); + + return randomChars; +} + +/** + * Creates a temporary file name with extension \c .part for the given file name + * \a fileName. The function makes sure that the created file name is not in use + * at the time the file name is chosen. + * + * Example: For the file name "this.is.an.archive.tar.gpg" the temporary file name + * "this.YHgf2tEl.is.an.archive.tar.gpg.part" could be returned. + */ +static QString createPartFileName(const QString &fileName) +{ + static const int maxAttempts = 10; + + const QFileInfo fi{fileName}; + const QString path = fi.path(); // path without trailing '/' + const QString baseName = fi.baseName(); + const QString suffix = fi.completeSuffix(); + for (int attempt = 0; attempt < maxAttempts; ++attempt) { + const QString candidate = (path + QLatin1Char('/') + + baseName + QLatin1Char('.') + + getRandomCharacters(8) + QLatin1Char('.') + + suffix + + QLatin1String(".part")); + if (!QFile::exists(candidate)) { + return candidate; + } + } + + qCWarning(QGPGME_LOG) << __func__ << "- Failed to create temporary file name for" << fileName; + return {}; +} + +PartialFileGuard::PartialFileGuard(const QString &fileName) + : mFileName{fileName} + , mTempFileName{createPartFileName(fileName)} +{ + qCDebug(QGPGME_LOG) << __func__ << "- Using temporary file name" << mTempFileName; +} + +PartialFileGuard::~PartialFileGuard() +{ + if (!mTempFileName.isEmpty()) { + removeFile(mTempFileName); + } +} + +QString PartialFileGuard::tempFileName() const +{ + return mTempFileName; +} + +bool PartialFileGuard::commit() +{ + if (mTempFileName.isEmpty()) { + qCWarning(QGPGME_LOG) << "PartialFileGuard::commit: Called more than once"; + return false; + } + const bool success = QFile::rename(mTempFileName, mFileName); + if (success) { + qCDebug(QGPGME_LOG) << __func__ << "- Renamed" << mTempFileName << "to" << mFileName; + mTempFileName.clear(); + } else { + qCDebug(QGPGME_LOG) << __func__ << "- Renaming" << mTempFileName << "to" << mFileName << "failed"; + } + return success; +} diff --git a/lang/qt/src/util.h b/lang/qt/src/util.h index 9128e4bf..c2d63405 100644 --- a/lang/qt/src/util.h +++ b/lang/qt/src/util.h @@ -57,4 +57,26 @@ QStringList toFingerprints(const std::vector &keys); void removeFile(const QString &fileName); +/** + * Helper for using a temporary "part" file for writing a result to, similar + * to what browsers do when downloading files. + * On success, you commit() which renames the temporary file to the + * final file name. Otherwise, you do nothing and let the helper remove the + * temporary file on destruction. + */ +class PartialFileGuard +{ +public: + explicit PartialFileGuard(const QString &fileName); + ~PartialFileGuard(); + + QString tempFileName() const; + + bool commit(); + +private: + QString mFileName; + QString mTempFileName; +}; + #endif // __QGPGME_UTIL_H__