use crate::error::PrivacyError;
use crate::genomic::{sequence_to_snp_commitments, SnpCommitment};
use crate::types::PrivacyProof;
use crate::zk::snark;
use sha2::{Digest, Sha256};
use serde::{Deserialize, Serialize};
extern crate alloc;
use alloc::{format, string::{String, ToString}, vec::Vec};
pub const SNP_MATCH_THRESHOLD: u32 = 98;
pub const SNP_TOTAL: u32 = 100;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BiometricSession {
pub session_id: String,
pub wyqcc_did: String,
pub match_proof: PrivacyProof,
pub created_ms: u64,
pub expiry_ms: u64,
}
#[derive(Debug, Clone)]
struct GenomicProfile {
commitments: Vec<SnpCommitment>,
}
pub struct QtaidLoginEngine {
profiles: alloc::collections::BTreeMap<String, GenomicProfile>,
}
impl QtaidLoginEngine {
pub fn new() -> Self {
Self { profiles: alloc::collections::BTreeMap::new() }
}
pub fn register(
&mut self,
did: impl Into<String>,
sequence: &str,
dp_epsilon: f64,
chaos_seed: &[u8; 32],
_expiry_ms: u64,
) -> Result<(), PrivacyError> {
let did = did.into();
let patient_id = did.as_bytes();
let commitments = sequence_to_snp_commitments(sequence, chaos_seed, patient_id, dp_epsilon)?;
self.profiles.insert(did, GenomicProfile { commitments });
Ok(())
}
pub fn authenticate(
&self,
did: &str,
challenge_sequence: &str,
chaos_seed: &[u8; 32],
now_ms: u64,
session_expiry_ms: u64,
) -> Result<BiometricSession, PrivacyError> {
let profile = self.profiles.get(did)
.ok_or_else(|| PrivacyError::TokenMintFailed(format!("No template for DID: {did}")))?;
let patient_id = did.as_bytes();
let challenge_commitments = sequence_to_snp_commitments(
challenge_sequence,
chaos_seed,
patient_id,
1e-6,
)?;
let match_count = self.count_snp_matches(&profile.commitments, &challenge_commitments)?;
let total = profile.commitments.len().min(challenge_commitments.len()) as u32;
let matched = match_count as u32;
let required = (total * 98 / 100).max(1);
if matched < required {
return Err(PrivacyError::SnpMatchFailed {
matched,
total,
threshold: SNP_MATCH_THRESHOLD,
});
}
let proof = self.prove_snp_match(matched, total, chaos_seed)?;
let mut id_hasher = Sha256::new();
id_hasher.update(did.as_bytes());
id_hasher.update(now_ms.to_le_bytes());
id_hasher.update(chaos_seed);
let session_id = hex::encode(id_hasher.finalize())[..16].to_string();
Ok(BiometricSession {
session_id,
wyqcc_did: did.to_string(),
match_proof: proof,
created_ms: now_ms,
expiry_ms: now_ms + session_expiry_ms,
})
}
pub fn revoke(&mut self, did: &str) -> Result<(), PrivacyError> {
self.profiles.remove(did)
.ok_or_else(|| PrivacyError::RevocationFailed(format!("DID not found: {did}")))?;
Ok(())
}
fn count_snp_matches(
&self,
registered: &[SnpCommitment],
authenticating: &[SnpCommitment],
) -> Result<usize, PrivacyError> {
let mut matches = 0usize;
for (reg, auth) in registered.iter().zip(authenticating.iter()) {
if !auth.verify() {
continue;
}
if reg.allele_bits == auth.allele_bits {
matches += 1;
}
}
Ok(matches)
}
fn prove_snp_match(
&self,
matched: u32,
total: u32,
chaos_seed: &[u8; 32],
) -> Result<PrivacyProof, PrivacyError> {
let mut stmt_hasher = Sha256::new();
stmt_hasher.update(b"snp-match-v1");
stmt_hasher.update(matched.to_le_bytes());
stmt_hasher.update(total.to_le_bytes());
let statement_hash: [u8; 32] = stmt_hasher.finalize().into();
let mut wit_hasher = Sha256::new();
wit_hasher.update(matched.to_le_bytes());
wit_hasher.update(total.to_le_bytes());
wit_hasher.update(chaos_seed);
wit_hasher.update(b"snp-witness-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))
}
}
impl Default for QtaidLoginEngine {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_register_authenticate() {
let mut engine = QtaidLoginEngine::new();
let seed = [0u8; 32];
let seq = "ACGTACGT";
engine.register("did:wyqcc:alice", seq, 1e-6, &seed, 9_999_999_999).unwrap();
let session = engine.authenticate("did:wyqcc:alice", seq, &seed, 0, 3_600_000).unwrap();
assert_eq!(session.wyqcc_did, "did:wyqcc:alice");
}
#[test]
fn test_revoke() {
let mut engine = QtaidLoginEngine::new();
let seed = [0u8; 32];
engine.register("did:wyqcc:bob", "ACGT", 1e-6, &seed, 0).unwrap();
engine.revoke("did:wyqcc:bob").unwrap();
assert!(engine.authenticate("did:wyqcc:bob", "ACGT", &seed, 0, 0).is_err());
}
#[test]
fn test_wrong_sequence_fails() {
let mut engine = QtaidLoginEngine::new();
let seed = [0u8; 32];
let registered = "A".repeat(100);
engine.register("did:wyqcc:carol", ®istered, 1e-6, &seed, 0).unwrap();
let challenge = "T".repeat(100);
let result = engine.authenticate("did:wyqcc:carol", &challenge, &seed, 0, 0);
assert!(result.is_err());
}
#[test]
fn test_high_match_succeeds() {
let mut engine = QtaidLoginEngine::new();
let seed = [1u8; 32];
let seq: String = "ACGT".repeat(25); engine.register("did:wyqcc:dave", &seq, 1e-6, &seed, 0).unwrap();
let result = engine.authenticate("did:wyqcc:dave", &seq, &seed, 0, 0);
assert!(result.is_ok());
}
}