Skip to content

Commit

Permalink
Verify hash of Python installer
Browse files Browse the repository at this point in the history
  • Loading branch information
Chocobo1 committed Nov 19, 2024
1 parent 7f901a8 commit 3229c8a
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 13 deletions.
69 changes: 62 additions & 7 deletions src/gui/mainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@
#include <QString>
#include <QTimer>

#ifdef Q_OS_WIN
#include <QCryptographicHash>
#include <QScopeGuard>
#endif

#include "base/bittorrent/session.h"
#include "base/bittorrent/sessionstatus.h"
#include "base/global.h"
Expand Down Expand Up @@ -113,7 +118,11 @@ namespace

const std::chrono::seconds PREVENT_SUSPEND_INTERVAL {60};

#ifdef Q_OS_WIN
const QString PYTHON_INSTALLER_URL = u"https://www.python.org/ftp/python/3.13.0/python-3.13.0-amd64.exe"_s;
const QByteArray PYTHON_INSTALLER_MD5 = QByteArrayLiteral("f5e5d48ba86586d4bef67bcb3790d339");
const QByteArray PYTHON_INSTALLER_SHA3_512 = QByteArrayLiteral("28ed23b82451efa5ec87e5dd18d7dacb9bc4d0a3643047091e5a687439f7e03a1c6e60ec64ee1210a0acaf2e5012504ff342ff27e5db108db05407e62aeff2f1");
#endif
}

MainWindow::MainWindow(IGUIApplication *app, const WindowState initialState, const QString &titleSuffix)
Expand Down Expand Up @@ -1899,33 +1908,79 @@ void MainWindow::installPython()
, this, &MainWindow::pythonDownloadFinished);
}

bool MainWindow::verifyPythonInstaller(const Path &installerPath)
{
// Verify installer hash
// Python.org only provides MD5 hash but MD5 is already broken and doesn't guarantee file is not tampered.
// Therefore, MD5 is only included to prove that the hash is still the same with upstream and we rely on
// SHA3-512 for the main check.

QFile file {installerPath.data()};
if (!file.open(QIODevice::ReadOnly))
{
LogMsg((tr("Failed to open Python installer. File: \"%1\".").arg(installerPath.toString())), Log::WARNING);
return false;
}

QCryptographicHash md5Hash {QCryptographicHash::Md5};
md5Hash.addData(&file);
if (const QByteArray hashHex = md5Hash.result().toHex(); hashHex != PYTHON_INSTALLER_MD5)
{
LogMsg((tr("Failed MD5 hash check for Python installer. File: \"%1\". Result hash: \"%2\". Expected hash: \"%3\".")
.arg(installerPath.toString(), QString::fromLatin1(hashHex), QString::fromLatin1(PYTHON_INSTALLER_MD5)))
, Log::WARNING);
return false;
}

file.seek(0);

QCryptographicHash sha3Hash {QCryptographicHash::Sha3_512};
sha3Hash.addData(&file);
if (const QByteArray hashHex = sha3Hash.result().toHex(); hashHex != PYTHON_INSTALLER_SHA3_512)
{
LogMsg((tr("Failed SHA3-512 hash check for Python installer. File: \"%1\". Result hash: \"%2\". Expected hash: \"%3\".")
.arg(installerPath.toString(), QString::fromLatin1(hashHex), QString::fromLatin1(PYTHON_INSTALLER_SHA3_512)))
, Log::WARNING);
return false;
}

return true;
}

void MainWindow::pythonDownloadFinished(const Net::DownloadResult &result)
{
const auto restoreWidgetsState = [this]
auto restoreWidgetsGuard = qScopeGuard([this]
{
m_ui->actionSearchWidget->setEnabled(true);
m_ui->actionSearchWidget->setToolTip({});
setCursor(Qt::ArrowCursor);
};
});

if (result.status != Net::DownloadStatus::Success)
{
restoreWidgetsState();
QMessageBox::warning(
this, tr("Download error")
, tr("Python setup could not be downloaded, reason: %1.\nPlease install it manually.")
, tr("Python installer could not be downloaded. Error: %1.\nPlease install it manually.")
.arg(result.errorString));
return;
}

const Path exePath = result.filePath + u".exe";
Utils::Fs::renameFile(result.filePath, exePath);
if (!Utils::Fs::renameFile(result.filePath, exePath))
{
LogMsg(tr("Rename Python installer failed. Source: \"%1\". Destination: \"%2\".")
.arg(result.filePath.toString(), exePath.toString())
, Log::WARNING);
return;
}

if (!verifyPythonInstaller(exePath))
return;

// launch installer
auto *installer = new QProcess(this);
installer->connect(installer, &QProcess::finished, this, [this, exePath, installer, restoreWidgetsState](const int exitCode, const QProcess::ExitStatus exitStatus)
installer->connect(installer, &QProcess::finished, this, [this, exePath, installer, restoreWidgetsGuard = std::move(restoreWidgetsGuard)](const int exitCode, const QProcess::ExitStatus exitStatus)
{
restoreWidgetsState();
installer->deleteLater();

if ((exitStatus == QProcess::NormalExit) && (exitCode == 0))
Expand Down
14 changes: 8 additions & 6 deletions src/gui/mainwindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,6 @@ private slots:
void optionsSaved();
void toggleAlternativeSpeeds();

#ifdef Q_OS_WIN
void pythonDownloadFinished(const Net::DownloadResult &result);
#endif
void addToolbarContextMenu();
void manageCookies();

Expand Down Expand Up @@ -184,12 +181,12 @@ private slots:
#else
void toggleVisibility();
#endif
#ifdef Q_OS_WIN
void pythonDownloadFinished(const Net::DownloadResult &result);
#endif

private:
void populateDesktopIntegrationMenu();
#ifdef Q_OS_WIN
void installPython();
#endif

void closeEvent(QCloseEvent *) override;
void showEvent(QShowEvent *) override;
Expand All @@ -204,6 +201,11 @@ private slots:
void refreshWindowTitle();
void refreshTrayIconTooltip();

#ifdef Q_OS_WIN
void installPython();
bool verifyPythonInstaller(const Path &installerPath);
#endif

Ui::MainWindow *m_ui = nullptr;

QString m_windowTitle;
Expand Down

0 comments on commit 3229c8a

Please sign in to comment.