Skip to content

Privacy (differential privacy)

concrete_fhe_toolkit.privacy

Differential-privacy mechanisms for released (decrypted) aggregates.

The encrypted trainers in :mod:concrete_fhe_toolkit.ml.trainers follow the aggregate-decrypt pattern: raw samples stay encrypted, but aggregate statistics (counts, sums) are decrypted to build the model. Those aggregates still carry some information about individuals. Adding calibrated noise to them right after decryption turns that informal leak into a formal differential-privacy guarantee — the classic FHE + DP combination.

These helpers are clear-side: apply them to already-decrypted integers. Accounting is the caller's responsibility — when one individual affects k released values, either scale sensitivity accordingly or split your epsilon budget across the releases.

laplace_mechanism

laplace_mechanism(value: int, *, sensitivity: float, epsilon: float, rng: Optional[Generator] = None) -> int

Release an integer aggregate with epsilon-DP Laplace noise.

Adds noise drawn from Laplace(0, sensitivity / epsilon) and rounds back to an integer. sensitivity is how much the aggregate can change when one individual's data changes (1 for a simple count).

Parameters:

Name Type Description Default
value int

The encrypted value to process or validate.

required
sensitivity float

The L1/L2 sensitivity of the aggregate function for differential privacy.

required
epsilon float

Privacy budget for this release (smaller = more private).

required
rng Optional[Generator]

Optional numpy.random.Generator for reproducible noise.

None

Returns:

Type Description
int

The noised integer, safe to publish under epsilon-DP.

Example
import numpy as np
from concrete_fhe_toolkit.privacy import laplace_mechanism

# A decrypted patient count, released with a privacy guarantee:
noisy = laplace_mechanism(
    842, sensitivity=1, epsilon=1.0,
    rng=np.random.default_rng(42),
)
print(noisy)  # 842 +/- a few
Source code in src/concrete_fhe_toolkit/privacy.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def laplace_mechanism(
    value: int,
    *,
    sensitivity: float,
    epsilon: float,
    rng: Optional[np.random.Generator] = None,
) -> int:
    """Release an integer aggregate with epsilon-DP Laplace noise.

    Adds noise drawn from ``Laplace(0, sensitivity / epsilon)`` and rounds
    back to an integer. ``sensitivity`` is how much the aggregate can change
    when one individual's data changes (1 for a simple count).

    Args:
        value: The encrypted value to process or validate.
        sensitivity: The L1/L2 sensitivity of the aggregate function for differential privacy.
        epsilon: Privacy budget for this release (smaller = more private).
        rng: Optional ``numpy.random.Generator`` for reproducible noise.

    Returns:
        The noised integer, safe to publish under epsilon-DP.

    Example:
        ```python
        import numpy as np
        from concrete_fhe_toolkit.privacy import laplace_mechanism

        # A decrypted patient count, released with a privacy guarantee:
        noisy = laplace_mechanism(
            842, sensitivity=1, epsilon=1.0,
            rng=np.random.default_rng(42),
        )
        print(noisy)  # 842 +/- a few
        ```
    """
    scale = _validate_positive("sensitivity", sensitivity) / _validate_positive(
        "epsilon", epsilon
    )
    generator = rng if rng is not None else np.random.default_rng()
    return int(round(int(value) + generator.laplace(0.0, scale)))

gaussian_mechanism

gaussian_mechanism(value: int, *, sensitivity: float, epsilon: float, delta: float, rng: Optional[Generator] = None) -> int

Release an integer aggregate with (epsilon, delta)-DP Gaussian noise.

Uses the classical sufficient calibration sigma = sensitivity * sqrt(2 ln(1.25/delta)) / epsilon (valid for epsilon <= 1). Prefer this over Laplace when many values are released together, since Gaussian noise composes better.

Parameters:

Name Type Description Default
value int

The encrypted value to process or validate.

required
sensitivity float

The L1/L2 sensitivity of the aggregate function for differential privacy.

required
epsilon float

Privacy budget (must be in (0, 1] for this calibration).

required
delta float

Failure probability (for example 1e-5).

required
rng Optional[Generator]

Optional numpy.random.Generator for reproducible noise.

None

Returns:

Type Description
int

The noised integer.

Example
import numpy as np
from concrete_fhe_toolkit.privacy import gaussian_mechanism

noisy = gaussian_mechanism(
    842, sensitivity=1, epsilon=0.5, delta=1e-5,
    rng=np.random.default_rng(7),
)
Source code in src/concrete_fhe_toolkit/privacy.py
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
def gaussian_mechanism(
    value: int,
    *,
    sensitivity: float,
    epsilon: float,
    delta: float,
    rng: Optional[np.random.Generator] = None,
) -> int:
    """Release an integer aggregate with (epsilon, delta)-DP Gaussian noise.

    Uses the classical sufficient calibration ``sigma = sensitivity * sqrt(2 ln(1.25/delta))
    / epsilon`` (valid for ``epsilon <= 1``). Prefer this over Laplace when
    many values are released together, since Gaussian noise composes better.

    Args:
        value: The encrypted value to process or validate.
        sensitivity: The L1/L2 sensitivity of the aggregate function for differential privacy.
        epsilon: Privacy budget (must be in ``(0, 1]`` for this calibration).
        delta: Failure probability (for example ``1e-5``).
        rng: Optional ``numpy.random.Generator`` for reproducible noise.

    Returns:
        The noised integer.

    Example:
        ```python
        import numpy as np
        from concrete_fhe_toolkit.privacy import gaussian_mechanism

        noisy = gaussian_mechanism(
            842, sensitivity=1, epsilon=0.5, delta=1e-5,
            rng=np.random.default_rng(7),
        )
        ```
    """
    eps = _validate_positive("epsilon", epsilon)
    if eps > 1:
        raise ValueError("epsilon must be at most 1 for the Gaussian calibration")
    dlt = _validate_positive("delta", delta)
    if dlt >= 1:
        raise ValueError("delta must be smaller than 1")
    sigma = (
        _validate_positive("sensitivity", sensitivity)
        * math.sqrt(2 * (math.log(1.25) - math.log(dlt)))
        / eps
    )
    generator = rng if rng is not None else np.random.default_rng()
    return int(round(int(value) + generator.normal(0.0, sigma)))

