use crate::error::Error;
use crate::extract::extract_manifest;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Exclusion {
pub start: usize,
pub length: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HashAlg {
Sha256,
Sha384,
Sha512,
}
impl HashAlg {
pub fn c2pa_id(self) -> &'static str {
match self {
HashAlg::Sha256 => "sha256",
HashAlg::Sha384 => "sha384",
HashAlg::Sha512 => "sha512",
}
}
}
pub fn data_hash_exclusion(text: &str) -> Result<Exclusion, Error> {
let found = extract_manifest(text)?;
Ok(Exclusion {
start: found.offset,
length: found.length,
})
}
#[cfg(feature = "hash")]
pub fn compute_data_hash(text: &str, alg: HashAlg) -> Result<Vec<u8>, Error> {
let ex = data_hash_exclusion(text)?;
hash_excluding(text.as_bytes(), ex, alg)
}
#[cfg(feature = "hash")]
pub fn verify_data_hash(text: &str, alg: HashAlg, expected: &[u8]) -> Result<bool, Error> {
let got = compute_data_hash(text, alg)?;
Ok(constant_time_eq(&got, expected))
}
#[cfg(feature = "hash")]
fn hash_excluding(bytes: &[u8], ex: Exclusion, alg: HashAlg) -> Result<Vec<u8>, Error> {
use c2pa_structured_text::hardbinding::{apply_exclusions, Exclusion as StExclusion};
use sha2::{Digest, Sha256, Sha384, Sha512};
let covered = apply_exclusions(
bytes,
&[StExclusion {
start: ex.start,
length: ex.length,
}],
)
.map_err(|_| Error::ExclusionOutOfRange)?;
Ok(match alg {
HashAlg::Sha256 => Sha256::digest(&covered).to_vec(),
HashAlg::Sha384 => Sha384::digest(&covered).to_vec(),
HashAlg::Sha512 => Sha512::digest(&covered).to_vec(),
})
}
#[cfg(feature = "hash")]
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b) {
diff |= x ^ y;
}
diff == 0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::embed::{embed_manifest, ManifestRef};
const PLAIN: &str = "WEBVTT\n\n00:00:00.000 --> 00:00:05.000\nHello world\n";
#[test]
fn exclusion_covers_note_line() {
let signed = embed_manifest(PLAIN, ManifestRef::Url("urn:x")).unwrap();
let ex = data_hash_exclusion(&signed).unwrap();
let excluded = &signed[ex.start..ex.start + ex.length];
assert!(excluded.starts_with("NOTE -----BEGIN C2PA MANIFEST-----"));
assert!(excluded.trim_end().ends_with("-----END C2PA MANIFEST-----"));
}
#[test]
fn alg_identifiers() {
assert_eq!(HashAlg::Sha256.c2pa_id(), "sha256");
assert_eq!(HashAlg::Sha384.c2pa_id(), "sha384");
assert_eq!(HashAlg::Sha512.c2pa_id(), "sha512");
}
#[cfg(feature = "hash")]
#[test]
fn hash_is_independent_of_reference() {
let a = embed_manifest(PLAIN, ManifestRef::Url("urn:a")).unwrap();
let b = embed_manifest(
PLAIN,
ManifestRef::Url("urn:completely-different-and-longer"),
)
.unwrap();
let ha = compute_data_hash(&a, HashAlg::Sha256).unwrap();
let hb = compute_data_hash(&b, HashAlg::Sha256).unwrap();
assert_eq!(ha, hb);
}
#[cfg(feature = "hash")]
#[test]
fn verify_round_trip_and_tamper() {
let signed = embed_manifest(PLAIN, ManifestRef::Url("urn:x")).unwrap();
let hash = compute_data_hash(&signed, HashAlg::Sha256).unwrap();
assert!(verify_data_hash(&signed, HashAlg::Sha256, &hash).unwrap());
let tampered = signed.replace("Hello world", "Goodbye world");
assert!(!verify_data_hash(&tampered, HashAlg::Sha256, &hash).unwrap());
}
#[cfg(feature = "hash")]
#[test]
fn hash_covers_exactly_the_non_excluded_bytes() {
let signed =
"WEBVTT\n\nNOTE -----BEGIN C2PA MANIFEST----- urn:x -----END C2PA MANIFEST-----\n";
let h = compute_data_hash(signed, HashAlg::Sha256).unwrap();
use sha2::{Digest, Sha256};
let mut d = Sha256::new();
d.update(b"WEBVTT\n\n");
assert_eq!(h, d.finalize().to_vec());
}
}