Remapping Attacks Against Geo-Indistinguishability
Geo-indistinguishability produces reports that can land in the sea, inside a mountain, or two kilometres past the city boundary — so almost every deployment adds a remapping step that moves the report somewhere plausible. That step is post-processing, which cannot weaken the formal guarantee, and yet a badly chosen remap reliably does weaken the practical one. The reason is that remapping is a deterministic function the adversary also knows, and inverting it narrows the set of true locations consistent with a report. This page explains the attack and the two remaps that survive it, under geo-indistinguishability in Differential Privacy for Geospatial Data.
Parameter Configuration and Calibration
region— the set the report must land in. A service area, a landmass, a road network. The tighter the region relative to the noise, the more often the remap fires and the more it matters. Measure the firing rate before choosing a remap: below a few percent almost anything is safe; above 20% the remap is the mechanism.remap_rule— how an out-of-region point is moved. Three families, in increasing order of safety: nearest-point projection (dangerous), resample-until-inside (safe but costly), and optimal remapping toward a prior (safe and useful). Which one you pick is the entire subject of this page.prior— the reference distribution for optimal remapping. Population density, or the historical distribution of reports. A remap toward a uniform prior throws away utility; a remap toward a very sharp prior pulls reports onto a handful of points and destroys the guarantee’s practical value even though the formal one survives.max_resample_attempts— the bound on the safe-but-costly option. Resampling has no upper bound in principle. Cap the attempts and fall back to a documented rule, because an unbounded loop on a handset is a battery incident.
| Remap rule | Formal guarantee | Practical effect | Verdict |
|---|---|---|---|
| Nearest point on the boundary | preserved | reports pile onto the boundary; inversion is trivial | do not use |
| Snap to nearest road/POI | preserved | many-to-one onto a sparse set; strong inversion | avoid |
| Resample until inside | preserved | conditional distribution, no pile-up | safe |
| Optimal remap toward a prior | preserved | improves expected error; well-studied | best |
Reference Implementation
from __future__ import annotations
import math
import random
from dataclasses import dataclass
from typing import Callable, Sequence
@dataclass(frozen=True)
class RemapReport:
x: float
y: float
attempts: int
remapped: bool
def naive_boundary_remap(
x: float, y: float, *, inside: Callable[[float, float], bool],
boundary: Sequence[tuple[float, float]],
) -> RemapReport:
"""The dangerous rule: project an outside point to the nearest boundary vertex.
Included so its failure can be measured, not because it should be deployed. It
is a many-to-one map onto a small set, so a report ON the boundary tells the
adversary that the true location was somewhere in the (large) region that maps
to that vertex — which is far more informative than the mechanism intended.
"""
if inside(x, y):
return RemapReport(x, y, 0, False)
bx, by = min(boundary, key=lambda p: math.hypot(p[0] - x, p[1] - y))
return RemapReport(bx, by, 1, True)
def resample_remap(
sample: Callable[[], tuple[float, float]],
*,
inside: Callable[[float, float], bool],
max_attempts: int = 64,
) -> RemapReport:
"""The safe rule: draw again until the report lands inside the region.
This yields the mechanism's own distribution CONDITIONED on the region, which
remains a valid geo-indistinguishable release. Cost is unbounded in principle,
so the attempt cap matters; on exhaustion the caller must fall back to a
documented, region-wide rule rather than to the last (outside) draw.
"""
for attempt in range(1, max_attempts + 1):
x, y = sample()
if inside(x, y):
return RemapReport(x, y, attempt, attempt > 1)
raise RuntimeError(
"resampling exhausted: the privacy radius is large relative to the region — "
"reduce the radius or widen the region rather than falling back to a projection"
)
def optimal_remap(
x: float, y: float,
*,
candidates: Sequence[tuple[float, float]],
prior: Sequence[float],
epsilon: float,
) -> RemapReport:
"""Move the report to the candidate minimising expected error under a prior.
The remap is a function of the REPORT only — never of the true location — which
is what keeps it post-processing. Weighting by the prior's posterior for each
candidate given the observed report is the standard optimal-remap construction.
"""
if len(candidates) != len(prior):
raise ValueError("prior must be aligned with candidates")
best, best_cost = None, float("inf")
for cx, cy in candidates:
cost = 0.0
for (px, py), w in zip(candidates, prior):
d_report = math.hypot(px - x, py - y)
posterior = w * math.exp(-epsilon * d_report)
cost += posterior * math.hypot(px - cx, py - cy)
if cost < best_cost:
best, best_cost = (cx, cy), cost
assert best is not None
return RemapReport(best[0], best[1], 1, best != (x, y))
Validation Checkpoint
def _validate() -> None:
rng = random.Random(17)
# A square service region, 4 km on a side, centred on the origin.
def inside(x: float, y: float) -> bool:
return abs(x) <= 2_000 and abs(y) <= 2_000
boundary = [(2_000.0, y) for y in range(-2_000, 2_001, 250)]
def sample_at(cx: float, cy: float, radius_m: float):
def draw() -> tuple[float, float]:
theta = rng.random() * 2 * math.pi
r = -radius_m / math.log(2) * math.log(rng.random())
return cx + r * math.cos(theta), cy + r * math.sin(theta)
return draw
# 1. Resampling always returns a point inside the region.
draw = sample_at(1_900.0, 0.0, 400.0)
reports = [resample_remap(draw, inside=inside) for _ in range(500)]
assert all(inside(r.x, r.y) for r in reports)
# 2. Resampling must NOT concentrate mass on the boundary.
on_edge = sum(1 for r in reports if abs(abs(r.x) - 2_000) < 1.0)
assert on_edge == 0, "the conditional distribution has no atom at the edge"
# 3. The naive projection DOES concentrate — this is the attack surface.
naive = [naive_boundary_remap(*draw(), inside=inside, boundary=boundary)
for _ in range(500)]
piled = sum(1 for r in naive if r.remapped)
distinct = len({(round(r.x), round(r.y)) for r in naive if r.remapped})
assert piled > 100 and distinct <= len(boundary), (piled, distinct)
# A report at a boundary vertex is consistent with a large half-plane of truths,
# so an adversary seeing one learns "outside, in this direction" with certainty.
# 4. Optimal remapping must move reports toward the prior's mass, not away.
candidates = [(-1_500.0, 0.0), (0.0, 0.0), (1_500.0, 0.0)]
prior = [0.1, 0.8, 0.1]
moved = optimal_remap(1_400.0, 0.0, candidates=candidates, prior=prior,
epsilon=math.log(2) / 800.0)
assert moved.x <= 1_500.0
# 5. Resampling must fail loudly when the region cannot contain the noise.
tiny = lambda x, y: abs(x) < 5 and abs(y) < 5
try:
resample_remap(sample_at(0.0, 0.0, 5_000.0), inside=tiny, max_attempts=32)
except RuntimeError as exc:
assert "resampling exhausted" in str(exc)
else:
raise AssertionError("an impossible region must raise, not silently project")
print("remapping: all assertions passed")
_validate()
Assertion 3 is the attack, quantified. Under the naive projection, a substantial fraction of reports land on a small set of boundary vertices, and each of those reports tells the adversary that the true location was outside the region in a particular direction — information the unremapped mechanism would never have revealed.
Incident Response and Edge Cases
- A large fraction of reports sit exactly on the region boundary. The naive projection is in use. Replace it with resampling; the change is small and the improvement is immediate.
- Reports cluster on a handful of POIs. Someone added a “snap to nearest landmark” step for usability. It is a many-to-one map onto a sparse set, which is the same failure as boundary projection with a friendlier name. If the product needs a landmark name, derive it for display only and never store or transmit the snapped coordinate as the report.
- Resampling exhausts on rural devices. The privacy radius is comparable to the region size. Either widen the region (often the service area was drawn too tightly) or reduce the radius and compensate with a temporal control; falling back to projection re-introduces exactly the attack.
- The prior used for optimal remapping is stale. A prior derived from historical reports drifts, and a mismatched prior increases expected error without breaking the guarantee. Refresh it on a schedule, and remember the prior is derived from data — publishing it is a release.
- The remap depends on the true location. Then it is not post-processing and the guarantee is void. Audit the code path: the remap function must take the report and public parameters only, with no access to the input coordinate.
Frequently Asked Questions
If post-processing cannot weaken DP, how can a remap be harmful?
The formal ε-guarantee does survive — the bound on the likelihood ratio between two true locations is unchanged. What changes is the adversary's practical posterior: a many-to-one remap makes many reports identical, and observing that value reveals membership of the large set that maps to it. The theorem and the operational risk are answering different questions.
Is resampling biased?
It produces the mechanism's distribution conditioned on the region, which is exactly what you want and is still geo-indistinguishable. It is "biased" only relative to the unconditioned mechanism, which was producing unusable reports outside the region.
Which prior should optimal remapping use?
Population density or the historical report distribution, at a resolution coarse enough that the prior is not itself a sensitive artefact. Avoid very sharp priors: they pull reports onto a few points, which reproduces the many-to-one problem the remap was supposed to avoid.
Should the remap run on the device or the server?
Either is sound, since the remap sees only the report. On-device keeps the server from ever seeing the out-of-region draw, which is tidier operationally. Server-side lets the prior be updated without shipping a client release. Choose on operational grounds, and document which one, because it affects what a network observer sees.
Related
- Geo-Indistinguishability — the mechanism and its truncation step.
- Implementing Geo-Indistinguishability in Python — the sampler these remaps wrap.
- Planar Laplace Noise for Location Privacy — the distribution being conditioned.
- Simulating Trajectory Reconstruction Attacks — measuring the effect of a remap on linkability.
Up one level: Geo-Indistinguishability · Section: Differential Privacy for Geospatial Data.