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.
This commit is contained in:
oop7
2025-11-12 20:25:48 +02:00
parent 37f75b7d75
commit b0c7616704
2 changed files with 96 additions and 9 deletions
+91 -7
View File
@@ -29,7 +29,9 @@ from src.utils.ytsage_constants import (
SUBPROCESS_CREATIONFLAGS, SUBPROCESS_CREATIONFLAGS,
YTDLP_APP_BIN_PATH, YTDLP_APP_BIN_PATH,
YTDLP_DOWNLOAD_URL, YTDLP_DOWNLOAD_URL,
YTDLP_SHA256_URL,
) )
from src.core.ytsage_ffmpeg import get_file_sha256
# YTDLP_URLS moved to src\utils\ytsage_constants.py # YTDLP_URLS moved to src\utils\ytsage_constants.py
# get_ytdlp_install_dir() 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 # 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): class DownloadYtdlpThread(QThread):
progress_signal = Signal(int) progress_signal = Signal(int)
finished_signal = Signal(bool, str) finished_signal = Signal(bool, str)
@@ -51,7 +113,9 @@ class DownloadYtdlpThread(QThread):
exe_path = YTDLP_APP_BIN_PATH exe_path = YTDLP_APP_BIN_PATH
# Download with progress reporting # Download with progress reporting
logger.info(f"Downloading yt-dlp from: {YTDLP_DOWNLOAD_URL}")
response = requests.get(YTDLP_DOWNLOAD_URL, stream=True) response = requests.get(YTDLP_DOWNLOAD_URL, stream=True)
response.raise_for_status()
total_size = int(response.headers.get("content-length", 0)) total_size = int(response.headers.get("content-length", 0))
block_size = 1024 # 1 Kibibyte block_size = 1024 # 1 Kibibyte
@@ -67,13 +131,29 @@ class DownloadYtdlpThread(QThread):
progress = int(downloaded / total_size * 100) progress = int(downloaded / total_size * 100)
self.progress_signal.emit(progress) 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 # Make executable on macOS and Linux
if OS_NAME != "Windows": if OS_NAME != "Windows":
os.chmod(exe_path, 0o755) 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: except Exception as e:
logger.exception(f"Error downloading yt-dlp: {e}")
self.finished_signal.emit(False, str(e)) self.finished_signal.emit(False, str(e))
@@ -568,26 +648,30 @@ def setup_ytdlp(parent_widget=None):
if result == QDialog.DialogCode.Accepted: if result == QDialog.DialogCode.Accepted:
# First check if we received a path from the signal # First check if we received a path from the signal
if setup_result["path"] and Path.exists(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']}") logger.debug(f"Using path from signal: {setup_result['path']}")
return setup_result["path"] return str(setup_result["path"])
# Get the expected path for verification as fallback # Get the expected path for verification as fallback
expected_path = YTDLP_APP_BIN_PATH expected_path = YTDLP_APP_BIN_PATH
logger.debug(f"Expected yt-dlp path: {expected_path}") logger.debug(f"Expected yt-dlp path: {expected_path}")
# Verify the path exists after dialog is accepted # 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}") logger.debug(f"yt-dlp successfully found at expected path: {expected_path}")
return expected_path return str(expected_path)
else: else:
logger.debug(f"Expected path does not exist, trying alternate detection") 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 # Try to use the get_yt_dlp_path function to find yt-dlp elsewhere
yt_dlp_path = get_yt_dlp_path() yt_dlp_path = get_yt_dlp_path()
logger.debug(f"Alternate detection result: {yt_dlp_path}") logger.debug(f"Alternate detection result: {yt_dlp_path}")
if yt_dlp_path != "yt-dlp" and Path.exists(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}") logger.debug(f"yt-dlp found at alternate location: {yt_dlp_path}")
return yt_dlp_path return str(yt_dlp_path)
# Something went wrong, show an error message # Something went wrong, show an error message
logger.debug(f"Setup failed, showing error dialog") logger.debug(f"Setup failed, showing error dialog")
+3
View File
@@ -145,6 +145,9 @@ else: # Linux and other UNIX-like
# Documentation URLs # Documentation URLs
YTDLP_DOCS_URL: str = "https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#usage-and-options" 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 download links (Essentials build - always latest version)
FFMPEG_7Z_DOWNLOAD_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z" 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" FFMPEG_ZIP_DOWNLOAD_URL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"