use crate::bytes::Bytes32;
use crate::resolve::{ResolveError, Result};
use crate::urn::{DigUrn, SecretSalt};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FoldedProof {
pub leaf: Bytes32,
pub root: Bytes32,
}
pub trait ContentCrypto {
fn decode_and_fold(&self, proof: &[u8]) -> Option<FoldedProof>;
fn decrypt_chunk(
&self,
urn: &DigUrn,
salt: Option<&SecretSalt>,
chunk: &[u8],
) -> Option<Vec<u8>>;
}
pub fn resource_leaf(ciphertext: &[u8]) -> Bytes32 {
let mut hasher = Sha256::new();
hasher.update(ciphertext);
Bytes32(hasher.finalize().into())
}
pub fn require_blind_root(urn: &DigUrn) -> Result<Bytes32> {
urn.root_hash.ok_or(ResolveError::RootRequired)
}
pub fn verify_inclusion<C: ContentCrypto>(
crypto: &C,
ciphertext: &[u8],
proof: &[u8],
trusted_root: &Bytes32,
) -> Result<()> {
let folded = crypto.decode_and_fold(proof).ok_or_else(|| {
ResolveError::VerifyFailed("inclusion proof is malformed or inconsistent".into())
})?;
if folded.leaf != resource_leaf(ciphertext) {
return Err(ResolveError::VerifyFailed(
"content does not match proof leaf (tampered ciphertext)".into(),
));
}
if &folded.root != trusted_root {
return Err(ResolveError::VerifyFailed(
"merkle root does not match the chain-anchored trusted root".into(),
));
}
Ok(())
}
pub fn chunk_ranges(ciphertext_len: usize, chunk_lens: &[u32]) -> Result<Vec<(usize, usize)>> {
let ct_len = ciphertext_len as u64;
let plan: Vec<u64> = if chunk_lens.is_empty() {
vec![ct_len]
} else {
chunk_lens.iter().map(|&l| l as u64).collect()
};
let mut total: u64 = 0;
for &len in &plan {
total = total.checked_add(len).ok_or(ResolveError::DecryptFailed)?;
}
if total != ct_len {
return Err(ResolveError::DecryptFailed);
}
let mut ranges = Vec::with_capacity(plan.len());
let mut start: usize = 0;
for len in plan {
let len = usize::try_from(len).map_err(|_| ResolveError::DecryptFailed)?;
let end = start
.checked_add(len)
.filter(|&e| e <= ciphertext_len)
.ok_or(ResolveError::DecryptFailed)?;
ranges.push((start, end));
start = end;
}
Ok(ranges)
}
pub fn decrypt<C: ContentCrypto>(
crypto: &C,
urn: &DigUrn,
salt: Option<&SecretSalt>,
ciphertext: &[u8],
chunk_lens: &[u32],
) -> Result<Vec<u8>> {
let mut plaintext = Vec::with_capacity(ciphertext.len());
for (start, end) in chunk_ranges(ciphertext.len(), chunk_lens)? {
let chunk = &ciphertext[start..end];
let pt = crypto
.decrypt_chunk(urn, salt, chunk)
.ok_or(ResolveError::DecryptFailed)?;
plaintext.extend_from_slice(&pt);
}
Ok(plaintext)
}
pub fn verify_and_decrypt<C: ContentCrypto>(
crypto: &C,
urn: &DigUrn,
salt: Option<&SecretSalt>,
ciphertext: &[u8],
proof: &[u8],
trusted_root: &Bytes32,
chunk_lens: &[u32],
) -> Result<Vec<u8>> {
verify_inclusion(crypto, ciphertext, proof, trusted_root)?;
decrypt(crypto, urn, salt, ciphertext, chunk_lens)
}
#[cfg(test)]
mod tests {
use super::*;
struct FakeCrypto;
impl ContentCrypto for FakeCrypto {
fn decode_and_fold(&self, proof: &[u8]) -> Option<FoldedProof> {
if proof.len() != 64 {
return None;
}
let mut leaf = [0u8; 32];
let mut root = [0u8; 32];
leaf.copy_from_slice(&proof[..32]);
root.copy_from_slice(&proof[32..]);
Some(FoldedProof {
leaf: Bytes32(leaf),
root: Bytes32(root),
})
}
fn decrypt_chunk(
&self,
_urn: &DigUrn,
_salt: Option<&SecretSalt>,
chunk: &[u8],
) -> Option<Vec<u8>> {
Some(chunk.iter().map(|b| b ^ 0xAA).collect())
}
}
fn urn() -> DigUrn {
DigUrn::parse(&format!("urn:dig:chia:{}/a.bin", "11".repeat(32))).unwrap()
}
fn valid_proof(ciphertext: &[u8], root: &Bytes32) -> Vec<u8> {
let mut p = resource_leaf(ciphertext).0.to_vec();
p.extend_from_slice(&root.0);
p
}
#[test]
fn require_blind_root_rejects_rootless() {
assert_eq!(require_blind_root(&urn()), Err(ResolveError::RootRequired));
let rooted = DigUrn::parse(&format!(
"urn:dig:chia:{}:{}/a",
"11".repeat(32),
"22".repeat(32)
))
.unwrap();
assert!(require_blind_root(&rooted).is_ok());
}
#[test]
fn verify_and_decrypt_happy_path() {
let ct = vec![0x01u8; 8];
let root = Bytes32([0x33u8; 32]);
let proof = valid_proof(&ct, &root);
let out = verify_and_decrypt(&FakeCrypto, &urn(), None, &ct, &proof, &root, &[]).unwrap();
assert_eq!(out, vec![0x01 ^ 0xAA; 8]);
}
#[test]
fn tampered_ciphertext_fails_leaf_binding() {
let ct = vec![0x01u8; 8];
let root = Bytes32([0x33u8; 32]);
let proof = valid_proof(&ct, &root);
let tampered = vec![0x02u8; 8];
assert!(matches!(
verify_inclusion(&FakeCrypto, &tampered, &proof, &root),
Err(ResolveError::VerifyFailed(_))
));
}
#[test]
fn wrong_trusted_root_fails_anchoring() {
let ct = vec![0x01u8; 8];
let proof = valid_proof(&ct, &Bytes32([0x33u8; 32]));
let other_root = Bytes32([0x44u8; 32]);
assert!(matches!(
verify_inclusion(&FakeCrypto, &ct, &proof, &other_root),
Err(ResolveError::VerifyFailed(_))
));
}
#[test]
fn malformed_proof_fails_closed() {
let ct = vec![0x01u8; 8];
assert!(matches!(
verify_inclusion(&FakeCrypto, &ct, &[0u8; 10], &Bytes32([0x33u8; 32])),
Err(ResolveError::VerifyFailed(_))
));
}
#[test]
fn chunk_ranges_empty_is_single_chunk() {
assert_eq!(chunk_ranges(10, &[]).unwrap(), vec![(0, 10)]);
}
#[test]
fn chunk_ranges_splits_in_order() {
assert_eq!(chunk_ranges(10, &[3, 7]).unwrap(), vec![(0, 3), (3, 10)]);
}
#[test]
fn chunk_ranges_rejects_total_mismatch() {
assert_eq!(chunk_ranges(10, &[999]), Err(ResolveError::DecryptFailed));
}
#[test]
fn chunk_ranges_rejects_overflow_without_panic() {
let bad = [(1u32 << 31) + 10, 1u32 << 31];
assert_eq!(chunk_ranges(10, &bad), Err(ResolveError::DecryptFailed));
}
#[test]
fn decrypt_maps_tag_failure_to_decryptfailed() {
struct AlwaysFail;
impl ContentCrypto for AlwaysFail {
fn decode_and_fold(&self, _p: &[u8]) -> Option<FoldedProof> {
None
}
fn decrypt_chunk(
&self,
_u: &DigUrn,
_s: Option<&SecretSalt>,
_c: &[u8],
) -> Option<Vec<u8>> {
None
}
}
assert_eq!(
decrypt(&AlwaysFail, &urn(), None, &[0u8; 4], &[]),
Err(ResolveError::DecryptFailed)
);
}
}