diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ecfe428..36c9e88 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,3 +34,8 @@ jobs: with: command: test args: --workspace + - name: Run clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --workspace diff --git a/Cargo.lock b/Cargo.lock index 25e5fde..91ac5c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -762,7 +762,10 @@ version = "0.1.0" dependencies = [ "anyhow", "ct-codecs", + "digest", "hard-xml", + "sha1", + "sha2", "url", "uuid", ] @@ -1142,11 +1145,22 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", diff --git a/examples/download_test.rs b/examples/download_test.rs index ba2f34b..367808a 100644 --- a/examples/download_test.rs +++ b/examples/download_test.rs @@ -13,10 +13,10 @@ fn main() -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let path = tempdir.path().join("tmpfile"); - let res = download_and_hash(&client, url, &path, false)?; + let res = download_and_hash(&client, url, &path, None, None, false)?; tempdir.close()?; - println!("hash: {}", res.hash); + println!("hash: {}", res.hash_sha256); Ok(()) } diff --git a/examples/full_test.rs b/examples/full_test.rs index 2992a6a..319ae4a 100644 --- a/examples/full_test.rs +++ b/examples/full_test.rs @@ -80,12 +80,12 @@ fn main() -> Result<(), Box> { let tempdir = tempfile::tempdir()?; let path = tempdir.path().join("tmpfile"); - let res = ue_rs::download_and_hash(&client, url.clone(), &path, false).context(format!("download_and_hash({url:?}) failed"))?; + let res = ue_rs::download_and_hash(&client, url.clone(), &path, Some(expected_sha256.clone()), None, false).context(format!("download_and_hash({url:?}) failed"))?; tempdir.close()?; println!("\texpected sha256: {}", expected_sha256); - println!("\tcalculated sha256: {}", res.hash); - println!("\tsha256 match? {}", expected_sha256 == res.hash); + println!("\tcalculated sha256: {}", res.hash_sha256); + println!("\tsha256 match? {}", expected_sha256 == res.hash_sha256); } Ok(()) diff --git a/omaha/Cargo.toml b/omaha/Cargo.toml index 16d7316..ef24690 100644 --- a/omaha/Cargo.toml +++ b/omaha/Cargo.toml @@ -10,6 +10,9 @@ uuid = "1.2" ct-codecs = "1" url = "2" anyhow = "1.0.75" +sha2 = "0.10.8" +sha1 = "0.10.6" +digest = "0.10.7" [dependencies.hard-xml] path = "../vendor/hard-xml" diff --git a/omaha/src/hash_types.rs b/omaha/src/hash_types.rs index 20683b6..05026c3 100644 --- a/omaha/src/hash_types.rs +++ b/omaha/src/hash_types.rs @@ -1,6 +1,8 @@ use std::fmt; use std::str; +use sha2::Digest; + use anyhow::{Error as CodecError, anyhow}; #[rustfmt::skip] @@ -22,24 +24,63 @@ pub trait HashAlgo { const HASH_NAME: &'static str; type Output: AsRef<[u8]> + AsMut<[u8]> + Default + Sized + Eq; + + fn hasher() -> impl digest::DynDigest; + fn from_boxed(s: Box<[u8]>) -> Self::Output; } impl HashAlgo for Sha1 { const HASH_NAME: &'static str = "Sha1"; type Output = [u8; 20]; + + fn hasher() -> impl digest::DynDigest { + sha1::Sha1::new() + } + + fn from_boxed(s: Box<[u8]>) -> Self::Output { + let mut v = s.into_vec(); + v.resize(Self::Output::default().len(), 0); + let boxed_array: Box = match v.into_boxed_slice().try_into() { + Ok(a) => a, + Err(e) => { + println!("Unexpected length {}", e.len()); + #[allow(clippy::box_default)] + Box::new(Self::Output::default()) + } + }; + *boxed_array + } } impl HashAlgo for Sha256 { const HASH_NAME: &'static str = "Sha256"; type Output = [u8; 32]; + + fn hasher() -> impl digest::DynDigest { + sha2::Sha256::new() + } + + fn from_boxed(s: Box<[u8]>) -> Self::Output { + let mut v = s.into_vec(); + v.resize(Self::Output::default().len(), 0); + let boxed_array: Box = match v.into_boxed_slice().try_into() { + Ok(a) => a, + Err(e) => { + println!("Unexpected length {}", e.len()); + #[allow(clippy::box_default)] + Box::new(Self::Output::default()) + } + }; + *boxed_array + } } #[derive(PartialEq, Eq, Clone)] pub struct Hash(T::Output); impl Hash { - pub fn from_bytes(digest: T::Output) -> Self { - Self(digest) + pub fn from_bytes(digest: Box<[u8]>) -> Self { + Self(T::from_boxed(digest)) } } diff --git a/src/bin/download_sysext.rs b/src/bin/download_sysext.rs index 62ebf9b..592faaa 100644 --- a/src/bin/download_sysext.rs +++ b/src/bin/download_sysext.rs @@ -21,7 +21,7 @@ use reqwest::redirect::Policy; use url::Url; use update_format_crau::delta_update; -use ue_rs::hash_on_disk_sha256; +use ue_rs::hash_on_disk; #[derive(Debug)] enum PackageStatus { @@ -38,7 +38,8 @@ enum PackageStatus { struct Package<'a> { url: Url, name: Cow<'a, str>, - hash: omaha::Hash, + hash_sha256: Option>, + hash_sha1: Option>, size: omaha::FileSize, status: PackageStatus, } @@ -48,8 +49,8 @@ impl<'a> Package<'a> { // Return Sha256 hash of data in the given path. // If maxlen is None, a simple read to the end of the file. // If maxlen is Some, read only until the given length. - fn hash_on_disk(&mut self, path: &Path, maxlen: Option) -> Result> { - hash_on_disk_sha256(path, maxlen) + fn hash_on_disk(&mut self, path: &Path, maxlen: Option) -> Result> { + hash_on_disk::(path, maxlen) } #[rustfmt::skip] @@ -80,10 +81,13 @@ impl<'a> Package<'a> { if size_on_disk == expected_size { info!("{}: download complete, checking hash...", path.display()); - let hash = self.hash_on_disk(&path, None).context({ + let hash_sha256 = self.hash_on_disk::(&path, None).context({ format!("failed to hash_on_disk, path ({:?})", path.display()) })?; - if self.verify_checksum(hash) { + let hash_sha1 = self.hash_on_disk::(&path, None).context({ + format!("failed to hash_on_disk, path ({:?})", path.display()) + })?; + if self.verify_checksum(hash_sha256, hash_sha1) { info!("{}: good hash, will continue without re-download", path.display()); } else { info!("{}: bad hash, will re-download", path.display()); @@ -105,7 +109,14 @@ impl<'a> Package<'a> { info!("downloading {}...", self.url); let path = into_dir.join(&*self.name); - let res = match ue_rs::download_and_hash(client, self.url.clone(), &path, print_progress) { + match ue_rs::download_and_hash( + client, + self.url.clone(), + &path, + self.hash_sha256.clone(), + self.hash_sha1.clone(), + print_progress, + ) { Ok(ok) => ok, Err(err) => { error!("Downloading failed with error {}", err); @@ -114,16 +125,19 @@ impl<'a> Package<'a> { } }; - self.verify_checksum(res.hash); + self.status = PackageStatus::Unverified; Ok(()) } - fn verify_checksum(&mut self, calculated: omaha::Hash) -> bool { - debug!(" expected sha256: {}", self.hash); - debug!(" calculated sha256: {}", calculated); - debug!(" sha256 match? {}", self.hash == calculated); + fn verify_checksum(&mut self, calculated_sha256: omaha::Hash, calculated_sha1: omaha::Hash) -> bool { + debug!(" expected sha256: {:?}", self.hash_sha256); + debug!(" calculated sha256: {}", calculated_sha256); + debug!(" sha256 match? {}", self.hash_sha256 == Some(calculated_sha256.clone())); + debug!(" expected sha1: {:?}", self.hash_sha1); + debug!(" calculated sha1: {}", calculated_sha1); + debug!(" sha1 match? {}", self.hash_sha1 == Some(calculated_sha1.clone())); - if self.hash != calculated { + if self.hash_sha256.is_some() && self.hash_sha256 != Some(calculated_sha256.clone()) || self.hash_sha1.is_some() && self.hash_sha1 != Some(calculated_sha1.clone()) { self.status = PackageStatus::BadChecksum; false } else { @@ -150,7 +164,7 @@ impl<'a> Package<'a> { // Get length of header and data, including header and manifest. let header_data_length = delta_update::get_header_data_length(&header, &delta_archive_manifest).context("failed to get header data length")?; - let hdhash = self.hash_on_disk(from_path, Some(header_data_length)).context(format!("failed to hash_on_disk path ({:?}) failed", from_path.display()))?; + let hdhash = self.hash_on_disk::(from_path, Some(header_data_length)).context(format!("failed to hash_on_disk path ({:?}) failed", from_path.display()))?; let hdhashvec: Vec = hdhash.clone().into(); // Extract data blobs into a file, datablobspath. @@ -162,8 +176,8 @@ impl<'a> Package<'a> { None => bail!("unable to get new_partition_info hash"), }; - let datahash = self.hash_on_disk(datablobspath.as_path(), None).context(format!("failed to hash_on_disk path ({:?})", datablobspath.display()))?; - if datahash != omaha::Hash::from_bytes(pinfo_hash.as_slice()[..].try_into().unwrap_or_default()) { + let datahash = self.hash_on_disk::(datablobspath.as_path(), None).context(format!("failed to hash_on_disk path ({:?})", datablobspath.display()))?; + if datahash != omaha::Hash::from_bytes(pinfo_hash.as_slice()[..].into()) { bail!( "mismatch of data hash ({:?}) with new_partition_info hash ({:?})", datahash, @@ -207,30 +221,29 @@ fn get_pkgs_to_download<'a>(resp: &'a omaha::Response, glob_set: &GlobSet) } let hash_sha256 = pkg.hash_sha256.as_ref(); + let hash_sha1 = pkg.hash.as_ref(); // TODO: multiple URLs per package // not sure if nebraska sends us more than one right now but i suppose this is // for mirrors? - let url = app.update_check.urls.get(0) - .map(|u| u.join(&pkg.name)); + let Some(Ok(url)) = app.update_check.urls.first() + .map(|u| u.join(&pkg.name)) else { + warn!("can't get url for package `{}`, skipping", pkg.name); + continue; + }; - match (url, hash_sha256) { - (Some(Ok(url)), Some(hash)) => { + if hash_sha256.is_none() && hash_sha1.is_none() { + warn!("package `{}` doesn't have a valid SHA256 or SHA1 hash, skipping", pkg.name); + continue; + } to_download.push(Package { url, name: Cow::Borrowed(&pkg.name), - hash: hash.clone(), + hash_sha256: hash_sha256.cloned(), + hash_sha1: hash_sha1.cloned(), size: pkg.size, status: PackageStatus::ToDownload - }) - } - - (Some(Ok(_)), None) => { - warn!("package `{}` doesn't have a valid SHA256 hash, skipping", pkg.name); - } - - _ => (), - } + }); } } @@ -243,11 +256,12 @@ where U: reqwest::IntoUrl + From + std::clone::Clone + std::fmt::Debug, Url: From, { - let r = ue_rs::download_and_hash(client, input_url.clone(), path, print_progress).context(format!("unable to download data(url {:?})", input_url))?; + let r = ue_rs::download_and_hash(client, input_url.clone(), path, None, None, print_progress).context(format!("unable to download data(url {:?})", input_url))?; Ok(Package { name: Cow::Borrowed(path.file_name().unwrap_or(OsStr::new("fakepackage")).to_str().unwrap_or("fakepackage")), - hash: r.hash, + hash_sha256: Some(r.hash_sha256), + hash_sha1: Some(r.hash_sha1), size: FileSize::from_bytes(r.data.metadata().context(format!("failed to get metadata, path ({:?})", path.display()))?.len() as usize), url: input_url.into(), status: PackageStatus::Unverified, diff --git a/src/download.rs b/src/download.rs index 27d3a9d..7b2fdac 100644 --- a/src/download.rs +++ b/src/download.rs @@ -2,24 +2,25 @@ use anyhow::{Context, Result, bail}; use std::io::{BufReader, Read}; use std::fs::File; use std::path::Path; -use log::info; +use log::{info, debug}; use url::Url; use reqwest::StatusCode; use reqwest::blocking::Client; -use sha2::{Sha256, Digest}; +use sha2::digest::DynDigest; const MAX_DOWNLOAD_RETRY: u32 = 20; pub struct DownloadResult { - pub hash: omaha::Hash, + pub hash_sha256: omaha::Hash, + pub hash_sha1: omaha::Hash, pub data: File, } -pub fn hash_on_disk_sha256(path: &Path, maxlen: Option) -> Result> { +pub fn hash_on_disk(path: &Path, maxlen: Option) -> Result> { let file = File::open(path).context(format!("failed to open path({:?})", path.display()))?; - let mut hasher = Sha256::new(); + let mut hasher = T::hasher(); let filelen = file.metadata().context(format!("failed to get metadata of {:?}", path.display()))?.len() as usize; @@ -55,10 +56,17 @@ pub fn hash_on_disk_sha256(path: &Path, maxlen: Option) -> Result(client: &Client, url: U, path: &Path, print_progress: bool) -> Result +fn do_download_and_hash( + client: &Client, + url: U, + path: &Path, + expected_sha256: Option>, + expected_sha1: Option>, + print_progress: bool, +) -> Result where U: reqwest::IntoUrl + Clone, Url: From, @@ -94,19 +102,53 @@ where let mut file = File::create(path).context(format!("failed to create path ({:?})", path.display()))?; res.copy_to(&mut file)?; + let calculated_sha256 = hash_on_disk::(path, None)?; + let calculated_sha1 = hash_on_disk::(path, None)?; + + debug!(" expected sha256: {:?}", expected_sha256); + debug!(" calculated sha256: {}", calculated_sha256); + debug!(" sha256 match? {}", expected_sha256 == Some(calculated_sha256.clone())); + debug!(" expected sha1: {:?}", expected_sha1); + debug!(" calculated sha1: {}", calculated_sha1); + debug!(" sha1 match? {}", expected_sha1 == Some(calculated_sha1.clone())); + + if expected_sha256.is_some() && expected_sha256 != Some(calculated_sha256.clone()) { + bail!("Checksum mismatch for sha256"); + } + if expected_sha1.is_some() && expected_sha1 != Some(calculated_sha1.clone()) { + bail!("Checksum mismatch for sha1"); + } + Ok(DownloadResult { - hash: hash_on_disk_sha256(path, None)?, + hash_sha256: calculated_sha256, + hash_sha1: calculated_sha1, data: file, }) } -pub fn download_and_hash(client: &Client, url: U, path: &Path, print_progress: bool) -> Result +pub fn download_and_hash( + client: &Client, + url: U, + path: &Path, + expected_sha256: Option>, + expected_sha1: Option>, + print_progress: bool, +) -> Result where U: reqwest::IntoUrl + Clone, Url: From, { crate::retry_loop( - || do_download_and_hash(client, url.clone(), path, print_progress), + || { + do_download_and_hash( + client, + url.clone(), + path, + expected_sha256.clone(), + expected_sha1.clone(), + print_progress, + ) + }, MAX_DOWNLOAD_RETRY, ) } diff --git a/src/lib.rs b/src/lib.rs index 8b05895..1ef93f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ mod download; pub use download::DownloadResult; pub use download::download_and_hash; -pub use download::hash_on_disk_sha256; +pub use download::hash_on_disk; mod util; pub use util::retry_loop; diff --git a/src/request.rs b/src/request.rs index 4adcf40..ccba9ff 100644 --- a/src/request.rs +++ b/src/request.rs @@ -28,7 +28,7 @@ pub struct Parameters<'a> { pub machine_id: Cow<'a, str>, } -pub fn perform<'a>(client: &reqwest::blocking::Client, parameters: Parameters<'a>) -> Result { +pub fn perform(client: &reqwest::blocking::Client, parameters: Parameters<'_>) -> Result { let req_body = { let r = omaha::Request { protocol_version: Cow::Borrowed(PROTOCOL_VERSION), diff --git a/test/crau_verify.rs b/test/crau_verify.rs index bd86474..5c53655 100644 --- a/test/crau_verify.rs +++ b/test/crau_verify.rs @@ -44,7 +44,7 @@ fn main() -> Result<(), Box> { // Get length of header and data, including header and manifest. let header_data_length = delta_update::get_header_data_length(&header, &delta_archive_manifest).context("failed to get header data length")?; - let hdhash = ue_rs::hash_on_disk_sha256(headerdatapath.as_path(), Some(header_data_length))?; + let hdhash = ue_rs::hash_on_disk::(headerdatapath.as_path(), Some(header_data_length))?; let hdhashvec: Vec = hdhash.clone().into(); // Get length of header and data diff --git a/vendor/hard-xml-derive/src/lib.rs b/vendor/hard-xml-derive/src/lib.rs index 6d869db..fc478f5 100644 --- a/vendor/hard-xml-derive/src/lib.rs +++ b/vendor/hard-xml-derive/src/lib.rs @@ -1,5 +1,7 @@ #![recursion_limit = "256"] +#![allow(clippy::all)] + extern crate proc_macro; mod attrs; diff --git a/vendor/hard-xml/src/lib.rs b/vendor/hard-xml/src/lib.rs index c193176..bdda8b1 100644 --- a/vendor/hard-xml/src/lib.rs +++ b/vendor/hard-xml/src/lib.rs @@ -333,6 +333,7 @@ //! Root { attr: true } //! ); //! ``` +#![allow(clippy::all)] #[cfg(feature = "log")] mod log;