diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index 5c690e47694..14e02ac72f2 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -56,6 +56,11 @@ #include #include +#ifdef Q_OS_WIN +#include +#include +#endif + #include "base/bittorrent/session.h" #include "base/bittorrent/sessionstatus.h" #include "base/global.h" @@ -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) @@ -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)) diff --git a/src/gui/mainwindow.h b/src/gui/mainwindow.h index 68b7f349a32..99b0989a555 100644 --- a/src/gui/mainwindow.h +++ b/src/gui/mainwindow.h @@ -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(); @@ -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; @@ -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;