1use crate::bytes::Bytes32;
34use crate::resolve::{ResolveError, Result};
35use crate::urn::{DigUrn, SecretSalt};
36use sha2::{Digest, Sha256};
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct FoldedProof {
45 pub leaf: Bytes32,
47 pub root: Bytes32,
49}
50
51pub trait ContentCrypto {
53 fn decode_and_fold(&self, proof: &[u8]) -> Option<FoldedProof>;
57
58 fn decrypt_chunk(
61 &self,
62 urn: &DigUrn,
63 salt: Option<&SecretSalt>,
64 chunk: &[u8],
65 ) -> Option<Vec<u8>>;
66}
67
68pub fn resource_leaf(ciphertext: &[u8]) -> Bytes32 {
71 let mut hasher = Sha256::new();
72 hasher.update(ciphertext);
73 Bytes32(hasher.finalize().into())
74}
75
76pub fn require_blind_root(urn: &DigUrn) -> Result<Bytes32> {
81 urn.root_hash.ok_or(ResolveError::RootRequired)
82}
83
84pub fn verify_inclusion<C: ContentCrypto>(
88 crypto: &C,
89 ciphertext: &[u8],
90 proof: &[u8],
91 trusted_root: &Bytes32,
92) -> Result<()> {
93 let folded = crypto.decode_and_fold(proof).ok_or_else(|| {
94 ResolveError::VerifyFailed("inclusion proof is malformed or inconsistent".into())
95 })?;
96
97 if folded.leaf != resource_leaf(ciphertext) {
98 return Err(ResolveError::VerifyFailed(
99 "content does not match proof leaf (tampered ciphertext)".into(),
100 ));
101 }
102 if &folded.root != trusted_root {
103 return Err(ResolveError::VerifyFailed(
104 "merkle root does not match the chain-anchored trusted root".into(),
105 ));
106 }
107 Ok(())
108}
109
110pub fn chunk_ranges(ciphertext_len: usize, chunk_lens: &[u32]) -> Result<Vec<(usize, usize)>> {
118 let ct_len = ciphertext_len as u64;
119 let plan: Vec<u64> = if chunk_lens.is_empty() {
120 vec![ct_len]
121 } else {
122 chunk_lens.iter().map(|&l| l as u64).collect()
123 };
124
125 let mut total: u64 = 0;
126 for &len in &plan {
127 total = total.checked_add(len).ok_or(ResolveError::DecryptFailed)?;
128 }
129 if total != ct_len {
130 return Err(ResolveError::DecryptFailed);
131 }
132
133 let mut ranges = Vec::with_capacity(plan.len());
134 let mut start: usize = 0;
135 for len in plan {
136 let len = usize::try_from(len).map_err(|_| ResolveError::DecryptFailed)?;
137 let end = start
138 .checked_add(len)
139 .filter(|&e| e <= ciphertext_len)
140 .ok_or(ResolveError::DecryptFailed)?;
141 ranges.push((start, end));
142 start = end;
143 }
144 Ok(ranges)
145}
146
147pub fn decrypt<C: ContentCrypto>(
151 crypto: &C,
152 urn: &DigUrn,
153 salt: Option<&SecretSalt>,
154 ciphertext: &[u8],
155 chunk_lens: &[u32],
156) -> Result<Vec<u8>> {
157 let mut plaintext = Vec::with_capacity(ciphertext.len());
158 for (start, end) in chunk_ranges(ciphertext.len(), chunk_lens)? {
159 let chunk = &ciphertext[start..end];
160 let pt = crypto
161 .decrypt_chunk(urn, salt, chunk)
162 .ok_or(ResolveError::DecryptFailed)?;
163 plaintext.extend_from_slice(&pt);
164 }
165 Ok(plaintext)
166}
167
168pub fn verify_and_decrypt<C: ContentCrypto>(
172 crypto: &C,
173 urn: &DigUrn,
174 salt: Option<&SecretSalt>,
175 ciphertext: &[u8],
176 proof: &[u8],
177 trusted_root: &Bytes32,
178 chunk_lens: &[u32],
179) -> Result<Vec<u8>> {
180 verify_inclusion(crypto, ciphertext, proof, trusted_root)?;
181 decrypt(crypto, urn, salt, ciphertext, chunk_lens)
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 struct FakeCrypto;
190 impl ContentCrypto for FakeCrypto {
191 fn decode_and_fold(&self, proof: &[u8]) -> Option<FoldedProof> {
192 if proof.len() != 64 {
193 return None;
194 }
195 let mut leaf = [0u8; 32];
196 let mut root = [0u8; 32];
197 leaf.copy_from_slice(&proof[..32]);
198 root.copy_from_slice(&proof[32..]);
199 Some(FoldedProof {
200 leaf: Bytes32(leaf),
201 root: Bytes32(root),
202 })
203 }
204 fn decrypt_chunk(
205 &self,
206 _urn: &DigUrn,
207 _salt: Option<&SecretSalt>,
208 chunk: &[u8],
209 ) -> Option<Vec<u8>> {
210 Some(chunk.iter().map(|b| b ^ 0xAA).collect())
211 }
212 }
213
214 fn urn() -> DigUrn {
215 DigUrn::parse(&format!("urn:dig:chia:{}/a.bin", "11".repeat(32))).unwrap()
216 }
217
218 fn valid_proof(ciphertext: &[u8], root: &Bytes32) -> Vec<u8> {
219 let mut p = resource_leaf(ciphertext).0.to_vec();
220 p.extend_from_slice(&root.0);
221 p
222 }
223
224 #[test]
225 fn require_blind_root_rejects_rootless() {
226 assert_eq!(require_blind_root(&urn()), Err(ResolveError::RootRequired));
227 let rooted = DigUrn::parse(&format!(
228 "urn:dig:chia:{}:{}/a",
229 "11".repeat(32),
230 "22".repeat(32)
231 ))
232 .unwrap();
233 assert!(require_blind_root(&rooted).is_ok());
234 }
235
236 #[test]
237 fn verify_and_decrypt_happy_path() {
238 let ct = vec![0x01u8; 8];
239 let root = Bytes32([0x33u8; 32]);
240 let proof = valid_proof(&ct, &root);
241 let out = verify_and_decrypt(&FakeCrypto, &urn(), None, &ct, &proof, &root, &[]).unwrap();
242 assert_eq!(out, vec![0x01 ^ 0xAA; 8]);
243 }
244
245 #[test]
246 fn tampered_ciphertext_fails_leaf_binding() {
247 let ct = vec![0x01u8; 8];
248 let root = Bytes32([0x33u8; 32]);
249 let proof = valid_proof(&ct, &root);
250 let tampered = vec![0x02u8; 8];
251 assert!(matches!(
252 verify_inclusion(&FakeCrypto, &tampered, &proof, &root),
253 Err(ResolveError::VerifyFailed(_))
254 ));
255 }
256
257 #[test]
258 fn wrong_trusted_root_fails_anchoring() {
259 let ct = vec![0x01u8; 8];
260 let proof = valid_proof(&ct, &Bytes32([0x33u8; 32]));
261 let other_root = Bytes32([0x44u8; 32]);
262 assert!(matches!(
263 verify_inclusion(&FakeCrypto, &ct, &proof, &other_root),
264 Err(ResolveError::VerifyFailed(_))
265 ));
266 }
267
268 #[test]
269 fn malformed_proof_fails_closed() {
270 let ct = vec![0x01u8; 8];
271 assert!(matches!(
272 verify_inclusion(&FakeCrypto, &ct, &[0u8; 10], &Bytes32([0x33u8; 32])),
273 Err(ResolveError::VerifyFailed(_))
274 ));
275 }
276
277 #[test]
278 fn chunk_ranges_empty_is_single_chunk() {
279 assert_eq!(chunk_ranges(10, &[]).unwrap(), vec![(0, 10)]);
280 }
281
282 #[test]
283 fn chunk_ranges_splits_in_order() {
284 assert_eq!(chunk_ranges(10, &[3, 7]).unwrap(), vec![(0, 3), (3, 10)]);
285 }
286
287 #[test]
288 fn chunk_ranges_rejects_total_mismatch() {
289 assert_eq!(chunk_ranges(10, &[999]), Err(ResolveError::DecryptFailed));
290 }
291
292 #[test]
293 fn chunk_ranges_rejects_overflow_without_panic() {
294 let bad = [(1u32 << 31) + 10, 1u32 << 31];
296 assert_eq!(chunk_ranges(10, &bad), Err(ResolveError::DecryptFailed));
297 }
298
299 #[test]
300 fn decrypt_maps_tag_failure_to_decryptfailed() {
301 struct AlwaysFail;
302 impl ContentCrypto for AlwaysFail {
303 fn decode_and_fold(&self, _p: &[u8]) -> Option<FoldedProof> {
304 None
305 }
306 fn decrypt_chunk(
307 &self,
308 _u: &DigUrn,
309 _s: Option<&SecretSalt>,
310 _c: &[u8],
311 ) -> Option<Vec<u8>> {
312 None
313 }
314 }
315 assert_eq!(
316 decrypt(&AlwaysFail, &urn(), None, &[0u8; 4], &[]),
317 Err(ResolveError::DecryptFailed)
318 );
319 }
320}