use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
pub(crate) fn compute_chain_hash(prev_hash: &[u8; 32], entry_json: &str) -> [u8; 32] {
#[allow(clippy::unwrap_used)]
let mut mac = HmacSha256::new_from_slice(prev_hash)
.expect("HMAC-SHA256 accepts any non-empty key length");
mac.update(entry_json.as_bytes());
mac.finalize().into_bytes().into()
}
pub(crate) fn encode_chain_hash(hash: &[u8; 32]) -> String {
hex::encode(hash)
}
pub struct ChainHasher {
current: [u8; 32],
}
impl ChainHasher {
#[must_use]
pub const fn new(seed: [u8; 32]) -> Self {
Self { current: seed }
}
pub fn advance(&mut self, entry_json: &str) -> String {
self.current = compute_chain_hash(&self.current, entry_json);
encode_chain_hash(&self.current)
}
#[must_use]
pub fn current_hash(&self) -> String {
encode_chain_hash(&self.current)
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ChainVerifyError {
#[error("Chain broken at entry {entry_index}: expected {expected_hash}, got {stored_hash}")]
BrokenLink {
entry_index: usize,
expected_hash: String,
stored_hash: String,
},
#[error("Entry {entry_index} is missing or has an invalid `chain_hash` field")]
MissingChainHash {
entry_index: usize,
},
#[error("Entry {entry_index} is not a JSON object")]
InvalidEntry {
entry_index: usize,
},
}
pub fn verify_chain(
entries: impl IntoIterator<Item = serde_json::Value>,
seed: [u8; 32],
) -> Result<usize, ChainVerifyError> {
let mut hasher = ChainHasher::new(seed);
let mut count = 0usize;
for (idx, entry) in entries.into_iter().enumerate() {
let stored_hash = entry
.get("chain_hash")
.and_then(|v| v.as_str())
.ok_or(ChainVerifyError::MissingChainHash { entry_index: idx })?
.to_string();
let mut entry_for_hash = entry;
entry_for_hash
.as_object_mut()
.ok_or(ChainVerifyError::InvalidEntry { entry_index: idx })?
.remove("chain_hash");
let entry_json = entry_for_hash.to_string();
let expected_hash = hasher.advance(&entry_json);
if expected_hash != stored_hash {
return Err(ChainVerifyError::BrokenLink {
entry_index: idx,
expected_hash,
stored_hash,
});
}
count += 1;
}
Ok(count)
}