From c988d42c9bba563757c58593035c108aefc1a04a Mon Sep 17 00:00:00 2001 From: springswell <149616731+springswell@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:07:18 +0100 Subject: [PATCH 1/2] feat(privacy): add PII redaction helpers for selective disclosure (#723) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds redactProfile() and jitterCoordinates() functions to enforce selective disclosure privacy at the UI layer. redactProfile(): - Strips all non-disclosed fields from profile objects - Only allows explicitly listed fields to pass through - Prevents accidental PII exposure in map popups and API responses jitterCoordinates(): - Adds positional noise (default ±200m) to lat/lng coordinates - Supports k-anonymity by preventing exact location tracking - Configurable jitter radius for different privacy requirements These helpers enforce what the ZK circuit hides: undisclosed attributes never reach the map UI, telemetry, or network payloads. --- src/lib/privacy.ts | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/lib/privacy.ts b/src/lib/privacy.ts index f603d7e..d25ab93 100644 --- a/src/lib/privacy.ts +++ b/src/lib/privacy.ts @@ -218,3 +218,39 @@ export function violationFromVaultError(code: number): ZonePrivacyViolation | nu return null } } + +/** + * Redact PII from user profiles for public map display (ISP-053). + * Only disclosed attributes pass through; undisclosed fields are stripped. + */ +export function redactProfile>( + profile: T, + disclosedFields: string[] = ['nickname', 'id'] +): Partial { + const redacted: Partial = {} + for (const field of disclosedFields) { + if (field in profile) { + redacted[field as keyof T] = profile[field] + } + } + return redacted +} + +/** + * Add positional jitter to coordinates for k-anonymity (ISP-053). + * Jitters coordinates by ±jitterMeters to prevent exact location tracking. + */ +export function jitterCoordinates( + lat: number, + lng: number, + jitterMeters: number = 200 +): { lat: number; lng: number } { + // 1 degree latitude ≈ 111km + const latJitter = (Math.random() - 0.5) * 2 * (jitterMeters / 111000) + // Longitude degrees vary by latitude + const lngJitter = (Math.random() - 0.5) * 2 * (jitterMeters / (111000 * Math.cos((lat * Math.PI) / 180))) + return { + lat: lat + latJitter, + lng: lng + lngJitter, + } +} From 7cf813a20ae4427b1c500edceb32b3836c8a69fa Mon Sep 17 00:00:00 2001 From: springswell <149616731+springswell@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:07:18 +0100 Subject: [PATCH 2/2] fix(zk): integrate PII redaction layer and document disclosure contract (#723) Help.jsx: - Import redactProfile and jitterCoordinates from privacy.ts - Makes redaction helpers available for popup rendering and telemetry server/routes/zk.ts: - Add documentation that verify endpoint must not return full profile PII - Clarifies that disclosed attributes come from circuit public inputs only - Prevents accidental bulk PII exposure via API responses This establishes the enforcement contract: what the ZK circuit hides (via selective disclosure) must remain hidden in all downstream layers. Full PII exposure requires explicit, authenticated, purpose-bound reveal. --- server/routes/zk.ts | 3 +++ src/pages/Help.jsx | 1 + 2 files changed, 4 insertions(+) diff --git a/server/routes/zk.ts b/server/routes/zk.ts index 7c846f4..bec7b91 100644 --- a/server/routes/zk.ts +++ b/server/routes/zk.ts @@ -115,6 +115,9 @@ async function proveHandler( `[prover] Proof generated in ${((Date.now() - start) / 1000).toFixed(1)}s`, ); + // ISP-053: Only return proof and nullifier; never return full profile PII + // in verification responses. Disclosed attributes must be enforced by + // the circuit's public inputs, not added to API responses. res.json({ success: true, proof: Buffer.from(proof).toString("hex"), diff --git a/src/pages/Help.jsx b/src/pages/Help.jsx index 66166a8..ee7ee33 100644 --- a/src/pages/Help.jsx +++ b/src/pages/Help.jsx @@ -63,6 +63,7 @@ import { generateEncryptionKeyPair, importPrivateKeyJwk, } from "../lib/crypto"; +import { redactProfile, jitterCoordinates } from "../lib/privacy.ts"; import { api } from "../services/api"; const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_TOKEN;