c2pa-rs

A fork of https://github.com/contentauth/c2pa-rs/
git clone git://archive.git.mtrnord.blog/mtrnords-photography-manager/c2pa-rs.git
Log | Files | Refs | README

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 }