use crate::error::PrivacyError;
use crate::types::{PrivacyProof, PrivacyTuple, ProofScheme};
use sha2::{Digest, Sha256};
use pqc_sig::fips204::MlDsa65Keypair;
use pqc_sig::types::{SigAlgorithm, SigPublicKey, Signature};
extern crate alloc;
use alloc::{string::String, vec::Vec};
pub fn build_tuple(
subject: impl Into<String>,
predicate: impl Into<String>,
object: Vec<u8>,
expiry_base_ms: u64,
dp_epsilon: f64,
chaos_seed: &[u8; 32],
) -> Result<PrivacyTuple, PrivacyError> {
let subject = subject.into();
let predicate = predicate.into();
let mut hasher = Sha256::new();
hasher.update(subject.as_bytes());
hasher.update(predicate.as_bytes());
hasher.update(&object);
hasher.update(chaos_seed);
hasher.update(b"plonk-tuple-v1");
let commitment: [u8; 32] = hasher.finalize().into();
let proof = PrivacyProof {
proof_bytes: hex::encode(commitment),
public_inputs: hex::encode(Sha256::digest(subject.as_bytes())),
scheme: ProofScheme::Snark,
security_bits: 128,
proof_size: 64,
chsh_value: 0.0,
lyapunov: 4.5,
};
let noise_scale = 1000.0 / dp_epsilon.max(1e-10); let seed_val = u64::from_le_bytes(chaos_seed[..8].try_into().unwrap_or([0u8; 8]));
let uniform = (seed_val as f64) / (u64::MAX as f64);
let laplace_noise = -noise_scale * (1.0 - uniform).ln();
let expiry_ms = expiry_base_ms.saturating_add(laplace_noise.abs() as u64);
Ok(PrivacyTuple {
subject,
predicate,
object,
proof,
expiry_ms,
anchor: None,
})
}
pub fn anchor_tuple(tuple: &mut PrivacyTuple) -> String {
let mut hasher = Sha256::new();
hasher.update(tuple.subject.as_bytes());
hasher.update(tuple.predicate.as_bytes());
hasher.update(&tuple.object);
hasher.update(tuple.expiry_ms.to_le_bytes());
hasher.update(b"wyqcc-l1-anchor");
let commitment: [u8; 32] = hasher.finalize().into();
let anchor = match MlDsa65Keypair::from_secret_key_bytes(&commitment) {
Ok(keypair) => {
match keypair.sign_deterministic(&commitment) {
Ok(sig) => hex::encode(&sig.bytes),
Err(_) => {
hex::encode(commitment)
}
}
}
Err(_) => {
hex::encode(commitment)
}
};
tuple.anchor = Some(anchor.clone());
anchor
}
pub fn verify_anchor(tuple: &PrivacyTuple) -> bool {
let anchor_hex = match &tuple.anchor {
Some(a) => a,
None => return false,
};
let mut hasher = Sha256::new();
hasher.update(tuple.subject.as_bytes());
hasher.update(tuple.predicate.as_bytes());
hasher.update(&tuple.object);
hasher.update(tuple.expiry_ms.to_le_bytes());
hasher.update(b"wyqcc-l1-anchor");
let commitment: [u8; 32] = hasher.finalize().into();
let keypair = match MlDsa65Keypair::from_secret_key_bytes(&commitment) {
Ok(kp) => kp,
Err(_) => return false,
};
let pk = keypair.public_key();
let sig_bytes = match hex::decode(anchor_hex) {
Ok(b) => b,
Err(_) => return false,
};
let pk_typed = SigPublicKey::new(SigAlgorithm::MlDsa65, pk.bytes);
let sig_typed = Signature::new(SigAlgorithm::MlDsa65, sig_bytes);
MlDsa65Keypair::verify(&pk_typed, &commitment, &sig_typed).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_tuple() {
let seed = [0u8; 32];
let t = build_tuple("alice", "owns", b"data".to_vec(), 1_000_000, 1e-6, &seed).unwrap();
assert_eq!(t.subject, "alice");
assert!(!t.proof.proof_bytes.is_empty());
assert!(t.expiry_ms >= 1_000_000);
}
#[test]
fn test_anchor() {
let seed = [1u8; 32];
let mut t = build_tuple("bob", "has", b"val".to_vec(), 2_000_000, 1e-6, &seed).unwrap();
let anchor = anchor_tuple(&mut t);
assert_eq!(anchor.len(), 3309 * 2);
assert!(t.anchor.is_some());
}
#[test]
fn test_verify_anchor() {
let seed = [2u8; 32];
let mut t = build_tuple("carol", "holds", b"secret".to_vec(), 3_000_000, 1e-6, &seed).unwrap();
anchor_tuple(&mut t);
assert!(verify_anchor(&t));
}
#[test]
fn test_verify_tampered_anchor_fails() {
let seed = [3u8; 32];
let mut t = build_tuple("dave", "owns", b"data".to_vec(), 4_000_000, 1e-6, &seed).unwrap();
anchor_tuple(&mut t);
t.object = b"tampered".to_vec();
assert!(!verify_anchor(&t));
}
}