dp_release

dp_release(values: Sequence[int], *, sensitivity: float, epsilon: float, mechanism: str = 'laplace', delta: Optional[float] = None, rng: Optional[Generator] = None) -> Union[np.ndarray, List[int]]

Noise a whole vector of decrypted aggregates with one shared budget.

The given epsilon is the budget for the entire release: it is split evenly across the values, so releasing more values means more noise per value. Laplace uses simple composition. For Gaussian, delta is also a total vector budget and total epsilon must be in (0, 1]. With n values of sensitivity s, the vector L2 sensitivity is at most sqrt(n)s; the existing noise nssqrt(2ln(1.25/delta))/epsilon is a conservative calibration for that vector. Delta need not be split again. These guarantees assume the stated sensitivity bounds and independent noise. Use the decrypted aggregate vectors (counts, sums, ...) as input.

Parameters:

Name Type Description Default
values Sequence[int]

The decrypted aggregates to release together.

required
sensitivity float

The L1/L2 sensitivity of the aggregate function for differential privacy.

required
epsilon float

Total privacy budget for the whole vector.

required
mechanism str

"laplace" (pure epsilon-DP) or "gaussian" (requires delta).

'laplace'
delta Optional[float]

Total delta budget for the Gaussian vector release, in (0, 1).

None
rng Optional[Generator]

Optional numpy.random.Generator for reproducible noise.

None

Returns:

Type Description
Union[ndarray, List[int]]

The noised integers, in the same order.

Example
import numpy as np
from concrete_fhe_toolkit.privacy import dp_release

# Class counts decrypted by an encrypted trainer:
counts = [412, 430]
noisy = dp_release(
    counts, sensitivity=1, epsilon=1.0,
    rng=np.random.default_rng(0),
)
print(noisy)  # e.g. [410, 431] — safe to use clear-side
Source code in src/concrete_fhe_toolkit/privacy.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def dp_release(
    values: Sequence[int],
    *,
    sensitivity: float,
    epsilon: float,
    mechanism: str = "laplace",
    delta: Optional[float] = None,
    rng: Optional[np.random.Generator] = None,
) -> Union[np.ndarray, List[int]]:
    """Noise a whole vector of decrypted aggregates with one shared budget.

    The given ``epsilon`` is the budget for the *entire* release: it is
    split evenly across the values, so releasing more values means more noise
    per value. Laplace uses simple composition. For Gaussian, ``delta`` is
    also a total vector budget and total ``epsilon`` must be in ``(0, 1]``.
    With n values of sensitivity s, the vector L2 sensitivity is at most
    sqrt(n)*s; the existing noise n*s*sqrt(2*ln(1.25/delta))/epsilon is a
    conservative calibration for that vector. Delta need not be split again.
    These guarantees assume the stated sensitivity bounds and independent
    noise. Use the decrypted aggregate vectors (counts, sums, ...) as input.

    Args:
        values: The decrypted aggregates to release together.
        sensitivity: The L1/L2 sensitivity of the aggregate function for differential privacy.
        epsilon: Total privacy budget for the whole vector.
        mechanism: ``"laplace"`` (pure epsilon-DP) or ``"gaussian"``
            (requires ``delta``).
        delta: Total delta budget for the Gaussian vector release, in (0, 1).
        rng: Optional ``numpy.random.Generator`` for reproducible noise.

    Returns:
        The noised integers, in the same order.

    Example:
        ```python
        import numpy as np
        from concrete_fhe_toolkit.privacy import dp_release

        # Class counts decrypted by an encrypted trainer:
        counts = [412, 430]
        noisy = dp_release(
            counts, sensitivity=1, epsilon=1.0,
            rng=np.random.default_rng(0),
        )
        print(noisy)  # e.g. [410, 431] — safe to use clear-side
        ```
    """
    if mechanism not in {"laplace", "gaussian"}:
        raise ValueError("mechanism must be 'laplace' or 'gaussian'")
    total_epsilon = _validate_positive("epsilon", epsilon)
    _validate_positive("sensitivity", sensitivity)
    if mechanism == "gaussian":
        if total_epsilon > 1:
            raise ValueError("total epsilon must be at most 1 for the Gaussian calibration")
        if delta is None:
            raise ValueError("the gaussian mechanism requires delta")
        if _validate_positive("delta", delta) >= 1:
            raise ValueError("delta must be smaller than 1")
    items = list(values)
    if not items:
        return []
    per_value_epsilon = total_epsilon / len(items)
    generator = rng if rng is not None else np.random.default_rng()
    if mechanism == "laplace":
        return [
            laplace_mechanism(
                value,
                sensitivity=sensitivity,
                epsilon=per_value_epsilon,
                rng=generator,
            )
            for value in items
        ]
    if mechanism == "gaussian":
        if delta is None:
            raise ValueError("the gaussian mechanism requires delta")
        return [
            gaussian_mechanism(
                value,
                sensitivity=sensitivity,
                epsilon=per_value_epsilon,
                delta=delta,
                rng=generator,
            )
            for value in items
        ]
    raise ValueError("mechanism must be 'laplace' or 'gaussian'")