pub mod login;
use crate::error::PrivacyError;
use crate::keyhop::hkdf_derive;
use crate::types::{GenomicToken, PrivacyProof};
use crate::zk::snark;
use sha2::{Digest, Sha256};
use serde::{Deserialize, Serialize};
extern crate alloc;
use alloc::{string::{String, ToString}, vec::Vec};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Allele {
A = 0b00,
C = 0b01,
G = 0b10,
T = 0b11,
}
impl Allele {
pub fn from_char(c: char) -> Result<Self, PrivacyError> {
match c {
'A' | 'a' => Ok(Allele::A),
'C' | 'c' => Ok(Allele::C),
'G' | 'g' => Ok(Allele::G),
'T' | 't' => Ok(Allele::T),
_ => Err(PrivacyError::InvalidAllele(c.to_string())),
}
}
pub fn to_bits(self) -> u8 {
self as u8
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnpCommitment {
pub commitment: String,
pub allele_bits: u8,
pub blinding_factor: String,
pub snp_position: u32,
pub dp_noise: f64,
}
impl SnpCommitment {
pub fn verify(&self) -> bool {
let Ok(bf) = hex::decode(&self.blinding_factor) else { return false; };
let mut hasher = Sha256::new();
hasher.update([self.allele_bits]);
hasher.update(&bf);
hasher.update(b"snp-commit-v1");
let expected = hex::encode(hasher.finalize());
expected == self.commitment
}
}
fn derive_blinding_factor(
chaos_seed: &[u8; 32],
patient_id: &[u8],
snp_position: u32,
) -> Result<[u8; 32], PrivacyError> {
let mut salt = Vec::with_capacity(patient_id.len() + 4);
salt.extend_from_slice(patient_id);
salt.extend_from_slice(&snp_position.to_le_bytes());
let mut out = [0u8; 32];
hkdf_derive(chaos_seed, &salt, b"genomic-blind-v1", &mut out)?;
Ok(out)
}
fn laplace_noise(chaos_seed: &[u8; 32], position: usize, dp_epsilon: f64) -> f64 {
let mut hasher = Sha256::new();
hasher.update(chaos_seed);
hasher.update((position as u64).to_le_bytes());
hasher.update(b"dp-laplace-v1");
let h: [u8; 32] = hasher.finalize().into();
let raw = u64::from_le_bytes(h[..8].try_into().unwrap_or([0u8; 8]));
let uniform = (raw as f64) / (u64::MAX as f64); let b = 1.0 / dp_epsilon.max(1e-12);
let u = uniform - 0.5;
let sign = if u >= 0.0 { 1.0 } else { -1.0 };
let abs_u = u.abs().min(0.4999);
-b * sign * (1.0 - 2.0 * abs_u).ln()
}
pub fn nano_tokenize(
sequence: &str,
dp_epsilon: f64,
chaos_seed: &[u8; 32],
expiry_ms: u64,
) -> Result<Vec<GenomicToken>, PrivacyError> {
nano_tokenize_with_id(sequence, dp_epsilon, chaos_seed, expiry_ms, b"default-patient")
}
pub fn nano_tokenize_with_id(
sequence: &str,
dp_epsilon: f64,
chaos_seed: &[u8; 32],
_expiry_ms: u64,
patient_id: &[u8],
) -> Result<Vec<GenomicToken>, PrivacyError> {
let mut tokens = Vec::new();
for (i, ch) in sequence.chars().enumerate() {
let allele = Allele::from_char(ch)?;
let bits = allele.to_bits();
let snp_position = i as u32;
let blinding_factor = derive_blinding_factor(chaos_seed, patient_id, snp_position)?;
let mut hasher = Sha256::new();
hasher.update([bits]);
hasher.update(blinding_factor);
hasher.update(b"snp-commit-v1");
let commitment: [u8; 32] = hasher.finalize().into();
let dp_noise = laplace_noise(chaos_seed, i, dp_epsilon);
let trait_proof = prove_allele_trait(bits, &blinding_factor, chaos_seed)?;
let tuple_id = hex::encode(&commitment[..8]);
tokens.push(GenomicToken {
commitment: hex::encode(commitment),
trait_proof,
tuple_id,
dp_epsilon,
});
let _ = dp_noise;
}
Ok(tokens)
}
pub fn make_snp_commitment(
allele_char: char,
snp_position: u32,
chaos_seed: &[u8; 32],
patient_id: &[u8],
dp_epsilon: f64,
) -> Result<SnpCommitment, PrivacyError> {
let allele = Allele::from_char(allele_char)?;
let bits = allele.to_bits();
let blinding_factor = derive_blinding_factor(chaos_seed, patient_id, snp_position)?;
let mut hasher = Sha256::new();
hasher.update([bits]);
hasher.update(blinding_factor);
hasher.update(b"snp-commit-v1");
let commitment: [u8; 32] = hasher.finalize().into();
let dp_noise = laplace_noise(chaos_seed, snp_position as usize, dp_epsilon);
Ok(SnpCommitment {
commitment: hex::encode(commitment),
allele_bits: bits,
blinding_factor: hex::encode(blinding_factor),
snp_position,
dp_noise,
})
}
pub fn sequence_to_snp_commitments(
sequence: &str,
chaos_seed: &[u8; 32],
patient_id: &[u8],
dp_epsilon: f64,
) -> Result<Vec<SnpCommitment>, PrivacyError> {
sequence.chars().enumerate().map(|(i, ch)| {
make_snp_commitment(ch, i as u32, chaos_seed, patient_id, dp_epsilon)
}).collect()
}
pub fn prove_allele_trait(
allele_bits: u8,
blinding_factor: &[u8],
chaos_seed: &[u8; 32],
) -> Result<PrivacyProof, PrivacyError> {
let mut stmt_hasher = Sha256::new();
stmt_hasher.update(b"allele-trait-v1");
stmt_hasher.update([allele_bits]);
let statement_hash: [u8; 32] = stmt_hasher.finalize().into();
let mut wit_hasher = Sha256::new();
wit_hasher.update([allele_bits]);
wit_hasher.update(blinding_factor);
wit_hasher.update(b"snp-commit-v1");
let witness_hash: [u8; 32] = wit_hasher.finalize().into();
let snark_proof = snark::prove(statement_hash, witness_hash, chaos_seed)?;
Ok(PrivacyProof::from(snark_proof))
}
pub fn verify_token(token: &GenomicToken) -> Result<(), PrivacyError> {
if token.commitment.is_empty() || token.trait_proof.proof_bytes.is_empty() {
return Err(PrivacyError::ProofVerificationFailed);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nano_tokenize() {
let seed = [0u8; 32];
let tokens = nano_tokenize("ACGT", 1e-6, &seed, 9_999_999_999).unwrap();
assert_eq!(tokens.len(), 4);
for t in &tokens {
assert!(!t.commitment.is_empty());
}
}
#[test]
fn test_invalid_allele() {
let seed = [0u8; 32];
assert!(nano_tokenize("ACGX", 1e-6, &seed, 0).is_err());
}
#[test]
fn test_allele_encoding() {
assert_eq!(Allele::A.to_bits(), 0b00);
assert_eq!(Allele::T.to_bits(), 0b11);
}
#[test]
fn test_snp_commitment_verify() {
let seed = [42u8; 32];
let c = make_snp_commitment('A', 0, &seed, b"patient-1", 1e-6).unwrap();
assert!(c.verify());
}
#[test]
fn test_same_allele_same_commitment() {
let seed = [7u8; 32];
let c1 = make_snp_commitment('G', 5, &seed, b"patient-x", 1e-6).unwrap();
let c2 = make_snp_commitment('G', 5, &seed, b"patient-x", 1e-6).unwrap();
assert_eq!(c1.commitment, c2.commitment);
}
#[test]
fn test_different_allele_different_commitment() {
let seed = [7u8; 32];
let c1 = make_snp_commitment('A', 0, &seed, b"patient-x", 1e-6).unwrap();
let c2 = make_snp_commitment('T', 0, &seed, b"patient-x", 1e-6).unwrap();
assert_ne!(c1.commitment, c2.commitment);
assert_ne!(c1.allele_bits, c2.allele_bits);
}
#[test]
fn test_sequence_to_snp_commitments() {
let seed = [0u8; 32];
let commitments = sequence_to_snp_commitments("ACGT", &seed, b"patient-1", 1e-6).unwrap();
assert_eq!(commitments.len(), 4);
for c in &commitments {
assert!(c.verify());
}
}
}