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

commit 0a735a0910b10e085ee36b1e02b15d935774f090
parent 18204cc2ecdc7ba0e18dc2ec619dad281da3dd9d
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date:   Tue, 21 Feb 2023 17:54:28 -0500

Bmff v2 (#186)

* Check for more than one manifest store

* Fix grabbing wrong UUID

* Updated to fix playback issue

* Add comment

* Initial code

* BMFF 1.1 hash support

* fix missing file

* Test wasm fix

* Slightly more readable check

* Better variable name

* Fix typos in comments

* Update to incorporate comments
Diffstat:
Msdk/src/assertions/bmff_hash.rs | 32+++++++++++++++++++++++++-------
Msdk/src/assertions/data_hash.rs | 16++++++++++++----
Msdk/src/asset_handlers/bmff_io.rs | 74+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msdk/src/store.rs | 35+++++++++++++++++++++++++++++------
Msdk/src/utils/hash_utils.rs | 142++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Asdk/tests/fixtures/legacy.mp4 | 0
6 files changed, 203 insertions(+), 96 deletions(-)

diff --git a/sdk/src/assertions/bmff_hash.rs b/sdk/src/assertions/bmff_hash.rs @@ -30,7 +30,7 @@ use crate::{ Error, }; -const ASSERTION_CREATION_VERSION: usize = 1; +const ASSERTION_CREATION_VERSION: usize = 2; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct ExclusionsMap { @@ -109,8 +109,11 @@ pub struct BmffHash { #[serde(skip_serializing_if = "Option::is_none")] url: Option<UriT>, - #[serde(skip_deserializing, skip_serializing)] + #[serde(skip)] pub path: PathBuf, + + #[serde(skip)] + bmff_version: usize, } impl BmffHash { @@ -123,6 +126,7 @@ impl BmffHash { name: Some(name.to_string()), url, path: PathBuf::new(), + bmff_version: ASSERTION_CREATION_VERSION, } } @@ -159,6 +163,14 @@ impl BmffHash { self.url.as_ref() } + pub fn bmff_version(&self) -> usize { + self.bmff_version + } + + fn set_bmff_version(&mut self, version: usize) { + self.bmff_version = version; + } + /// Returns `true` if this is a remote hash. pub fn is_remote_hash(&self) -> bool { self.url.is_some() @@ -200,7 +212,8 @@ impl BmffHash { // convert BMFF exclusion map to flat exclusion list let mut data = fs::File::open(asset_path)?; - let exclusions = bmff_to_jumbf_exclusions(&mut data, bmff_exclusions)?; + let exclusions = + bmff_to_jumbf_exclusions(&mut data, bmff_exclusions, self.bmff_version > 1)?; let hash = hash_asset_by_alg(&alg, asset_path, Some(exclusions))?; @@ -225,7 +238,8 @@ impl BmffHash { let mut data_reader = Cursor::new(data); // convert BMFF exclusion map to flat exclusion list - let exclusions = bmff_to_jumbf_exclusions(&mut data_reader, bmff_exclusions)?; + let exclusions = + bmff_to_jumbf_exclusions(&mut data_reader, bmff_exclusions, self.bmff_version > 1)?; if verify_by_alg(&curr_alg, &self.hash, data, Some(exclusions)) { Ok(()) @@ -241,7 +255,8 @@ impl BmffHash { // convert BMFF exclusion map to flat exclusion list let mut data = fs::File::open(asset_path)?; - let exclusions = bmff_to_jumbf_exclusions(&mut data, bmff_exclusions)?; + let exclusions = + bmff_to_jumbf_exclusions(&mut data, bmff_exclusions, self.bmff_version > 1)?; if verify_asset_by_alg(curr_alg, &self.hash, asset_path, Some(exclusions)) { Ok(()) @@ -255,13 +270,16 @@ impl AssertionCbor for BmffHash {} impl AssertionBase for BmffHash { const LABEL: &'static str = Self::LABEL; - const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION); + const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION); // todo: this mechanism needs to change since a struct could support different versions fn to_assertion(&self) -> Result<Assertion> { Self::to_cbor_assertion(self) } fn from_assertion(assertion: &Assertion) -> Result<Self> { - Self::from_cbor_assertion(assertion) + let mut bmff_hash = Self::from_cbor_assertion(assertion)?; + bmff_hash.set_bmff_version(assertion.get_ver().unwrap_or(1)); + + Ok(bmff_hash) } } diff --git a/sdk/src/assertions/data_hash.rs b/sdk/src/assertions/data_hash.rs @@ -11,7 +11,10 @@ // specific language governing permissions and limitations under // each license. -use std::path::*; +use std::{ + io::{Read, Seek}, + path::*, +}; use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; @@ -19,7 +22,6 @@ use serde_bytes::ByteBuf; use crate::{ assertion::{Assertion, AssertionBase, AssertionCbor}, assertions::labels, - asset_io::CAIReadWrite, cbor_types::UriT, error::{Error, Result}, utils::hash_utils::{hash_stream_by_alg, verify_asset_by_alg, verify_by_alg, Exclusion}, @@ -106,7 +108,10 @@ impl DataHash { } /// generate the hash value for the Asset stream using the range from the DataHash - pub fn gen_hash_from_stream(&mut self, stream: &mut dyn CAIReadWrite) -> Result<()> { + pub fn gen_hash_from_stream<R>(&mut self, stream: &mut R) -> Result<()> + where + R: Read + Seek + ?Sized, + { self.hash = self.hash_from_stream(stream)?; Ok(()) } @@ -156,7 +161,10 @@ impl DataHash { /// generate the asset hash from a stream using the constructed /// start and length values - pub fn hash_from_stream(&mut self, stream: &mut dyn CAIReadWrite) -> Result<Vec<u8>> { + pub fn hash_from_stream<R>(&mut self, stream: &mut R) -> Result<Vec<u8>> + where + R: Read + Seek + ?Sized, + { if self.is_remote_hash() { return Err(Error::BadParam( "asset hash is remote, not yet supported".to_owned(), diff --git a/sdk/src/asset_handlers/bmff_io.rs b/sdk/src/asset_handlers/bmff_io.rs @@ -45,7 +45,8 @@ impl BmffIO { } } -const HEADER_SIZE: u64 = 8; +const HEADER_SIZE: u64 = 8; // 4 byte type + 4 byte size +const HEADER_SIZE_LARGE: u64 = 16; // 4 byte type + 4 byte size + 8 byte large size const C2PA_UUID: [u8; 16] = [ 0xD8, 0xFE, 0xC3, 0xD6, 0x1B, 0x0E, 0x48, 0x3C, 0x92, 0x97, 0x58, 0x28, 0x87, 0x7E, 0xC4, 0x81, @@ -165,6 +166,7 @@ struct BoxHeaderLite { pub name: BoxType, pub size: u64, pub fourcc: String, + pub large_size: bool, } impl BoxHeaderLite { @@ -173,6 +175,7 @@ impl BoxHeaderLite { name, size, fourcc: fourcc.to_string(), + large_size: false, } } pub fn read<R: Read + ?Sized>(reader: &mut R) -> Result<Self> { @@ -198,14 +201,16 @@ impl BoxHeaderLite { Ok(BoxHeaderLite { name: BoxType::from(typ), - size: largesize - HEADER_SIZE, + size: largesize, fourcc, + large_size: true, }) } else { Ok(BoxHeaderLite { name: BoxType::from(typ), size: size as u64, fourcc, + large_size: false, }) } } @@ -252,8 +257,12 @@ fn write_box_header_ext<W: Write>(w: &mut W, v: u8, f: u32) -> Result<u64> { Ok(4) } -fn box_start(reader: &mut dyn CAIRead) -> Result<u64> { - Ok(reader.stream_position()? - HEADER_SIZE) +fn box_start(reader: &mut dyn CAIRead, is_large: bool) -> Result<u64> { + if is_large { + Ok(reader.stream_position()? - HEADER_SIZE_LARGE) + } else { + Ok(reader.stream_position()? - HEADER_SIZE) + } } fn _skip_bytes(reader: &mut dyn CAIRead, size: u64) -> Result<()> { @@ -266,12 +275,6 @@ fn skip_bytes_to(reader: &mut dyn CAIRead, pos: u64) -> Result<u64> { Ok(pos) } -fn _skip_box(reader: &mut dyn CAIRead, size: u64) -> Result<()> { - let start = box_start(reader)?; - skip_bytes_to(reader, start + size)?; - Ok(()) -} - fn write_c2pa_box<W: Write>( w: &mut W, data: &[u8], @@ -360,9 +363,30 @@ fn path_from_token(bmff_tree: &mut Arena<BoxInfo>, current_node_token: &Token) - Ok(path) } +fn get_top_level_box_offsets( + bmff_tree: &Arena<BoxInfo>, + bmff_path_map: &HashMap<String, Vec<Token>>, +) -> Vec<u64> { + let mut tl_offsets = Vec::new(); + + for (p, t) in bmff_path_map { + // look for top level offsets + if p.matches('/').count() == 1 { + for token in t { + if let Some(box_info) = bmff_tree.get(*token) { + tl_offsets.push(box_info.data.offset); + } + } + } + } + + tl_offsets +} + pub fn bmff_to_jumbf_exclusions( reader: &mut dyn CAIRead, bmff_exclusions: &[ExclusionsMap], + bmff_v2: bool, ) -> Result<Vec<Exclusion>> { let start = reader.stream_position()?; let size = reader.seek(SeekFrom::End(0))?; @@ -386,6 +410,10 @@ pub fn bmff_to_jumbf_exclusions( // build layout of the BMFF structure build_bmff_tree(reader, size, &mut bmff_tree, &root_token, &mut bmff_map)?; + // get top level box offsets + let mut tl_offsets = get_top_level_box_offsets(&bmff_tree, &bmff_map); + tl_offsets.sort(); + let mut exclusions = Vec::new(); for bmff_exclusion in bmff_exclusions { @@ -481,17 +509,37 @@ pub fn bmff_to_jumbf_exclusions( subset.length as u64 }) as usize, ); + exclusions.push(exclusion); } } else { + // exclude box in its entirty let exclusion = Exclusion::new(exclusion_start as usize, exclusion_length as usize); + exclusions.push(exclusion); + + // for BMFF V2 hashes we do not add hash offsets for top level boxes + // that are completely excluded, so remove from BMFF V2 hash offset calc + if let Some(pos) = tl_offsets.iter().position(|x| *x == exclusion_start) { + tl_offsets.remove(pos); + } } } } } + // add remaining top level offsets to be included when generating BMFF V2 hashes + // note: this is technically not an exclusion but a replacement with a new range of bytes to be hashed + if bmff_v2 { + for tl_start in tl_offsets { + let mut exclusion = Exclusion::new(tl_start as usize, 1); + exclusion.set_bmff_offset(tl_start); + + exclusions.push(exclusion); + } + } + Ok(exclusions) } @@ -637,7 +685,7 @@ pub(crate) fn build_bmff_tree( // Match and parse the supported atom boxes. match header.name { BoxType::UuidBox => { - let start = box_start(reader)?; + let start = box_start(reader, header.large_size)?; let mut extended_type = [0u8; 16]; // 16 bytes of UUID reader.read_exact(&mut extended_type)?; @@ -680,7 +728,7 @@ pub(crate) fn build_bmff_tree( | BoxType::MfraBox | BoxType::MetaBox | BoxType::SchiBox => { - let start = box_start(reader)?; + let start = box_start(reader, header.large_size)?; let b = if FULL_BOX_TYPES.contains(&header.fourcc.as_str()) { let (version, flags) = read_box_header_ext(reader)?; // box extensions @@ -727,7 +775,7 @@ pub(crate) fn build_bmff_tree( skip_bytes_to(reader, start + s)?; } _ => { - let start = reader.stream_position()? - HEADER_SIZE; + let start = box_start(reader, header.large_size)?; let b = if FULL_BOX_TYPES.contains(&header.fourcc.as_str()) { let (version, flags) = read_box_header_ext(reader)?; // box extensions diff --git a/sdk/src/store.rs b/sdk/src/store.rs @@ -12,7 +12,7 @@ // each license. #[cfg(feature = "sign")] -use std::io::SeekFrom; +use std::io::{Read, Seek, SeekFrom}; use std::{collections::HashMap, io::Cursor}; #[cfg(feature = "file_io")] use std::{fs, path::Path}; @@ -1337,12 +1337,15 @@ impl Store { // generate a list of AssetHashes based on the location of objects in the stream #[cfg(feature = "sign")] - fn generate_data_hashes_for_stream( - stream: &mut dyn CAIReadWrite, + fn generate_data_hashes_for_stream<R>( + stream: &mut R, alg: &str, block_locations: &mut Vec<HashObjectPositions>, calc_hashes: bool, - ) -> Result<Vec<DataHash>> { + ) -> Result<Vec<DataHash>> + where + R: Read + Seek + ?Sized, + { if block_locations.is_empty() { let out: Vec<DataHash> = vec![]; return Ok(out); @@ -1485,6 +1488,17 @@ impl Store { trun.flags = Some(ByteBuf::from([1, 0, 0])); exclusions.push(trun); + // V2 exclusions + // /mdat exclusion + let mut mdat = ExclusionsMap::new("/mdat".to_owned()); + let subset_mdat = SubsetMap { + offset: 16, + length: 0, + }; + let subset_mdat_vec = vec![subset_mdat]; + mdat.subset = Some(subset_mdat_vec); + exclusions.push(mdat); + if calc_hashes { dh.gen_hash(asset_path)?; } else { @@ -2023,8 +2037,6 @@ impl Store { #[cfg(not(target_arch = "wasm32"))] #[cfg(feature = "file_io")] fn fetch_remote_manifest(url: &str) -> Result<Vec<u8>> { - use std::io::Read; - use conv::ValueFrom; use ureq::Error as uError; @@ -3180,6 +3192,8 @@ pub mod tests { let ap = fixture_path("CA.jpg"); let mut report = DetailedStatusTracker::new(); let store = Store::load_from_asset(&ap, true, &mut report).expect("load_from_asset"); + let _errors = report_split_errors(report.get_log_mut()); + println!("store = {store}"); } @@ -3194,6 +3208,15 @@ pub mod tests { #[test] #[cfg(all(feature = "file_io", feature = "bmff"))] + fn test_bmff_legacy() { + // test 1.0 bmff hash + let ap = fixture_path("legacy.mp4"); + let mut report = DetailedStatusTracker::new(); + let store = Store::load_from_asset(&ap, true, &mut report).expect("load_from_asset"); + println!("store = {store}"); + } + #[test] + #[cfg(all(feature = "file_io", feature = "bmff"))] fn test_bmff_jumbf_generation() { // test adding to actual image let ap = fixture_path("video1.mp4"); diff --git a/sdk/src/utils/hash_utils.rs b/sdk/src/utils/hash_utils.rs @@ -13,12 +13,13 @@ use std::{ fs::File, - io::{Read, Seek, SeekFrom}, + io::{Cursor, Read, Seek, SeekFrom}, ops::RangeInclusive, path::Path, }; -use log::{debug, warn}; +//use conv::ValueFrom; +use log::warn; // multihash versions use multibase::{decode, encode}; use multihash::{wrap, Code, Multihash, Sha2_256, Sha2_512, Sha3_256, Sha3_384, Sha3_512}; @@ -27,7 +28,7 @@ use serde::{Deserialize, Serialize}; // direct sha functions use sha2::{Digest, Sha256, Sha384, Sha512}; -use crate::{asset_io::CAIReadWrite, Error, Result}; +use crate::{Error, Result}; const MAX_HASH_BUF: usize = 256 * 1024 * 1024; // cap memory usage to 256MB @@ -35,11 +36,16 @@ const MAX_HASH_BUF: usize = 256 * 1024 * 1024; // cap memory usage to 256MB pub struct Exclusion { start: usize, length: usize, + bmff_offset: Option<u64>, // optional offset position to include in BMFF_V2 hashes in BE format } impl Exclusion { pub fn new(start: usize, length: usize) -> Self { - Exclusion { start, length } + Exclusion { + start, + length, + bmff_offset: None, + } } /// update the start value @@ -57,6 +63,16 @@ impl Exclusion { pub fn length(&self) -> usize { self.length } + + // set offset for BMFF_V2 to be hashed in addition to data + pub fn set_bmff_offset(&mut self, offset: u64) { + self.bmff_offset = Some(offset); + } + + // get option offset for BMFF_V2 hash + pub fn bmff_offset(&self) -> Option<u64> { + self.bmff_offset + } } /// Compare two byte vectors return true if match, false otherwise @@ -115,61 +131,9 @@ impl Hasher { // Return hash bytes for desired hashing algorithm. pub fn hash_by_alg(alg: &str, data: &[u8], exclusions: Option<Vec<Exclusion>>) -> Vec<u8> { - use Hasher::*; - let mut hasher_enum = match alg { - "sha256" => SHA256(Sha256::new()), - "sha384" => SHA384(Sha384::new()), - "sha512" => SHA512(Sha512::new()), - _ => { - warn!( - "Unsupported hashing algorithm: {}, substituting sha256", - alg - ); - SHA256(Sha256::new()) - } - }; - - match exclusions { - Some(mut e) if !e.is_empty() => { - // hash data skipping excluded regions - // sort the exclusions - e.sort_by_key(|a| a.start()); + let mut reader = Cursor::new(data); - // verify structure of blocks - let num_blocks = e.len(); - let exclusion_end = e[num_blocks - 1].start() + e[num_blocks - 1].length(); - let data_len = data.len(); - let data_end = data_len - 1; - - // if not enough range we will just calc to the end - if data_len < exclusion_end { - debug!("the exclusion range exceed the data length"); - return Vec::new(); - } - - //build final ranges - let mut ranges = RangeSet::<[RangeInclusive<usize>; 1]>::from(0..=data_end); - for exclusion in e { - let end = exclusion.start() + exclusion.length() - 1; - ranges.remove_range(exclusion.start()..=end); - } - - // hash the data for ranges - for r in ranges.into_smallvec() { - hasher_enum.update(&data[r]); - } - - // return the hash - Hasher::finalize(hasher_enum) - } - _ => { - // add the data - hasher_enum.update(data); - - // return the hash - Hasher::finalize(hasher_enum) - } - } + hash_stream_by_alg(alg, &mut reader, exclusions).unwrap_or(Vec::new()) } // Return hash bytes for asset using desired hashing algorithm. @@ -183,11 +147,16 @@ pub fn hash_asset_by_alg( } // Return hash bytes for stream using desired hashing algorithm. -pub fn hash_stream_by_alg( +pub fn hash_stream_by_alg<R>( alg: &str, - data: &mut dyn CAIReadWrite, + data: &mut R, exclusions: Option<Vec<Exclusion>>, -) -> Result<Vec<u8>> { +) -> Result<Vec<u8>> +where + R: Read + Seek + ?Sized, +{ + let mut bmff_v2_starts: Vec<u64> = Vec::new(); + use Hasher::*; let mut hasher_enum = match alg { "sha256" => SHA256(Sha256::new()), @@ -224,24 +193,55 @@ pub fn hash_stream_by_alg( } //build final ranges + let mut ranges_vec: Vec<RangeInclusive<u64>> = Vec::new(); let mut ranges = RangeSet::<[RangeInclusive<u64>; 1]>::from(0..=data_end); for exclusion in e { let end = (exclusion.start() + exclusion.length() - 1) as u64; let exclusion_start = exclusion.start() as u64; ranges.remove_range(exclusion_start..=end); + + // add new BMFF V2 offset as a new range to be included so that we can + // pause to add the offset hash + if let Some(offset) = exclusion.bmff_offset() { + ranges_vec.push(RangeInclusive::new(offset, offset)); + bmff_v2_starts.push(offset); + } } - ranges + // merge standard ranges and BMFF V2 ranges into single list + if !bmff_v2_starts.is_empty() { + // add regularly included ranges + for r in ranges.into_smallvec() { + ranges_vec.push(r); + } + + // sort by start position + ranges_vec.sort_by(|a, b| { + let a_start = a.start(); + let b_start = b.start(); + a_start.cmp(b_start) + }); + + ranges_vec + } else { + for r in ranges.into_smallvec() { + ranges_vec.push(r); + } + ranges_vec + } } _ => { + let mut ranges_vec: Vec<RangeInclusive<u64>> = Vec::new(); let data_end = data_len - 1; - RangeSet::<[RangeInclusive<u64>; 1]>::from(0..=data_end) + ranges_vec.push(RangeInclusive::new(0_u64, data_end)); + + ranges_vec } }; - if cfg!(feature = "no_interleaved_io") { + if cfg!(feature = "no_interleaved_io") || cfg!(target_arch = "wasm32") { // hash the data for ranges - for r in ranges.into_smallvec() { + for r in ranges { let start = r.start(); let end = r.end(); let mut chunk_left = end - start + 1; @@ -249,6 +249,11 @@ pub fn hash_stream_by_alg( // move to start of range data.seek(SeekFrom::Start(*start))?; + // check to see if this range is an BMFF V2 offset to include in the hash + if bmff_v2_starts.contains(start) && (end - start) == 0 { + hasher_enum.update(&start.to_be_bytes()); + } + loop { let mut chunk = vec![0u8; std::cmp::min(chunk_left as usize, MAX_HASH_BUF)]; @@ -264,7 +269,7 @@ pub fn hash_stream_by_alg( } } else { // hash the data for ranges - for r in ranges.into_smallvec() { + for r in ranges { let start = r.start(); let end = r.end(); let mut chunk_left = end - start + 1; @@ -272,6 +277,11 @@ pub fn hash_stream_by_alg( // move to start of range data.seek(SeekFrom::Start(*start))?; + // check to see if this range is an BMFF V2 offset to include in the hash + if bmff_v2_starts.contains(start) && (end - start) == 0 { + hasher_enum.update(&start.to_be_bytes()); + } + let mut chunk = vec![0u8; std::cmp::min(chunk_left as usize, MAX_HASH_BUF)]; data.read_exact(&mut chunk)?; diff --git a/sdk/tests/fixtures/legacy.mp4 b/sdk/tests/fixtures/legacy.mp4 Binary files differ.