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
This commit is contained in:
Ingo Klöcker 2023-10-27 16:07:16 +02:00
parent 8d8985bda1
commit 46f5d5eeb3
No known key found for this signature in database
GPG Key ID: F5A5D1692277A1E9
5 changed files with 160 additions and 15 deletions

View File

@ -133,18 +133,23 @@ static QGpgMEEncryptArchiveJob::result_type encrypt_to_filename(Context *ctx,
Context::EncryptionFlags flags, Context::EncryptionFlags flags,
const QString &baseDirectory) const QString &baseDirectory)
{ {
PartialFileGuard partFileGuard{outputFileName};
if (partFileGuard.tempFileName().isEmpty()) {
return std::make_tuple(EncryptionResult{Error::fromCode(GPG_ERR_EEXIST)}, QString{}, Error{});
}
Data outdata; Data outdata;
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
outdata.setFileName(outputFileName.toUtf8().constData()); outdata.setFileName(partFileGuard.tempFileName().toUtf8().constData());
#else #else
outdata.setFileName(QFile::encodeName(outputFileName).constData()); outdata.setFileName(QFile::encodeName(partFileGuard.tempFileName()).constData());
#endif #endif
const auto result = encrypt(ctx, recipients, paths, outdata, flags, baseDirectory); const auto result = encrypt(ctx, recipients, paths, outdata, flags, baseDirectory);
const auto &encryptionResult = std::get<0>(result); const auto &encryptionResult = std::get<0>(result);
if (encryptionResult.error().code()) { if (!encryptionResult.error().code()) {
// ensure that the output file is removed if the operation was canceled or failed // the operation succeeded -> save the result under the requested file name
removeFile(outputFileName); partFileGuard.commit();
} }
return result; return result;

View File

@ -138,18 +138,23 @@ static QGpgMESignArchiveJob::result_type sign_to_filename(Context *ctx,
const QString &outputFileName, const QString &outputFileName,
const QString &baseDirectory) const QString &baseDirectory)
{ {
PartialFileGuard partFileGuard{outputFileName};
if (partFileGuard.tempFileName().isEmpty()) {
return std::make_tuple(SigningResult{Error::fromCode(GPG_ERR_EEXIST)}, QString{}, Error{});
}
Data outdata; Data outdata;
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
outdata.setFileName(outputFileName.toUtf8().constData()); outdata.setFileName(partFileGuard.tempFileName().toUtf8().constData());
#else #else
outdata.setFileName(QFile::encodeName(outputFileName).constData()); outdata.setFileName(QFile::encodeName(partFileGuard.tempFileName()).constData());
#endif #endif
const auto result = sign(ctx, signers, paths, outdata, baseDirectory); const auto result = sign(ctx, signers, paths, outdata, baseDirectory);
const auto &signingResult = std::get<0>(result); const auto &signingResult = std::get<0>(result);
if (signingResult.error().code()) { if (!signingResult.error().code()) {
// ensure that the output file is removed if the operation was canceled or failed // the operation succeeded -> save the result under the requested file name
removeFile(outputFileName); partFileGuard.commit();
} }
return result; return result;

View File

@ -147,19 +147,27 @@ static QGpgMESignEncryptArchiveJob::result_type sign_encrypt_to_filename(Context
Context::EncryptionFlags encryptionFlags, Context::EncryptionFlags encryptionFlags,
const QString &baseDirectory) 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; Data outdata;
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
outdata.setFileName(outputFileName.toUtf8().constData()); outdata.setFileName(partFileGuard.tempFileName().toUtf8().constData());
#else #else
outdata.setFileName(QFile::encodeName(outputFileName).constData()); outdata.setFileName(QFile::encodeName(partFileGuard.tempFileName()).constData());
#endif #endif
const auto result = sign_encrypt(ctx, signers, recipients, paths, outdata, encryptionFlags, baseDirectory); const auto result = sign_encrypt(ctx, signers, recipients, paths, outdata, encryptionFlags, baseDirectory);
const auto &signingResult = std::get<0>(result); const auto &signingResult = std::get<0>(result);
const auto &encryptionResult = std::get<1>(result); const auto &encryptionResult = std::get<1>(result);
if (signingResult.error().code() || encryptionResult.error().code()) { if (!signingResult.error().code() && !encryptionResult.error().code()) {
// ensure that the output file is removed if the operation was canceled or failed // the operation succeeded -> save the result under the requested file name
removeFile(outputFileName); partFileGuard.commit();
} }
return result; return result;

View File

@ -40,6 +40,8 @@
#include "qgpgme_debug.h" #include "qgpgme_debug.h"
#include <QFile> #include <QFile>
#include <QFileInfo>
#include <QRandomGenerator>
#include <key.h> #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;
}

View File

@ -57,4 +57,26 @@ QStringList toFingerprints(const std::vector<GpgME::Key> &keys);
void removeFile(const QString &fileName); 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__ #endif // __QGPGME_UTIL_H__