aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIngo Klöcker <[email protected]>2023-10-27 14:07:16 +0000
committerIngo Klöcker <[email protected]>2023-10-27 14:07:34 +0000
commit46f5d5eeb3b1d0586106b33cecf600ab66170b45 (patch)
tree58b4e222157b0770eb0603174906f55fba5bf525
parentqt: Refactor removal of output file on cancel or error (diff)
downloadgpgme-46f5d5eeb3b1d0586106b33cecf600ab66170b45.tar.gz
gpgme-46f5d5eeb3b1d0586106b33cecf600ab66170b45.zip
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
-rw-r--r--lang/qt/src/qgpgmeencryptarchivejob.cpp15
-rw-r--r--lang/qt/src/qgpgmesignarchivejob.cpp15
-rw-r--r--lang/qt/src/qgpgmesignencryptarchivejob.cpp18
-rw-r--r--lang/qt/src/util.cpp105
-rw-r--r--lang/qt/src/util.h22
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 <QFile>
+#include <QFileInfo>
+#include <QRandomGenerator>
#include <key.h>
@@ -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<GpgME::Key> &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__