diff --git a/Cargo.lock b/Cargo.lock index ebd7313..d897212 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,6 +131,15 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bstr" version = "1.12.0" @@ -263,6 +272,15 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -294,12 +312,32 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dirs" version = "6.0.0" @@ -503,6 +541,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -1450,6 +1498,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1541,6 +1600,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "sha2", "tar", "tempfile", "tokio", @@ -1791,6 +1851,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -1844,6 +1910,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wait-timeout" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index f51f1d0..0a07971 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ indicatif = "0.18" lazy_static = "1.5.0" regex = "1.11.1" md5 = "0.8" +sha2 = "0.10" reqwest = { version = "0.12.22", default-features = false, features = ["blocking", "json", "stream", "rustls-tls"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.140" diff --git a/src/handlers/download.rs b/src/handlers/download.rs index ef65740..9fce13f 100644 --- a/src/handlers/download.rs +++ b/src/handlers/download.rs @@ -11,6 +11,7 @@ use anyhow::{anyhow, bail, Error}; use futures_util::StreamExt; use indicatif::{HumanBytes, ProgressBar, ProgressStyle}; use md5::Context; +use sha2::{Digest, Sha256}; use reqwest::{ header::{HeaderMap, HeaderValue, USER_AGENT}, Client, @@ -21,6 +22,67 @@ use std::{cmp::min, io::Write, path::PathBuf, time::Instant}; use tracing::debug; +/// Verifies file integrity using available checksum files +/// Prioritizes SHA256 over MD5 for better security +fn verify_file_integrity(file_path: &PathBuf, name: &str) -> Result { + let sha256_path = file_path.with_extension("sha256"); + let md5_path = file_path.with_extension("md5"); + + // Prefer SHA256 if available + if sha256_path.exists() { + let mut file = File::open(file_path)?; + let mut hasher = Sha256::new(); + let mut buffer = [0u8; 8192]; + loop { + let n = file.read(&mut buffer)?; + if n == 0 { + break; + } + hasher.update(&buffer[..n]); + } + let result = hasher.finalize(); + let local_sha256 = format!("{:x}", result); + let expected_sha256 = std::fs::read_to_string(sha256_path)?.trim().to_string(); + + if local_sha256 == expected_sha256 { + println!("SHA256 check passed for {name}"); + return Ok(true); + } else { + println!("SHA256 mismatch for {name}: expected {}, got {}", expected_sha256, local_sha256); + return Ok(false); + } + } + + // Fall back to MD5 if SHA256 not available + if md5_path.exists() { + let mut file = File::open(file_path)?; + let mut hasher = Context::new(); + let mut buffer = [0u8; 8192]; + loop { + let n = file.read(&mut buffer)?; + if n == 0 { + break; + } + hasher.consume(&buffer[..n]); + } + let result = hasher.finalize(); + let local_md5 = format!("{:x}", result); + let expected_md5 = std::fs::read_to_string(md5_path)?.trim().to_string(); + + if local_md5 == expected_md5 { + println!("MD5 check passed for {name}"); + return Ok(true); + } else { + println!("MD5 mismatch for {name}: expected {}, got {}", expected_md5, local_md5); + return Ok(false); + } + } + + // No checksum files available + println!("No checksum files (.sha256 or .md5) found for {name}"); + Ok(true) // Allow download without verification if no checksums available +} + /// Generate helpful error message with network suggestions /// Note: This is only applicable for sui and walrus. MVR binary is standalone, not tied to a network. fn generate_network_suggestions_error( @@ -237,31 +299,18 @@ pub async fn download_file( if download_to.exists() { if download_to.metadata()?.len() == total_size { - // Check md5 if .md5 file exists - let md5_path = download_to.with_extension("md5"); - if md5_path.exists() { - let mut file = File::open(download_to)?; - let mut hasher = Context::new(); - let mut buffer = [0u8; 8192]; - loop { - let n = file.read(&mut buffer)?; - if n == 0 { - break; - } - hasher.consume(&buffer[..n]); - } - let result = hasher.finalize(); - let local_md5 = format!("{:x}", result); - let expected_md5 = std::fs::read_to_string(md5_path)?.trim().to_string(); - if local_md5 == expected_md5 { - println!("Found {name} in cache, md5 verified"); + // Check integrity using available checksum files (SHA256 preferred, MD5 fallback) + match verify_file_integrity(download_to, name) { + Ok(true) => { + println!("Found {name} in cache, checksum verified"); return Ok(name.to_string()); - } else { - println!("MD5 mismatch for {name}, re-downloading..."); } - } else { - println!("Found {name} in cache (no md5 to check)"); - return Ok(name.to_string()); + Ok(false) => { + println!("Checksum mismatch for {name}, re-downloading..."); + } + Err(e) => { + println!("Error verifying {name}: {}, re-downloading...", e); + } } } std::fs::remove_file(download_to)?; @@ -294,29 +343,16 @@ pub async fn download_file( pb.finish_with_message("Download complete"); - // After download, check md5 if .md5 file exists - let md5_path = download_to.with_extension("md5"); - if md5_path.exists() { - let mut file = File::open(download_to)?; - let mut hasher = Context::new(); - let mut buffer = [0u8; 8192]; - loop { - let n = file.read(&mut buffer)?; - if n == 0 { - break; - } - hasher.consume(&buffer[..n]); + // After download, verify integrity using available checksum files + match verify_file_integrity(download_to, name) { + Ok(true) => { + // Checksum verification passed or no checksums available } - let result = hasher.finalize(); - let local_md5 = format!("{:x}", result); - let expected_md5 = std::fs::read_to_string(md5_path)?.trim().to_string(); - if local_md5 != expected_md5 { - return Err(anyhow!(format!( - "MD5 check failed for {}: expected {}, got {}", - name, expected_md5, local_md5 - ))); - } else { - println!("MD5 check passed for {name}"); + Ok(false) => { + return Err(anyhow!("Checksum verification failed for {}", name)); + } + Err(e) => { + return Err(anyhow!("Error during checksum verification for {}: {}", name, e)); } }