1use crate::error::Error;
21use crate::extract::extract_manifest;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct Exclusion {
27 pub start: usize,
29 pub length: usize,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum HashAlg {
36 Sha256,
37 Sha384,
38 Sha512,
39}
40
41impl HashAlg {
42 pub fn c2pa_id(self) -> &'static str {
44 match self {
45 HashAlg::Sha256 => "sha256",
46 HashAlg::Sha384 => "sha384",
47 HashAlg::Sha512 => "sha512",
48 }
49 }
50}
51
52pub fn data_hash_exclusion(text: &str) -> Result<Exclusion, Error> {
58 let found = extract_manifest(text)?;
59 Ok(Exclusion {
60 start: found.offset,
61 length: found.length,
62 })
63}
64
65#[cfg(feature = "hash")]
69pub fn compute_data_hash(text: &str, alg: HashAlg) -> Result<Vec<u8>, Error> {
70 let ex = data_hash_exclusion(text)?;
71 hash_excluding(text.as_bytes(), ex, alg)
72}
73
74#[cfg(feature = "hash")]
77pub fn verify_data_hash(text: &str, alg: HashAlg, expected: &[u8]) -> Result<bool, Error> {
78 let got = compute_data_hash(text, alg)?;
79 Ok(constant_time_eq(&got, expected))
80}
81
82#[cfg(feature = "hash")]
83fn hash_excluding(bytes: &[u8], ex: Exclusion, alg: HashAlg) -> Result<Vec<u8>, Error> {
84 use c2pa_structured_text::hardbinding::{apply_exclusions, Exclusion as StExclusion};
85 use sha2::{Digest, Sha256, Sha384, Sha512};
86
87 let covered = apply_exclusions(
88 bytes,
89 &[StExclusion {
90 start: ex.start,
91 length: ex.length,
92 }],
93 )
94 .map_err(|_| Error::ExclusionOutOfRange)?;
95
96 Ok(match alg {
97 HashAlg::Sha256 => Sha256::digest(&covered).to_vec(),
98 HashAlg::Sha384 => Sha384::digest(&covered).to_vec(),
99 HashAlg::Sha512 => Sha512::digest(&covered).to_vec(),
100 })
101}
102
103#[cfg(feature = "hash")]
104fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
105 if a.len() != b.len() {
106 return false;
107 }
108 let mut diff = 0u8;
109 for (x, y) in a.iter().zip(b) {
110 diff |= x ^ y;
111 }
112 diff == 0
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::embed::{embed_manifest, ManifestRef};
119
120 const PLAIN: &str = "WEBVTT\n\n00:00:00.000 --> 00:00:05.000\nHello world\n";
121
122 #[test]
123 fn exclusion_covers_note_line() {
124 let signed = embed_manifest(PLAIN, ManifestRef::Url("urn:x")).unwrap();
125 let ex = data_hash_exclusion(&signed).unwrap();
126 let excluded = &signed[ex.start..ex.start + ex.length];
127 assert!(excluded.starts_with("NOTE -----BEGIN C2PA MANIFEST-----"));
128 assert!(excluded.trim_end().ends_with("-----END C2PA MANIFEST-----"));
129 }
130
131 #[test]
132 fn alg_identifiers() {
133 assert_eq!(HashAlg::Sha256.c2pa_id(), "sha256");
134 assert_eq!(HashAlg::Sha384.c2pa_id(), "sha384");
135 assert_eq!(HashAlg::Sha512.c2pa_id(), "sha512");
136 }
137
138 #[cfg(feature = "hash")]
139 #[test]
140 fn hash_is_independent_of_reference() {
141 let a = embed_manifest(PLAIN, ManifestRef::Url("urn:a")).unwrap();
144 let b = embed_manifest(
145 PLAIN,
146 ManifestRef::Url("urn:completely-different-and-longer"),
147 )
148 .unwrap();
149 let ha = compute_data_hash(&a, HashAlg::Sha256).unwrap();
150 let hb = compute_data_hash(&b, HashAlg::Sha256).unwrap();
151 assert_eq!(ha, hb);
152 }
153
154 #[cfg(feature = "hash")]
155 #[test]
156 fn verify_round_trip_and_tamper() {
157 let signed = embed_manifest(PLAIN, ManifestRef::Url("urn:x")).unwrap();
158 let hash = compute_data_hash(&signed, HashAlg::Sha256).unwrap();
159 assert!(verify_data_hash(&signed, HashAlg::Sha256, &hash).unwrap());
160
161 let tampered = signed.replace("Hello world", "Goodbye world");
162 assert!(!verify_data_hash(&tampered, HashAlg::Sha256, &hash).unwrap());
163 }
164
165 #[cfg(feature = "hash")]
166 #[test]
167 fn hash_covers_exactly_the_non_excluded_bytes() {
168 let signed =
171 "WEBVTT\n\nNOTE -----BEGIN C2PA MANIFEST----- urn:x -----END C2PA MANIFEST-----\n";
172 let h = compute_data_hash(signed, HashAlg::Sha256).unwrap();
173 use sha2::{Digest, Sha256};
174 let mut d = Sha256::new();
175 d.update(b"WEBVTT\n\n");
176 assert_eq!(h, d.finalize().to_vec());
177 }
178}