use crate::error::{ResolveError, Result};
use crate::urn::ParsedUrn;
use base64::Engine;
use dig_urn_protocol::verify::{self, ContentCrypto, FoldedProof};
use dig_urn_protocol::{Bytes32, DigUrn, SecretSalt};
use digstore_core::codec::Decode;
use digstore_core::crypto::{decrypt_chunk, derive_decryption_key};
use digstore_core::MerkleProof;
struct DigstoreCrypto;
impl ContentCrypto for DigstoreCrypto {
fn decode_and_fold(&self, proof: &[u8]) -> Option<FoldedProof> {
let proof = MerkleProof::from_bytes(proof).ok()?;
if !proof.verify() {
return None;
}
Some(FoldedProof {
leaf: Bytes32(proof.leaf.0),
root: Bytes32(proof.root.0),
})
}
fn decrypt_chunk(
&self,
urn: &DigUrn,
salt: Option<&SecretSalt>,
chunk: &[u8],
) -> Option<Vec<u8>> {
let canonical = urn.canonical_rootless().canonical();
let core_salt = salt.map(|s| digstore_core::SecretSalt(s.0));
let key = derive_decryption_key(&canonical, core_salt.as_ref());
decrypt_chunk(&key, chunk).ok()
}
}
fn parse_salt(salt_hex: Option<&str>) -> Result<Option<SecretSalt>> {
match salt_hex {
None => Ok(None),
Some(s) if s.trim().is_empty() => Ok(None),
Some(s) => DigUrn::salt_bytes(s)
.map(Some)
.map_err(|_| ResolveError::Parse("secret salt must be 64 hex chars".into())),
}
}
fn parse_trusted_root(trusted_root_hex: &str) -> Result<Bytes32> {
Bytes32::from_hex(trusted_root_hex.trim())
.map_err(|_| ResolveError::VerifyFailed("trusted root must be 64 hex chars".into()))
}
fn decode_proof_b64(proof_b64: &str) -> Result<Vec<u8>> {
base64::engine::general_purpose::STANDARD
.decode(proof_b64.trim().as_bytes())
.map_err(|_| ResolveError::VerifyFailed("inclusion proof is not valid base64".into()))
}
fn map_err(err: dig_urn_protocol::ResolveError) -> ResolveError {
use dig_urn_protocol::ResolveError as P;
match err {
P::Parse(m) => ResolveError::Parse(m),
P::Transport(m) => ResolveError::Transport(m),
P::Rpc(m) => ResolveError::Rpc(m),
P::NotFound => ResolveError::NotFound,
P::RootRequired => ResolveError::RootRequired,
P::VerifyFailed(m) => ResolveError::VerifyFailed(m),
P::DecryptFailed => ResolveError::DecryptFailed,
}
}
pub fn verify_inclusion(ciphertext: &[u8], proof_b64: &str, trusted_root_hex: &str) -> Result<()> {
let trusted_root = parse_trusted_root(trusted_root_hex)?;
let proof = decode_proof_b64(proof_b64)?;
verify::verify_inclusion(&DigstoreCrypto, ciphertext, &proof, &trusted_root).map_err(map_err)
}
pub fn decrypt(parsed: &ParsedUrn, ciphertext: &[u8], chunk_lens: &[u32]) -> Result<Vec<u8>> {
let salt = parse_salt(parsed.salt.as_deref())?;
verify::decrypt(
&DigstoreCrypto,
&parsed.urn,
salt.as_ref(),
ciphertext,
chunk_lens,
)
.map_err(map_err)
}
pub fn verify_and_decrypt(
parsed: &ParsedUrn,
ciphertext: &[u8],
proof_b64: &str,
trusted_root_hex: &str,
chunk_lens: &[u32],
) -> Result<Vec<u8>> {
let salt = parse_salt(parsed.salt.as_deref())?;
let trusted_root = parse_trusted_root(trusted_root_hex)?;
let proof = decode_proof_b64(proof_b64)?;
verify::verify_and_decrypt(
&DigstoreCrypto,
&parsed.urn,
salt.as_ref(),
ciphertext,
&proof,
&trusted_root,
chunk_lens,
)
.map_err(map_err)
}
#[cfg(test)]
mod tests {
use super::*;
fn urn() -> ParsedUrn {
ParsedUrn::parse(&format!("urn:dig:chia:{}/a.bin", "ab".repeat(32))).unwrap()
}
#[test]
fn decrypt_rejects_overflowing_chunk_lens_without_panic() {
let ct = vec![0u8; 10];
let bad = [(1u32 << 31) + 10, 1u32 << 31];
assert!(matches!(
decrypt(&urn(), &ct, &bad),
Err(ResolveError::DecryptFailed)
));
}
#[test]
fn decrypt_rejects_chunk_total_mismatch() {
let ct = vec![0u8; 10];
assert!(matches!(
decrypt(&urn(), &ct, &[999]),
Err(ResolveError::DecryptFailed)
));
}
#[test]
fn verify_inclusion_rejects_invalid_base64_proof() {
let err = verify_inclusion(&[0u8; 4], "not base64!!", &"11".repeat(32)).unwrap_err();
assert!(matches!(err, ResolveError::VerifyFailed(_)));
}
#[test]
fn verify_inclusion_rejects_bad_trusted_root() {
let err = verify_inclusion(&[0u8; 4], "AAAA", "not-hex").unwrap_err();
assert!(matches!(err, ResolveError::VerifyFailed(_)));
}
}