Skip to main content

qualia_core_db/specialized_libs/statistical_computing/
privacy.rs

1use super::*;
2
3/// Noise mechanisms
4#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
5pub enum NoiseMechanism {
6    Laplace,
7    Gaussian,
8    Exponential,
9    Geometric,
10    Custom(String),
11}
12
13/// Privacy accountant
14pub struct PrivacyAccountant {
15    pub total_epsilon_spent: f64,
16    pub total_delta_spent: f64,
17    pub composition_method: CompositionMethod,
18    pub remaining_budget: PrivacyBudget,
19}
20
21/// Composition methods
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub enum CompositionMethod {
24    BasicComposition,
25    AdvancedComposition,
26    RDPComposition,
27    GaussianDP,
28    Custom(String),
29}
30
31/// Sensitivity function
32#[derive(Debug, Clone)]
33pub struct SensitivityFunction {
34    pub function_id: String,
35    pub sensitivity: f64,
36    pub computation_method: SensitivityMethod,
37}
38
39/// Sensitivity methods
40#[derive(Debug, Clone, PartialEq)]
41pub enum SensitivityMethod {
42    Global,
43    Local,
44    Smooth,
45    Approximate,
46}
47
48/// Secure aggregation
49pub struct SecureAggregation {
50    aggregation_protocols: Vec<AggregationProtocol>,
51    encryption_schemes: Vec<EncryptionScheme>,
52    integrity_checks: Vec<IntegrityCheck>,
53}
54
55/// Aggregation protocols
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub enum AggregationProtocol {
58    SecureSum,
59    SecureMean,
60    SecureMin,
61    SecureMax,
62    SecureMedian,
63    Custom(String),
64}
65
66/// Encryption schemes
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68pub enum EncryptionScheme {
69    Homomorphic,
70    SecretSharing,
71    Threshold,
72    Oblivious,
73    Custom(String),
74}
75
76/// Integrity checks
77#[derive(Debug, Clone)]
78pub struct IntegrityCheck {
79    pub check_id: String,
80    pub check_type: IntegrityCheckType,
81    pub verification_method: VerificationMethod,
82}
83
84/// Integrity check types
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86pub enum IntegrityCheckType {
87    Hash,
88    MAC,
89    DigitalSignature,
90    ZeroKnowledge,
91}
92
93/// Verification methods
94#[derive(Debug, Clone, PartialEq)]
95pub enum VerificationMethod {
96    Deterministic,
97    Probabilistic,
98    Interactive,
99    NonInteractive,
100}
101
102/// Privacy budget
103pub struct PrivacyBudget {
104    pub epsilon: f64,
105    pub delta: f64,
106    pub remaining_epsilon: f64,
107    pub remaining_delta: f64,
108    pub budget_period: u64,
109    pub last_reset: u64,
110}
111
112impl StatisticalPrivacyEngine {
113    pub fn new() -> Self {
114        Self {
115            fiduciary_crypto: Arc::new(Mutex::new(FiduciaryCrypto::new())),
116            zk_proofs: Arc::new(Mutex::new(ZkProofSystem::new())),
117            differential_privacy: DifferentialPrivacy::new(),
118            secure_aggregation: SecureAggregation::new(),
119            privacy_budget: PrivacyBudget {
120                epsilon: 1.0,
121                delta: 1e-6,
122                remaining_epsilon: 1.0,
123                remaining_delta: 1e-6,
124                budget_period: 86400, // 24 hours
125                last_reset: 0,
126            },
127        }
128    }
129
130    pub fn initialize(&mut self) -> Result<(), StatisticalError> {
131        self.differential_privacy.initialize()?;
132        self.secure_aggregation.initialize()?;
133        Ok(())
134    }
135
136    pub fn add_laplace_noise(
137        &mut self,
138        value: f64,
139        sensitivity: f64,
140    ) -> Result<(f64, f64), StatisticalError> {
141        let epsilon = 1.0;
142        let scale = sensitivity / epsilon;
143
144        let noise = self.generate_laplace_noise(scale)?;
145        let noisy_value = value + noise;
146
147        // Update privacy budget
148        self.privacy_budget.remaining_epsilon -= epsilon;
149
150        Ok((noisy_value, epsilon))
151    }
152
153    pub fn add_histogram_noise(
154        &mut self,
155        counts: &[u32],
156    ) -> Result<(Vec<u32>, f64), StatisticalError> {
157        let epsilon = 1.0;
158        let sensitivity = 1.0;
159        let scale = sensitivity / epsilon;
160
161        let mut noisy_counts = Vec::with_capacity(counts.len());
162        for &count in counts {
163            let noise = self.generate_laplace_noise(scale)?;
164            let noisy_count = (count as f64 + noise).max(0.0) as u32;
165            noisy_counts.push(noisy_count);
166        }
167
168        // Update privacy budget
169        self.privacy_budget.remaining_epsilon -= epsilon;
170
171        Ok((noisy_counts, epsilon))
172    }
173
174    /// Sample real Laplace(0, `scale`) noise via inverse-CDF transform over OS
175    /// entropy.
176    ///
177    /// SECURITY / CORRECTNESS: a former implementation drew from a global
178    /// monotonic `AtomicU64` counter, so the "noise" was fully deterministic
179    /// and predictable — which **voids** the differential-privacy guarantee
180    /// (an observer who knows the call sequence can subtract the exact noise
181    /// and recover the raw value). The old transform was also not a Laplace
182    /// sample. This uses real OS entropy (`getrandom`, native + wasm) and the
183    /// correct inverse CDF, and **fails closed** (returns `PrivacyError`, no
184    /// output) when entropy is unavailable rather than degrade to weak noise.
185    ///
186    /// Note: `epsilon` is fixed at 1.0 by the callers and the budget is a
187    /// simple per-query decrement — this is a valid fixed-ε mechanism, but it
188    /// does not enforce ε-composition across many queries (that lives in the
189    /// separate `PrivacyAccountant`). Per-query noise is now genuinely random.
190    fn generate_laplace_noise(&self, scale: f64) -> Result<f64, StatisticalError> {
191        let mut bytes = [0u8; 8];
192        getrandom::fill(&mut bytes).map_err(|e| {
193            StatisticalError::PrivacyError(format!(
194                "no OS entropy for differential-privacy noise: {e}"
195            ))
196        })?;
197        // Map the random u64 to the OPEN interval (0,1) so that ln(1 - 2|u|)
198        // stays finite: (x + 0.5) / 2^64 is never exactly 0 or 1.
199        let x = u64::from_le_bytes(bytes) as f64;
200        let r = (x + 0.5) / (u64::MAX as f64 + 1.0);
201        // u ~ Uniform(-1/2, 1/2); Laplace inverse CDF:
202        //   X = -scale * sgn(u) * ln(1 - 2|u|)
203        let u = r - 0.5;
204        Ok(-scale * u.signum() * (1.0 - 2.0 * u.abs()).ln())
205    }
206
207    /// Encrypt (seal) a statistical result using the fiduciary crypto system.
208    ///
209    /// `FiduciaryCrypto` exposes ML-DSA (FIPS-204) signing rather than symmetric
210    /// encryption, so "encryption" here means producing an authenticated
211    /// signature over the result bytes. The returned bytes are the ML-DSA
212    /// signature; a holder of the public key can verify that the result was
213    /// produced by this engine and has not been tampered with. A default
214    /// signing key is generated lazily on first use.
215    pub fn encrypt_result(&self, data: &[u8]) -> Result<Vec<u8>, StatisticalError> {
216        let mut crypto = self
217            .fiduciary_crypto
218            .lock()
219            .map_err(|e| StatisticalError::PrivacyError(e.to_string()))?;
220
221        const STAT_KEY_ID: &str = "statistical_results";
222        if !crypto.list_keys().iter().any(|k| k == STAT_KEY_ID) {
223            crypto
224                .generate_key(STAT_KEY_ID.to_string())
225                .map_err(|e| StatisticalError::PrivacyError(e.to_string()))?;
226        }
227
228        let signature = crypto
229            .sign(
230                data,
231                Some(STAT_KEY_ID),
232                "statistical_computing".to_string(),
233                "result_encryption".to_string(),
234            )
235            .map_err(|e| StatisticalError::PrivacyError(e.to_string()))?;
236
237        Ok(signature.sig_bytes)
238    }
239
240    /// Verify (open) a statistical result sealed by `encrypt_result`.
241    ///
242    /// Returns `Ok(true)` when the signature is valid for `data` under the
243    /// engine's statistical-results key.
244    pub fn verify_result(&self, data: &[u8], signature: &[u8]) -> Result<bool, StatisticalError> {
245        let crypto = self
246            .fiduciary_crypto
247            .lock()
248            .map_err(|e| StatisticalError::PrivacyError(e.to_string()))?;
249
250        const STAT_KEY_ID: &str = "statistical_results";
251        let sig = MlDsaSignature {
252            sig_bytes: signature.to_vec(),
253        };
254        crypto
255            .verify(
256                data,
257                &sig,
258                Some(STAT_KEY_ID),
259                "statistical_computing".to_string(),
260                "result_encryption".to_string(),
261            )
262            .map_err(|e| StatisticalError::PrivacyError(e.to_string()))
263    }
264
265    /// Generate a zero-knowledge proof that a statistical computation was
266    /// performed correctly.
267    ///
268    /// The proof binds the private `inputs` and public `outputs` together: a
269    /// SHA-256 commitment over all inputs/outputs becomes a private witness,
270    /// and the same commitment is exposed as the single public input. The
271    /// circuit enforces `one * commitment = commitment`, so a verifying party
272    /// learns only that the prover knows the commitment bound to the published
273    /// outputs — not the inputs themselves. The returned bytes are a
274    /// `serde_json`-serialised `ZkProof` (which carries its own public inputs),
275    /// so it can be verified by `verify_computation` without extra state.
276    pub fn prove_computation(
277        &self,
278        computation_id: &str,
279        inputs: &[Vec<u8>],
280        outputs: &[Vec<u8>],
281    ) -> Result<Vec<u8>, StatisticalError> {
282        let mut zk = self
283            .zk_proofs
284            .lock()
285            .map_err(|e| StatisticalError::PrivacyError(e.to_string()))?;
286
287        // Commitment over inputs and outputs: SHA-256 -> 32-byte field element.
288        let mut hasher = Sha256::new();
289        for chunk in inputs {
290            hasher.update(chunk);
291        }
292        for chunk in outputs {
293            hasher.update(chunk);
294        }
295        let digest = hasher.finalize();
296        let mut commitment = [0u8; 32];
297        commitment.copy_from_slice(&digest);
298
299        let circuit_id = format!("stat_comp_{}", computation_id);
300        zk.create_circuit(circuit_id.clone())
301            .map_err(|e| StatisticalError::PrivacyError(e.to_string()))?;
302
303        // Public input: the commitment bound to the published outputs.
304        zk.add_variable(&circuit_id, "commitment".to_string(), VariableType::Public)
305            .map_err(|e| StatisticalError::PrivacyError(e.to_string()))?;
306        // Private witness: the multiplicative identity and the same commitment.
307        zk.add_variable(&circuit_id, "one".to_string(), VariableType::Private)
308            .map_err(|e| StatisticalError::PrivacyError(e.to_string()))?;
309        zk.add_variable(&circuit_id, "in_commit".to_string(), VariableType::Private)
310            .map_err(|e| StatisticalError::PrivacyError(e.to_string()))?;
311
312        // Constraint: one * in_commit = commitment (binds private/public).
313        zk.add_constraint(
314            &circuit_id,
315            CircuitExpression::Variable("one".to_string()),
316            CircuitExpression::Variable("in_commit".to_string()),
317            CircuitExpression::Variable("commitment".to_string()),
318        )
319        .map_err(|e| StatisticalError::PrivacyError(e.to_string()))?;
320
321        zk.generate_keys(&circuit_id)
322            .map_err(|e| StatisticalError::PrivacyError(e.to_string()))?;
323
324        // Field-one in little-endian: [1, 0, ...].
325        let mut one_val = [0u8; 32];
326        one_val[0] = 1;
327
328        let mut witness = HashMap::new();
329        witness.insert("one".to_string(), FieldElement { value: one_val });
330        witness.insert("in_commit".to_string(), FieldElement { value: commitment });
331        witness.insert("commitment".to_string(), FieldElement { value: commitment });
332
333        let public_inputs = vec![FieldElement { value: commitment }];
334
335        let proof = zk
336            .generate_proof(&circuit_id, witness, public_inputs)
337            .map_err(|e| StatisticalError::PrivacyError(e.to_string()))?;
338
339        serde_json::to_vec(&proof).map_err(|e| StatisticalError::PrivacyError(e.to_string()))
340    }
341
342    /// Verify a zero-knowledge computation proof produced by `prove_computation`.
343    ///
344    /// `proof` is the serialised `ZkProof` bytes. When `public_inputs` is
345    /// non-empty, each entry is interpreted as a 32-byte little-endian field
346    /// element and checked against the public inputs embedded in the proof, so
347    /// callers can confirm the proof binds to the outputs they expect.
348    pub fn verify_computation(
349        &self,
350        proof: &[u8],
351        public_inputs: &[Vec<u8>],
352    ) -> Result<bool, StatisticalError> {
353        let zk_proof: ZkProof = serde_json::from_slice(proof)
354            .map_err(|e| StatisticalError::PrivacyError(e.to_string()))?;
355
356        // Optional binding check: the caller-supplied public inputs must match
357        // the ones embedded in the proof.
358        if !public_inputs.is_empty() {
359            if public_inputs.len() != zk_proof.public_inputs.len() {
360                return Ok(false);
361            }
362            for (expected, actual) in public_inputs.iter().zip(&zk_proof.public_inputs) {
363                let mut expected_arr = [0u8; 32];
364                let len = expected.len().min(32);
365                expected_arr[..len].copy_from_slice(&expected[..len]);
366                if expected_arr != actual.value {
367                    return Ok(false);
368                }
369            }
370        }
371
372        let mut zk = self
373            .zk_proofs
374            .lock()
375            .map_err(|e| StatisticalError::PrivacyError(e.to_string()))?;
376
377        let result = zk
378            .verify_proof(&zk_proof)
379            .map_err(|e| StatisticalError::PrivacyError(e.to_string()))?;
380
381        Ok(result.is_valid)
382    }
383}
384
385impl DifferentialPrivacy {
386    pub fn new() -> Self {
387        Self {
388            noise_mechanisms: vec![NoiseMechanism::Laplace, NoiseMechanism::Gaussian],
389            privacy_accountant: PrivacyAccountant {
390                total_epsilon_spent: 0.0,
391                total_delta_spent: 0.0,
392                composition_method: CompositionMethod::AdvancedComposition,
393                remaining_budget: PrivacyBudget {
394                    epsilon: 1.0,
395                    delta: 1e-6,
396                    remaining_epsilon: 1.0,
397                    remaining_delta: 1e-6,
398                    budget_period: 86400,
399                    last_reset: 0,
400                },
401            },
402            sensitivity_analyzer: SensitivityAnalyzer::new(),
403        }
404    }
405
406    pub fn initialize(&mut self) -> Result<(), StatisticalError> {
407        self.sensitivity_analyzer.initialize()?;
408        Ok(())
409    }
410
411    /// Returns the list of noise mechanisms available to this DP engine.
412    pub fn noise_mechanisms(&self) -> &[NoiseMechanism] {
413        &self.noise_mechanisms
414    }
415
416    /// Register an additional noise mechanism if not already present.
417    pub fn add_noise_mechanism(&mut self, mechanism: NoiseMechanism) {
418        if !self.noise_mechanisms.contains(&mechanism) {
419            self.noise_mechanisms.push(mechanism);
420        }
421    }
422
423    /// Returns `true` when the given noise mechanism is registered.
424    pub fn supports_noise_mechanism(&self, mechanism: &NoiseMechanism) -> bool {
425        self.noise_mechanisms.contains(mechanism)
426    }
427
428    /// Returns a reference to the privacy accountant tracking epsilon/delta spend.
429    pub fn privacy_accountant(&self) -> &PrivacyAccountant {
430        &self.privacy_accountant
431    }
432
433    /// Returns a mutable reference to the privacy accountant.
434    pub fn privacy_accountant_mut(&mut self) -> &mut PrivacyAccountant {
435        &mut self.privacy_accountant
436    }
437
438    /// Spend `epsilon`/`delta` from the privacy budget, recording the
439    /// consumption in the accountant. Returns `Err` when the budget is
440    /// insufficient.
441    pub fn spend_budget(&mut self, epsilon: f64, delta: f64) -> Result<(), StatisticalError> {
442        let remaining = &self.privacy_accountant.remaining_budget;
443        if remaining.remaining_epsilon < epsilon || remaining.remaining_delta < delta {
444            return Err(StatisticalError::PrivacyError(
445                "Insufficient privacy budget".to_string(),
446            ));
447        }
448        self.privacy_accountant.remaining_budget.remaining_epsilon -= epsilon;
449        self.privacy_accountant.remaining_budget.remaining_delta -= delta;
450        self.privacy_accountant.total_epsilon_spent += epsilon;
451        self.privacy_accountant.total_delta_spent += delta;
452        Ok(())
453    }
454}
455
456impl SensitivityAnalyzer {
457    pub fn new() -> Self {
458        Self {
459            sensitivity_functions: HashMap::new(),
460            sensitivity_cache: HashMap::new(),
461        }
462    }
463
464    pub fn initialize(&mut self) -> Result<(), StatisticalError> {
465        Ok(())
466    }
467
468    /// Register a named sensitivity function so it can be looked up by name
469    /// from `compute_sensitivity` / `get_sensitivity`.
470    pub fn register_function(&mut self, name: &str, func: SensitivityFunction) {
471        self.sensitivity_functions.insert(name.to_string(), func);
472    }
473
474    /// Compute the L1 sensitivity of a statistical operation over `data`.
475    ///
476    /// Sensitivity is the maximum change in the operation's output when a single
477    /// record is added or removed. The following closed-form approximations are
478    /// used (each assumes a bounded domain where one record can shift a value by
479    /// at most 1.0):
480    ///
481    /// - `mean`:      `1/n`        — one record moves the mean by `1/n`.
482    /// - `sum`:       `1.0`        — one record changes the sum by at most 1.
483    /// - `count`:     `1.0`        — one record changes the count by 1.
484    /// - `median`:    `range / n`  — adjacent-element approximation.
485    /// - `variance`:  `(max-min)^2 / n` — bounded shift approximation.
486    /// - `histogram`: `1.0`        — one record changes a single bin by 1.
487    ///
488    /// Results are cached keyed by `operation` so repeated DP queries reuse the
489    /// computed sensitivity.
490    pub fn compute_sensitivity(
491        &mut self,
492        operation: &str,
493        data: &[f64],
494    ) -> Result<f64, StatisticalError> {
495        // A registered function wins over the built-in approximations.
496        if let Some(func) = self.sensitivity_functions.get(operation) {
497            self.sensitivity_cache
498                .insert(operation.to_string(), func.sensitivity);
499            return Ok(func.sensitivity);
500        }
501
502        if data.is_empty() {
503            return Err(StatisticalError::InvalidData(
504                "Cannot compute sensitivity over empty data".to_string(),
505            ));
506        }
507
508        let n = data.len() as f64;
509        let sensitivity = match operation {
510            "mean" => 1.0 / n,
511            "sum" => 1.0,
512            "count" => 1.0,
513            "histogram" => 1.0,
514            "median" => {
515                let min = data.iter().cloned().fold(f64::INFINITY, f64::min);
516                let max = data.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
517                (max - min) / n
518            }
519            "variance" => {
520                let min = data.iter().cloned().fold(f64::INFINITY, f64::min);
521                let max = data.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
522                let range = max - min;
523                (range * range) / n
524            }
525            other => {
526                return Err(StatisticalError::InvalidOperation(format!(
527                    "Unknown sensitivity operation '{}'",
528                    other
529                )))
530            }
531        };
532
533        self.sensitivity_cache
534            .insert(operation.to_string(), sensitivity);
535        Ok(sensitivity)
536    }
537
538    /// Get the sensitivity for an operation, returning the cached value when
539    /// available and computing (and caching) it otherwise.
540    pub fn get_sensitivity(
541        &mut self,
542        operation: &str,
543        data: &[f64],
544    ) -> Result<f64, StatisticalError> {
545        if let Some(cached) = self.sensitivity_cache.get(operation) {
546            return Ok(*cached);
547        }
548        self.compute_sensitivity(operation, data)
549    }
550}
551
552impl SecureAggregation {
553    pub fn new() -> Self {
554        Self {
555            aggregation_protocols: vec![
556                AggregationProtocol::SecureSum,
557                AggregationProtocol::SecureMean,
558            ],
559            encryption_schemes: vec![
560                EncryptionScheme::Homomorphic,
561                EncryptionScheme::SecretSharing,
562            ],
563            integrity_checks: Vec::new(),
564        }
565    }
566
567    pub fn initialize(&mut self) -> Result<(), StatisticalError> {
568        Ok(())
569    }
570
571    /// Returns the list of registered aggregation protocols.
572    pub fn aggregation_protocols(&self) -> &[AggregationProtocol] {
573        &self.aggregation_protocols
574    }
575
576    /// Register an additional aggregation protocol if not already present.
577    pub fn add_aggregation_protocol(&mut self, protocol: AggregationProtocol) {
578        if !self.aggregation_protocols.contains(&protocol) {
579            self.aggregation_protocols.push(protocol);
580        }
581    }
582
583    /// Returns the list of registered encryption schemes.
584    pub fn encryption_schemes(&self) -> &[EncryptionScheme] {
585        &self.encryption_schemes
586    }
587
588    /// Register an additional encryption scheme if not already present.
589    pub fn add_encryption_scheme(&mut self, scheme: EncryptionScheme) {
590        if !self.encryption_schemes.contains(&scheme) {
591            self.encryption_schemes.push(scheme);
592        }
593    }
594
595    /// Register an integrity check.
596    pub fn add_integrity_check(&mut self, check: IntegrityCheck) {
597        self.integrity_checks.push(check);
598    }
599
600    /// Returns the list of registered integrity checks.
601    pub fn integrity_checks(&self) -> &[IntegrityCheck] {
602        &self.integrity_checks
603    }
604
605    /// Look up an integrity check by id.
606    pub fn get_integrity_check(&self, check_id: &str) -> Option<&IntegrityCheck> {
607        self.integrity_checks
608            .iter()
609            .find(|c| c.check_id == check_id)
610    }
611
612    /// Returns the number of registered integrity checks.
613    pub fn integrity_check_count(&self) -> usize {
614        self.integrity_checks.len()
615    }
616}