Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verify hash of Python installer #21877

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) const
{
// 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) const;
#endif

Ui::MainWindow *m_ui = nullptr;

QString m_windowTitle;
Expand Down
Loading