use crate::error::{Result, SecurityError};
use scirs2_core::random::prelude::{Normal, thread_rng};
pub struct LaplaceMechanism {
epsilon: f64,
sensitivity: f64,
}
impl LaplaceMechanism {
pub fn new(epsilon: f64, sensitivity: f64) -> Result<Self> {
if !epsilon.is_finite() || epsilon <= 0.0 {
return Err(SecurityError::invalid_input(format!(
"epsilon must be finite and > 0.0, got {epsilon}"
)));
}
if !sensitivity.is_finite() || sensitivity < 0.0 {
return Err(SecurityError::invalid_input(format!(
"sensitivity must be finite and >= 0.0, got {sensitivity}"
)));
}
Ok(Self {
epsilon,
sensitivity,
})
}
pub fn add_noise(&self, value: f64) -> f64 {
let scale = self.sensitivity / self.epsilon;
let noise = self.sample_laplace(scale);
value + noise
}
fn sample_laplace(&self, scale: f64) -> f64 {
let mut rng = thread_rng();
let u: f64 = rng.random_range(-0.5..0.5);
-scale * u.signum() * (1.0 - 2.0 * u.abs()).ln()
}
}
pub struct GaussianMechanism {
epsilon: f64,
delta: f64,
sensitivity: f64,
}
impl GaussianMechanism {
pub fn new(epsilon: f64, delta: f64, sensitivity: f64) -> Result<Self> {
if !epsilon.is_finite() || epsilon <= 0.0 {
return Err(SecurityError::invalid_input(format!(
"epsilon must be finite and > 0.0, got {epsilon}"
)));
}
if !delta.is_finite() || delta <= 0.0 || delta >= 1.0 {
return Err(SecurityError::invalid_input(format!(
"delta must be finite and in the open interval (0.0, 1.0), got {delta}"
)));
}
if !sensitivity.is_finite() || sensitivity < 0.0 {
return Err(SecurityError::invalid_input(format!(
"sensitivity must be finite and >= 0.0, got {sensitivity}"
)));
}
Ok(Self {
epsilon,
delta,
sensitivity,
})
}
pub fn add_noise(&self, value: f64) -> Result<f64> {
let sigma = self.sensitivity * (2.0 * (1.25 / self.delta).ln()).sqrt() / self.epsilon;
if !sigma.is_finite() || sigma < 0.0 {
return Err(SecurityError::internal(format!(
"computed Gaussian noise sigma is invalid: {sigma}"
)));
}
let mut rng = thread_rng();
let normal = Normal::new(0.0, sigma).map_err(|e| {
SecurityError::internal(format!("failed to build Normal distribution: {e}"))
})?;
let noise: f64 = rng.sample(normal);
Ok(value + noise)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_laplace_mechanism() {
let mechanism = LaplaceMechanism::new(1.0, 1.0).expect("valid parameters");
let original = 100.0;
let noisy = mechanism.add_noise(original);
assert!((original - noisy).abs() < 50.0); }
#[test]
fn test_laplace_mechanism_rejects_non_positive_epsilon() {
assert!(LaplaceMechanism::new(0.0, 1.0).is_err());
assert!(LaplaceMechanism::new(-1.0, 1.0).is_err());
assert!(LaplaceMechanism::new(f64::NAN, 1.0).is_err());
}
#[test]
fn test_laplace_mechanism_rejects_invalid_sensitivity() {
assert!(LaplaceMechanism::new(1.0, -1.0).is_err());
assert!(LaplaceMechanism::new(1.0, f64::INFINITY).is_err());
}
#[test]
fn test_gaussian_mechanism_valid_parameters() {
let mechanism = GaussianMechanism::new(1.0, 0.5, 1.0).expect("valid parameters");
let original = 100.0;
let noisy = mechanism
.add_noise(original)
.expect("add_noise should succeed");
assert!(noisy.is_finite());
}
#[test]
fn test_gaussian_mechanism_rejects_delta_greater_than_1_25() {
let result = GaussianMechanism::new(1.0, 1.5, 1.0);
assert!(result.is_err());
}
#[test]
fn test_gaussian_mechanism_rejects_delta_at_or_above_1() {
assert!(GaussianMechanism::new(1.0, 1.0, 1.0).is_err());
}
#[test]
fn test_gaussian_mechanism_rejects_non_positive_delta() {
assert!(GaussianMechanism::new(1.0, 0.0, 1.0).is_err());
assert!(GaussianMechanism::new(1.0, -0.1, 1.0).is_err());
}
#[test]
fn test_gaussian_mechanism_rejects_non_positive_epsilon() {
assert!(GaussianMechanism::new(0.0, 0.5, 1.0).is_err());
assert!(GaussianMechanism::new(-1.0, 0.5, 1.0).is_err());
}
#[test]
fn test_gaussian_mechanism_rejects_invalid_sensitivity() {
assert!(GaussianMechanism::new(1.0, 0.5, -1.0).is_err());
assert!(GaussianMechanism::new(1.0, 0.5, f64::NAN).is_err());
}
}