From b0c76167041f58a23c715767e2882b818d2cd1fc Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Wed, 12 Nov 2025 20:25:48 +0200 Subject: [PATCH] Add SHA256 verification for yt-dlp downloads Introduced a function to verify the SHA256 hash of the downloaded yt-dlp binary against official checksums, improving security and integrity checks. Updated the download thread to perform hash verification after download and handle failures appropriately. Added the SHA256 checksums URL to constants. --- src/core/ytsage_yt_dlp.py | 102 +++++++++++++++++++++++++++++++--- src/utils/ytsage_constants.py | 3 + 2 files changed, 96 insertions(+), 9 deletions(-) diff --git a/src/core/ytsage_yt_dlp.py b/src/core/ytsage_yt_dlp.py index e440c94..500515a 100644 --- a/src/core/ytsage_yt_dlp.py +++ b/src/core/ytsage_yt_dlp.py @@ -29,7 +29,9 @@ from src.utils.ytsage_constants import ( SUBPROCESS_CREATIONFLAGS, YTDLP_APP_BIN_PATH, YTDLP_DOWNLOAD_URL, + YTDLP_SHA256_URL, ) +from src.core.ytsage_ffmpeg import get_file_sha256 # YTDLP_URLS moved to src\utils\ytsage_constants.py # get_ytdlp_install_dir() moved to src\utils\ytsage_constants.py @@ -38,6 +40,66 @@ from src.utils.ytsage_constants import ( # ensure_install_dir_exists() moved to src\utils\ytsage_constants.py +def verify_ytdlp_sha256(file_path: Path, download_url: str) -> bool: + """ + Verify yt-dlp file SHA256 hash against official checksums. + + Args: + file_path: Path to the downloaded yt-dlp file + download_url: The URL used to download the file (to determine the filename) + + Returns: + bool: True if verification successful, False otherwise + """ + try: + # Download the SHA2-256SUMS file + logger.info(f"Downloading SHA256 checksums from: {YTDLP_SHA256_URL}") + response = requests.get(YTDLP_SHA256_URL, timeout=10) + response.raise_for_status() + checksum_content = response.text + + # Extract filename from download URL (e.g., yt-dlp.exe, yt-dlp_macos, yt-dlp) + filename = download_url.split("/")[-1] + logger.info(f"Looking for checksum for file: {filename}") + + # Parse the checksum file to find the matching hash + expected_hash = None + for line in checksum_content.strip().split("\n"): + if filename in line: + # Format: "hash filename" + parts = line.strip().split() + if len(parts) >= 2 and parts[1] == filename: + expected_hash = parts[0] + break + + if not expected_hash: + logger.error(f"Could not find SHA256 hash for {filename} in checksums file") + return False + + # Calculate actual hash of downloaded file + logger.info("Calculating SHA256 hash of downloaded file...") + actual_hash = get_file_sha256(file_path) + + # Compare hashes + if actual_hash.lower() == expected_hash.lower(): + logger.info("✓ SHA256 verification successful!") + logger.info(f" Expected: {expected_hash}") + logger.info(f" Actual: {actual_hash}") + return True + else: + logger.error("✗ SHA256 verification failed!") + logger.error(f" Expected: {expected_hash}") + logger.error(f" Actual: {actual_hash}") + return False + + except requests.RequestException as e: + logger.error(f"Failed to download SHA256 checksums: {e}") + return False + except Exception as e: + logger.exception(f"Error during SHA256 verification: {e}") + return False + + class DownloadYtdlpThread(QThread): progress_signal = Signal(int) finished_signal = Signal(bool, str) @@ -51,7 +113,9 @@ class DownloadYtdlpThread(QThread): exe_path = YTDLP_APP_BIN_PATH # Download with progress reporting + logger.info(f"Downloading yt-dlp from: {YTDLP_DOWNLOAD_URL}") response = requests.get(YTDLP_DOWNLOAD_URL, stream=True) + response.raise_for_status() total_size = int(response.headers.get("content-length", 0)) block_size = 1024 # 1 Kibibyte @@ -67,13 +131,29 @@ class DownloadYtdlpThread(QThread): progress = int(downloaded / total_size * 100) self.progress_signal.emit(progress) + logger.info("Download complete, verifying SHA256 hash...") + + # Verify SHA256 hash + if not verify_ytdlp_sha256(exe_path, YTDLP_DOWNLOAD_URL): + # Hash verification failed - delete the downloaded file + logger.error("SHA256 verification failed! Removing downloaded file.") + if Path(exe_path).exists(): + Path(exe_path).unlink() + self.finished_signal.emit( + False, + "SHA256 verification failed. The downloaded file may be corrupted or tampered with." + ) + return + # Make executable on macOS and Linux if OS_NAME != "Windows": os.chmod(exe_path, 0o755) - self.finished_signal.emit(True, exe_path) + logger.info("yt-dlp downloaded and verified successfully!") + self.finished_signal.emit(True, str(exe_path)) except Exception as e: + logger.exception(f"Error downloading yt-dlp: {e}") self.finished_signal.emit(False, str(e)) @@ -568,26 +648,30 @@ def setup_ytdlp(parent_widget=None): if result == QDialog.DialogCode.Accepted: # First check if we received a path from the signal - if setup_result["path"] and Path.exists(setup_result["path"]): - logger.debug(f"Using path from signal: {setup_result['path']}") - return setup_result["path"] + if setup_result["path"]: + path_obj = Path(setup_result["path"]) if isinstance(setup_result["path"], str) else setup_result["path"] + if path_obj.exists(): + logger.debug(f"Using path from signal: {setup_result['path']}") + return str(setup_result["path"]) # Get the expected path for verification as fallback expected_path = YTDLP_APP_BIN_PATH logger.debug(f"Expected yt-dlp path: {expected_path}") # Verify the path exists after dialog is accepted - if Path.exists(expected_path): + if expected_path.exists(): logger.debug(f"yt-dlp successfully found at expected path: {expected_path}") - return expected_path + return str(expected_path) else: logger.debug(f"Expected path does not exist, trying alternate detection") # Try to use the get_yt_dlp_path function to find yt-dlp elsewhere yt_dlp_path = get_yt_dlp_path() logger.debug(f"Alternate detection result: {yt_dlp_path}") - if yt_dlp_path != "yt-dlp" and Path.exists(yt_dlp_path): - logger.debug(f"yt-dlp found at alternate location: {yt_dlp_path}") - return yt_dlp_path + if yt_dlp_path != "yt-dlp": + path_obj = Path(yt_dlp_path) if isinstance(yt_dlp_path, str) else yt_dlp_path + if path_obj.exists(): + logger.debug(f"yt-dlp found at alternate location: {yt_dlp_path}") + return str(yt_dlp_path) # Something went wrong, show an error message logger.debug(f"Setup failed, showing error dialog") diff --git a/src/utils/ytsage_constants.py b/src/utils/ytsage_constants.py index 6334a28..5edc11f 100644 --- a/src/utils/ytsage_constants.py +++ b/src/utils/ytsage_constants.py @@ -145,6 +145,9 @@ else: # Linux and other UNIX-like # Documentation URLs YTDLP_DOCS_URL: str = "https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#usage-and-options" +# yt-dlp SHA256 checksums URL +YTDLP_SHA256_URL: str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/SHA2-256SUMS" + # FFmpeg download links (Essentials build - always latest version) FFMPEG_7Z_DOWNLOAD_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z" FFMPEG_ZIP_DOWNLOAD_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"