diff --git a/.github/workflows/release-binary.yml b/.github/workflows/release-binary.yml index c6b0963..b00af37 100644 --- a/.github/workflows/release-binary.yml +++ b/.github/workflows/release-binary.yml @@ -40,6 +40,6 @@ jobs: # Binary auto-downloads on first launch ``` - > Checksums and platform list will be added after binary uploads. + > Binary integrity is verified automatically via SHA-256 checksums on download. > > Release signed with CloakHQ GPG key: `C60C0DDC9D0DE2DD` diff --git a/README.md b/README.md index 025069c..2a60fb9 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Drop-in Playwright/Puppeteer replacement for Python and JavaScript. Same API, same code — just swap the import. Your browser now scores **0.9 on reCAPTCHA v3**, passes **Cloudflare Turnstile**, and clears **30 out of 30** stealth detection tests. -- 🔒 **22 source-level C++ patches** — not JS injection, not config flags +- 🔒 **26 source-level C++ patches** — not JS injection, not config flags - 🛡️ **CDP stealth built-in** — powered by [Patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright), hides Playwright's automation signals - 🎯 **0.9 reCAPTCHA v3 score** — human-level, server-verified - ☁️ **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — 30/30 tests diff --git a/cloakbrowser/download.py b/cloakbrowser/download.py index 5afc721..751c9ac 100644 --- a/cloakbrowser/download.py +++ b/cloakbrowser/download.py @@ -6,6 +6,7 @@ Similar to how Playwright downloads its own bundled Chromium. from __future__ import annotations +import hashlib import logging import os import platform @@ -23,6 +24,7 @@ from .config import ( CHROMIUM_VERSION, DOWNLOAD_BASE_URL, GITHUB_API_URL, + GITHUB_DOWNLOAD_BASE_URL, _version_newer, check_platform_available, get_binary_dir, @@ -107,6 +109,7 @@ def _download_and_extract(version: str | None = None) -> None: Tries the primary server (cloakbrowser.dev) first, falls back to GitHub Releases if the primary is unreachable or returns an error. + Verifies SHA-256 checksum before extraction when available. """ primary_url = get_download_url(version) fallback_url = get_fallback_download_url(version) @@ -133,6 +136,10 @@ def _download_and_extract(version: str | None = None) -> None: ) _download_file(fallback_url, tmp_path) + # Verify checksum before extraction + if os.environ.get("CLOAKBROWSER_SKIP_CHECKSUM", "").lower() != "true": + _verify_download_checksum(tmp_path, version) + _extract_archive(tmp_path, binary_dir, binary_path) logger.info("Visit https://cloakbrowser.dev for docs and release notifications.") logger.info("Issues? https://github.com/CloakHQ/CloakBrowser/issues") @@ -142,6 +149,76 @@ def _download_and_extract(version: str | None = None) -> None: tmp_path.unlink(missing_ok=True) +def _verify_download_checksum(file_path: Path, version: str | None = None) -> None: + """Fetch SHA256SUMS and verify the downloaded file. Warn if unavailable, fail on mismatch.""" + checksums = _fetch_checksums(version) + tarball_name = f"cloakbrowser-{get_platform_tag()}.tar.gz" + + if checksums is None: + logger.warning("SHA256SUMS not available for this release — skipping checksum verification") + return + + expected = checksums.get(tarball_name) + if expected is None: + logger.warning("SHA256SUMS found but no entry for %s — skipping verification", tarball_name) + return + + _verify_checksum(file_path, expected) + + +def _fetch_checksums(version: str | None = None) -> dict[str, str] | None: + """Fetch SHA256SUMS file for a version. Returns {filename: hash} or None.""" + v = version or CHROMIUM_VERSION + has_custom_url = os.environ.get("CLOAKBROWSER_DOWNLOAD_URL") + + # Build URL list — respect custom URL contract (no GitHub fallback) + urls = [f"{DOWNLOAD_BASE_URL}/chromium-v{v}/SHA256SUMS"] + if not has_custom_url: + urls.append(f"{GITHUB_DOWNLOAD_BASE_URL}/chromium-v{v}/SHA256SUMS") + + for url in urls: + try: + resp = httpx.get(url, follow_redirects=True, timeout=10.0) + resp.raise_for_status() + return _parse_checksums(resp.text) + except Exception: + continue + return None + + +def _parse_checksums(text: str) -> dict[str, str]: + """Parse SHA256SUMS format: 'hash filename' per line.""" + result = {} + for line in text.strip().splitlines(): + line = line.strip() + if not line: + continue + parts = line.split(None, 1) + if len(parts) == 2: + hash_val, filename = parts + filename = filename.lstrip("*") + result[filename] = hash_val.lower() + return result + + +def _verify_checksum(file_path: Path, expected_hash: str) -> None: + """Verify SHA-256 of a file. Raises RuntimeError on mismatch.""" + sha256 = hashlib.sha256() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + sha256.update(chunk) + actual = sha256.hexdigest().lower() + if actual != expected_hash: + raise RuntimeError( + f"Checksum verification failed!\n" + f" Expected: {expected_hash}\n" + f" Got: {actual}\n" + f" File may be corrupted or tampered with. " + f"Please retry or report at https://github.com/CloakHQ/cloakbrowser/issues" + ) + logger.info("Checksum verified: SHA-256 OK") + + def _download_file(url: str, dest: Path) -> None: """Download a file with progress logging.""" logger.info("Downloading from %s", url) diff --git a/js/README.md b/js/README.md index 4e6b926..91b3b47 100644 --- a/js/README.md +++ b/js/README.md @@ -11,7 +11,7 @@ Drop-in Playwright/Puppeteer replacement. Same API — just swap the import. Scores **0.9 on reCAPTCHA v3**, passes **Cloudflare Turnstile**, and clears **30/30** stealth detection tests. -- 🔒 **22 source-level C++ patches** — not JS injection, not config flags +- 🔒 **26 source-level C++ patches** — not JS injection, not config flags - 🎯 **0.9 reCAPTCHA v3 score** — human-level, server-verified - ☁️ **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — 30/30 tests - 🔄 **Drop-in replacement** — works with both Playwright and Puppeteer diff --git a/js/src/download.ts b/js/src/download.ts index d3aa560..f155969 100644 --- a/js/src/download.ts +++ b/js/src/download.ts @@ -5,6 +5,7 @@ */ import { execFileSync } from "node:child_process"; +import { createHash } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { pipeline } from "node:stream/promises"; @@ -14,7 +15,9 @@ import { extract as tarExtract } from "tar"; import type { BinaryInfo } from "./types.js"; import { CHROMIUM_VERSION, + DOWNLOAD_BASE_URL, GITHUB_API_URL, + GITHUB_DOWNLOAD_BASE_URL, checkPlatformAvailable, getBinaryDir, getBinaryPath, @@ -164,6 +167,11 @@ async function downloadAndExtract(version?: string): Promise { await downloadFile(fallbackUrl, tmpPath); } + // Verify checksum before extraction + if (process.env.CLOAKBROWSER_SKIP_CHECKSUM?.toLowerCase() !== "true") { + await verifyDownloadChecksum(tmpPath, version); + } + await extractArchive(tmpPath, binaryDir, binaryPath); console.log( `[cloakbrowser] Visit https://cloakbrowser.dev for docs and release notifications.` @@ -182,6 +190,81 @@ async function downloadAndExtract(version?: string): Promise { } } +async function verifyDownloadChecksum(filePath: string, version?: string): Promise { + const checksums = await fetchChecksums(version); + const tarballName = `cloakbrowser-${getPlatformTag()}.tar.gz`; + + if (!checksums) { + console.warn("[cloakbrowser] SHA256SUMS not available for this release — skipping checksum verification"); + return; + } + + const expected = checksums.get(tarballName); + if (!expected) { + console.warn(`[cloakbrowser] SHA256SUMS found but no entry for ${tarballName} — skipping verification`); + return; + } + + await verifyChecksum(filePath, expected); +} + +async function fetchChecksums(version?: string): Promise | null> { + const v = version || CHROMIUM_VERSION; + const hasCustomUrl = !!process.env.CLOAKBROWSER_DOWNLOAD_URL; + + // Respect custom URL contract — no GitHub fallback when custom URL is set + const urls = [`${DOWNLOAD_BASE_URL}/chromium-v${v}/SHA256SUMS`]; + if (!hasCustomUrl) { + urls.push(`${GITHUB_DOWNLOAD_BASE_URL}/chromium-v${v}/SHA256SUMS`); + } + + for (const url of urls) { + try { + const resp = await fetch(url, { + redirect: "follow", + signal: AbortSignal.timeout(10_000), + }); + if (!resp.ok) continue; + return parseChecksums(await resp.text()); + } catch { + continue; + } + } + return null; +} + +function parseChecksums(text: string): Map { + const result = new Map(); + for (const line of text.trim().split("\n")) { + const trimmed = line.trim(); + if (!trimmed) continue; + const match = trimmed.match(/^([a-f0-9]{64})\s+\*?(.+)$/); + if (match) { + result.set(match[2]!, match[1]!.toLowerCase()); + } + } + return result; +} + +async function verifyChecksum(filePath: string, expectedHash: string): Promise { + const hash = createHash("sha256"); + const stream = fs.createReadStream(filePath); + for await (const chunk of stream) { + hash.update(chunk); + } + const actual = hash.digest("hex").toLowerCase(); + if (actual !== expectedHash) { + throw new Error( + `Checksum verification failed!\n` + + ` Expected: ${expectedHash}\n` + + ` Got: ${actual}\n` + + ` File may be corrupted or tampered with. ` + + `Please retry or report at https://github.com/CloakHQ/cloakbrowser/issues` + ); + } + console.log("[cloakbrowser] Checksum verified: SHA-256 OK"); +} + async function downloadFile(url: string, dest: string): Promise { console.log(`[cloakbrowser] Downloading from ${url}`);