hash_utils.rs (18130B)
1 // Copyright 2022 Adobe. All rights reserved. 2 // This file is licensed to you under the Apache License, 3 // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 4 // or the MIT license (http://opensource.org/licenses/MIT), 5 // at your option. 6 7 // Unless required by applicable law or agreed to in writing, 8 // this software is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or 10 // implied. See the LICENSE-MIT and LICENSE-APACHE files for the 11 // specific language governing permissions and limitations under 12 // each license. 13 14 use std::{ 15 fs::File, 16 io::{Cursor, Read, Seek, SeekFrom}, 17 ops::RangeInclusive, 18 path::Path, 19 }; 20 21 // multihash versions 22 use multibase::{decode, encode}; 23 use multihash::{wrap, Code, Multihash, Sha1, Sha2_256, Sha2_512, Sha3_256, Sha3_384, Sha3_512}; 24 use range_set::RangeSet; 25 use serde::{Deserialize, Serialize}; 26 // direct sha functions 27 use sha2::{Digest, Sha256, Sha384, Sha512}; 28 //use conv::ValueFrom; 29 use tracing::warn; 30 31 use crate::{Error, Result}; 32 33 const MAX_HASH_BUF: usize = 256 * 1024 * 1024; // cap memory usage to 256MB 34 35 #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] 36 pub struct HashRange { 37 start: usize, 38 length: usize, 39 40 #[serde(skip)] 41 bmff_offset: Option<u64>, /* optional tracking of offset positions to include in BMFF_V2 hashes in BE format */ 42 } 43 44 impl HashRange { 45 pub const fn new(start: usize, length: usize) -> Self { 46 HashRange { 47 start, 48 length, 49 bmff_offset: None, 50 } 51 } 52 53 /// update the start value 54 #[allow(dead_code)] 55 pub fn set_start(&mut self, start: usize) { 56 self.start = start; 57 } 58 59 /// return start as usize 60 pub const fn start(&self) -> usize { 61 self.start 62 } 63 64 /// return length as usize 65 pub const fn length(&self) -> usize { 66 self.length 67 } 68 69 pub fn set_length(&mut self, length: usize) { 70 self.length = length; 71 } 72 73 // set offset for BMFF_V2 to be hashed in addition to data 74 pub fn set_bmff_offset(&mut self, offset: u64) { 75 self.bmff_offset = Some(offset); 76 } 77 78 // get option offset for BMFF_V2 hash 79 pub const fn bmff_offset(&self) -> Option<u64> { 80 self.bmff_offset 81 } 82 } 83 84 /// Compare two byte vectors return true if match, false otherwise 85 pub fn vec_compare(va: &[u8], vb: &[u8]) -> bool { 86 (va.len() == vb.len()) && // zip stops at the shortest 87 va.iter() 88 .zip(vb) 89 .all(|(a,b)| a == b) 90 } 91 92 /// Generate hash of type hash_type for supplied data array. The 93 /// hash_type are those specified in the multihash specification. Currently 94 /// we only support Sha2-256/512 or Sha2-256/512. 95 /// Returns hash or None if incompatible type 96 pub fn hash_by_type(hash_type: u8, data: &[u8]) -> Option<Multihash> { 97 match hash_type { 98 0x12 => Some(Sha2_256::digest(data)), 99 0x13 => Some(Sha2_512::digest(data)), 100 0x14 => Some(Sha3_512::digest(data)), 101 0x15 => Some(Sha3_384::digest(data)), 102 0x16 => Some(Sha3_256::digest(data)), 103 _ => None, 104 } 105 } 106 107 #[derive(Clone)] 108 pub enum Hasher { 109 SHA256(Sha256), 110 SHA384(Sha384), 111 SHA512(Sha512), 112 } 113 114 impl Hasher { 115 // update hash value with new data 116 pub fn update(&mut self, data: &[u8]) { 117 use Hasher::*; 118 // update the hash 119 match self { 120 SHA256(ref mut d) => d.update(data), 121 SHA384(ref mut d) => d.update(data), 122 SHA512(ref mut d) => d.update(data), 123 } 124 } 125 126 // consume hasher and return the final digest 127 pub fn finalize(hasher_enum: Hasher) -> Vec<u8> { 128 use Hasher::*; 129 // return the hash 130 match hasher_enum { 131 SHA256(d) => d.finalize().to_vec(), 132 SHA384(d) => d.finalize().to_vec(), 133 SHA512(d) => d.finalize().to_vec(), 134 } 135 } 136 } 137 138 // Return hash bytes for desired hashing algorithm. 139 pub fn hash_by_alg(alg: &str, data: &[u8], exclusions: Option<Vec<HashRange>>) -> Vec<u8> { 140 let mut reader = Cursor::new(data); 141 142 hash_stream_by_alg(alg, &mut reader, exclusions, true).unwrap_or_default() 143 } 144 145 // Return hash inclusive bytes for desired hashing algorithm. 146 pub fn hash_by_alg_with_inclusions(alg: &str, data: &[u8], inclusions: Vec<HashRange>) -> Vec<u8> { 147 let mut reader = Cursor::new(data); 148 149 hash_stream_by_alg(alg, &mut reader, Some(inclusions), false).unwrap_or_default() 150 } 151 152 // Return hash bytes for asset using desired hashing algorithm. 153 pub fn hash_asset_by_alg( 154 alg: &str, 155 asset_path: &Path, 156 exclusions: Option<Vec<HashRange>>, 157 ) -> Result<Vec<u8>> { 158 let mut file = File::open(asset_path)?; 159 hash_stream_by_alg(alg, &mut file, exclusions, true) 160 } 161 162 // Return hash inclusive bytes for asset using desired hashing algorithm. 163 pub fn hash_asset_by_alg_with_inclusions( 164 alg: &str, 165 asset_path: &Path, 166 inclusions: Vec<HashRange>, 167 ) -> Result<Vec<u8>> { 168 let mut file = File::open(asset_path)?; 169 hash_stream_by_alg(alg, &mut file, Some(inclusions), false) 170 } 171 172 /* Returns hash bytes for a stream using desired hashing algorithm. The function handles the many 173 possible hash requirements of C2PA. The function accepts a source stream 'data', an optional 174 set of hash ranges 'hash_range' and a boolean to indicate whether the hash range is an exclusion 175 or inclusion set of hash ranges. 176 177 The basic case is to hash a stream without hash ranges: 178 The data represents a single contiguous stream of bytes to be hash where D are data bytes 179 180 to_be_hashed: [DDDDDDDDD...DDDDDDDDDD] 181 182 The data is then chunked and hashed in groups to reduce memory 183 footprint and increase performance. 184 185 The most common case for C2PA is the use of an exclusion hash. In this case the 'hash_range' indicate 186 which byte ranges should be excluded shown here depicted with I for included bytes and X for excluded bytes 187 188 to_be_hashed: [IIIIXXXIIIIXXXXXIIIXXIII...IIII] 189 190 In this case the data is split into a set of ranges covering the included bytes. The set of ranged bytes 191 are then chunked and hashed just like the default case. 192 193 The opposite of this is when 'is_exclusion' is set to false indicating the 'hash_ranges' represent the bytes 194 to include in the hash. Here are the bytes in 'data' are excluded except those explicitly referenced. 195 196 to_be_hashed: [XXXXXXIIIIXXXXXIIXXXX...XXXX] 197 198 Again a set of ranged bytes are created and hashed as described above. 199 200 The last case is a special requirement for BMFF based assets (exclusion hashes only). For this case we not 201 only hash the data but also the location where the data was found in the asset. To do this we add a special 202 HashRange object to the hash ranges to indicate which locations in the stream require this special offset 203 hash. To make processing efficient we again split the data into ranges at not just the exclusion 204 points but also for these markers. The hashing loop knows to pause at these special marker ranges to insert 205 the hash of the offset. The stream sent to the hashing loop logically looks like this where M is the marker. 206 to_be_hashed: [IIIIIXXXXXMIIIIIMXXXXXMXXXXIII...III] 207 208 The data is again split into range sets breaking at the exclusion points and now also the markers. 209 */ 210 pub fn hash_stream_by_alg<R>( 211 alg: &str, 212 data: &mut R, 213 hash_range: Option<Vec<HashRange>>, 214 is_exclusion: bool, 215 ) -> Result<Vec<u8>> 216 where 217 R: Read + Seek + ?Sized, 218 { 219 let mut bmff_v2_starts: Vec<u64> = Vec::new(); 220 221 use Hasher::*; 222 let mut hasher_enum = match alg { 223 "sha256" => SHA256(Sha256::new()), 224 "sha384" => SHA384(Sha384::new()), 225 "sha512" => SHA512(Sha512::new()), 226 _ => { 227 warn!( 228 "Unsupported hashing algorithm: {}, substituting sha256", 229 alg 230 ); 231 SHA256(Sha256::new()) 232 } 233 }; 234 235 let data_len = data.seek(SeekFrom::End(0))?; 236 data.rewind()?; 237 238 let ranges = match hash_range { 239 Some(mut hr) if !hr.is_empty() => { 240 // hash data skipping excluded regions 241 // sort the exclusions 242 hr.sort_by_key(|a| a.start()); 243 244 // verify structure of blocks 245 let num_blocks = hr.len(); 246 let range_end = hr[num_blocks - 1].start() + hr[num_blocks - 1].length(); 247 let data_end = data_len - 1; 248 249 // range extends past end of file so fail 250 if data_len < range_end as u64 { 251 return Err(Error::BadParam( 252 "The exclusion range exceed the data length".to_string(), 253 )); 254 } 255 256 if is_exclusion { 257 //build final ranges 258 let mut ranges_vec: Vec<RangeInclusive<u64>> = Vec::new(); 259 let mut ranges = RangeSet::<[RangeInclusive<u64>; 1]>::from(0..=data_end); 260 for exclusion in hr { 261 let end = (exclusion.start() + exclusion.length() - 1) as u64; 262 let exclusion_start = exclusion.start() as u64; 263 ranges.remove_range(exclusion_start..=end); 264 265 // add new BMFF V2 offset as a new range to be included so that we can 266 // pause to add the offset hash 267 if let Some(offset) = exclusion.bmff_offset() { 268 bmff_v2_starts.push(offset); 269 } 270 } 271 272 // merge standard ranges and BMFF V2 ranges into single list 273 if !bmff_v2_starts.is_empty() { 274 // remove any offset hashes that would be excluded 275 let test_ranges = ranges.clone().into_smallvec(); 276 bmff_v2_starts.retain(|o| test_ranges.iter().any(|r| r.contains(&(*o + 1)))); 277 278 // add in remaining BMFF V2 offsets 279 for os in bmff_v2_starts.iter() { 280 ranges_vec.push(RangeInclusive::new(*os, *os)); 281 } 282 283 // add regularly included ranges 284 for r in ranges.into_smallvec() { 285 ranges_vec.push(r); 286 } 287 288 // sort by start position 289 ranges_vec.sort_by(|a, b| { 290 let a_start = a.start(); 291 let b_start = b.start(); 292 a_start.cmp(b_start) 293 }); 294 295 ranges_vec 296 } else { 297 for r in ranges.into_smallvec() { 298 ranges_vec.push(r); 299 } 300 ranges_vec 301 } 302 } else { 303 //build final ranges 304 let mut ranges_vec: Vec<RangeInclusive<u64>> = Vec::new(); 305 for inclusion in hr { 306 let end = (inclusion.start() + inclusion.length() - 1) as u64; 307 let inclusion_start = inclusion.start() as u64; 308 309 // add new BMFF V2 offset as a new range to be included so that we can 310 // pause to add the offset hash 311 if let Some(offset) = inclusion.bmff_offset() { 312 ranges_vec.push(RangeInclusive::new(offset, offset)); 313 bmff_v2_starts.push(offset); 314 } 315 316 // add inclusion 317 ranges_vec.push(RangeInclusive::new(inclusion_start, end)); 318 } 319 ranges_vec 320 } 321 } 322 _ => { 323 let mut ranges_vec: Vec<RangeInclusive<u64>> = Vec::new(); 324 let data_end = data_len - 1; 325 ranges_vec.push(RangeInclusive::new(0_u64, data_end)); 326 327 ranges_vec 328 } 329 }; 330 331 if cfg!(feature = "no_interleaved_io") || cfg!(target_arch = "wasm32") { 332 // hash the data for ranges 333 for r in ranges { 334 let start = r.start(); 335 let end = r.end(); 336 let mut chunk_left = end - start + 1; 337 338 // move to start of range 339 data.seek(SeekFrom::Start(*start))?; 340 341 // check to see if this range is an BMFF V2 offset to include in the hash 342 if bmff_v2_starts.contains(start) && (end - start) == 0 { 343 hasher_enum.update(&start.to_be_bytes()); 344 } 345 346 loop { 347 let mut chunk = vec![0u8; std::cmp::min(chunk_left as usize, MAX_HASH_BUF)]; 348 349 data.read_exact(&mut chunk)?; 350 351 hasher_enum.update(&chunk); 352 353 chunk_left -= chunk.len() as u64; 354 if chunk_left == 0 { 355 break; 356 } 357 } 358 } 359 } else { 360 // hash the data for ranges 361 for r in ranges { 362 let start = r.start(); 363 let end = r.end(); 364 let mut chunk_left = end - start + 1; 365 366 // move to start of range 367 data.seek(SeekFrom::Start(*start))?; 368 369 // check to see if this range is an BMFF V2 offset to include in the hash 370 if bmff_v2_starts.contains(start) && (end - start) == 0 { 371 hasher_enum.update(&start.to_be_bytes()); 372 } 373 374 let mut chunk = vec![0u8; std::cmp::min(chunk_left as usize, MAX_HASH_BUF)]; 375 data.read_exact(&mut chunk)?; 376 377 loop { 378 let (tx, rx) = std::sync::mpsc::channel(); 379 380 chunk_left -= chunk.len() as u64; 381 382 std::thread::spawn(move || { 383 hasher_enum.update(&chunk); 384 tx.send(hasher_enum).unwrap_or_default(); 385 }); 386 387 // are we done 388 if chunk_left == 0 { 389 hasher_enum = match rx.recv() { 390 Ok(hasher) => hasher, 391 Err(_) => return Err(Error::ThreadReceiveError), 392 }; 393 break; 394 } 395 396 // read next chunk while we wait for hash 397 let mut next_chunk = vec![0u8; std::cmp::min(chunk_left as usize, MAX_HASH_BUF)]; 398 data.read_exact(&mut next_chunk)?; 399 400 hasher_enum = match rx.recv() { 401 Ok(hasher) => hasher, 402 Err(_) => return Err(Error::ThreadReceiveError), 403 }; 404 405 chunk = next_chunk; 406 } 407 } 408 } 409 410 // return the hash 411 Ok(Hasher::finalize(hasher_enum)) 412 } 413 414 // verify the hash using the specified algorithm 415 pub fn verify_by_alg( 416 alg: &str, 417 hash: &[u8], 418 data: &[u8], 419 exclusions: Option<Vec<HashRange>>, 420 ) -> bool { 421 // hash with the same algorithm as target 422 let data_hash = hash_by_alg(alg, data, exclusions); 423 vec_compare(hash, &data_hash) 424 } 425 426 // verify the hash using the specified algorithm 427 pub fn verify_asset_by_alg( 428 alg: &str, 429 hash: &[u8], 430 asset_path: &Path, 431 exclusions: Option<Vec<HashRange>>, 432 ) -> bool { 433 // hash with the same algorithm as target 434 if let Ok(data_hash) = hash_asset_by_alg(alg, asset_path, exclusions) { 435 vec_compare(hash, &data_hash) 436 } else { 437 false 438 } 439 } 440 441 pub fn verify_stream_by_alg<R>( 442 alg: &str, 443 hash: &[u8], 444 reader: &mut R, 445 hash_range: Option<Vec<HashRange>>, 446 is_exclusion: bool, 447 ) -> bool 448 where 449 R: Read + Seek + ?Sized, 450 { 451 if let Ok(data_hash) = hash_stream_by_alg(alg, reader, hash_range, is_exclusion) { 452 vec_compare(hash, &data_hash) 453 } else { 454 false 455 } 456 } 457 458 /// Return a Sha256 hash of array of bytes 459 #[allow(dead_code)] 460 pub fn hash_sha256(data: &[u8]) -> Vec<u8> { 461 let mh = Sha2_256::digest(data); 462 let digest = mh.digest(); 463 464 digest.to_vec() 465 } 466 467 pub fn hash_sha1(data: &[u8]) -> Vec<u8> { 468 let mh = Sha1::digest(data); 469 let digest = mh.digest(); 470 digest.to_vec() 471 } 472 473 /// Verify muiltihash against input data. True if match, 474 /// false if no match or unsupported. The hash value should be 475 /// be multibase encoded string. 476 pub fn verify_hash(hash: &str, data: &[u8]) -> bool { 477 match decode(hash) { 478 Ok((_code, mh)) => { 479 if mh.len() < 2 { 480 return false; 481 } 482 483 // multihash lead bytes 484 let hash_type = mh[0]; // hash type 485 let _hash_len = mh[1]; // hash data length 486 487 // hash with the same algorithm as target 488 if let Some(data_hash) = hash_by_type(hash_type, data) { 489 vec_compare(data_hash.digest(), &mh.as_slice()[2..]) 490 } else { 491 false 492 } 493 } 494 Err(_) => false, 495 } 496 } 497 498 /// Return the hash of data in the same hash format in_hash 499 pub fn hash_as_source(in_hash: &str, data: &[u8]) -> Option<String> { 500 match decode(in_hash) { 501 Ok((code, mh)) => { 502 if mh.len() < 2 { 503 return None; 504 } 505 506 // multihash lead bytes 507 let hash_type = mh[0]; // hash type 508 509 // hash with the same algorithm as target 510 match hash_by_type(hash_type, data) { 511 Some(hash) => { 512 let digest = hash.digest(); 513 514 let wrapped = match hash_type { 515 0x12 => wrap(Code::Sha2_256, digest), 516 0x13 => wrap(Code::Sha2_512, digest), 517 0x14 => wrap(Code::Sha3_512, digest), 518 0x15 => wrap(Code::Sha3_384, digest), 519 0x16 => wrap(Code::Sha3_256, digest), 520 _ => return None, 521 }; 522 523 // Return encoded hash. 524 Some(encode(code, wrapped.as_bytes())) 525 } 526 None => None, 527 } 528 } 529 Err(_) => None, 530 } 531 } 532 533 // Used by Merkle tree calculations to generate the pair wise hash 534 pub fn concat_and_hash(alg: &str, left: &[u8], right: Option<&[u8]>) -> Vec<u8> { 535 let mut temp = left.to_vec(); 536 537 if let Some(r) = right { 538 temp.append(&mut r.to_vec()) 539 } 540 541 hash_by_alg(alg, &temp, None) 542 }