# PIR-Server — iOS NEURLFilter Backend

**Status: LIVE (Staging)** — `https://pir.staging.rebreak.org` · Stand 2026-05-21

Dieses Dokument beschreibt den PIR-Server vollständig, damit eine andere Session
ihn verstehen, betreiben und weiterentwickeln kann.

---

## 1. Was & Warum

Rebreaks iOS-Gambling-Blocker nutzt Apples **`NEURLFilter`** (iOS 26). NEURLFilter
filtert URLs **systemweit** (Safari + alle WebKit-Browser + `URLSession`) und
verlangt dafür **zwingend** einen vom Vendor gehosteten **PIR-Server** (Private
Information Retrieval) — `NEURLFilterManager.setConfiguration()` akzeptiert keine
Konfiguration ohne `pirServerURL`. Dieser Server ist dieser PIR-Server.

**Ablauf zur Laufzeit:**
1. Das Gerät prüft die URL gegen einen lokalen **Bloom-Filter** (Prefilter, in der
   iOS-App-Extension `NEURLFilterControlProvider`).
2. Bei einem Bloom-Treffer → **PIR-Lookup** gegen diesen Server, geroutet über
   Apples Oblivious-HTTP-Relay.
3. Per Homomorphic Encryption sieht der Server die geprüfte URL **nie im Klartext**.

**DSGVO/DiGA-Vorteil:** Kein Beteiligter sieht URL + Identität zusammen — Rebreak
erfährt das Surfverhalten der User nie. Datenminimierung per Krypto-Konstruktion.

---

## 2. Architektur-Bausteine

Basiert auf Apples Open-Source-Vorlagen:
- `apple/pir-service-example` → `PIRService` (Swift/Hummingbird-HTTP-Server, bedient
  PIR-Queries + eingebauten PrivacyPass-Issuer) + `ConstructDatabase`.
- `apple/swift-homomorphic-encryption` → `PIRProcessDatabase` (wandelt `input.txtpb`
  in PIR-optimierte Shards).

---

## 3. Deployment-Stand (Hetzner `rebreak-server`, 49.13.55.22)

| Komponente | Ort / Wert |
|---|---|
| Docker-Container | `pir-service-staging`, `restart=unless-stopped`, `127.0.0.1:8090->8090` |
| Docker-Image | `pir-service-staging:latest` (Multi-Stage Swift-Build, ~5 GB) |
| Build-Repos | `/srv/pir-build/{pir-service-example,swift-homomorphic-encryption}` + `Dockerfile` |
| Deploy-Verzeichnis | `/srv/pir-server/` (`build-and-deploy.sh`, `gen-test-input.sh`, `config/`, `data/`) |
| PIR-DB-Artifacts | `/srv/pir-server/data/` — `input.txtpb` + `url-0..3.bin` + `url-0..3.params.txtpb` (4 Shards) |
| Service-Config | `/srv/pir-server/config/service-config.json` (Token inline — NICHT im Repo) |
| nginx | `/etc/nginx/sites-{available,enabled}/pir-staging.rebreak.org`, `server_name pir.staging.rebreak.org` |
| TLS | Let's Encrypt für `pir.staging.rebreak.org`, läuft 2026-08-18 ab, Auto-Renew aktiv |
| Public-URL | `https://pir.staging.rebreak.org` |

---

## 4. Konfiguration

**`service-config.json`** (auf dem Server; Token nicht im Repo → siehe `service-config.template.json`):
- usecase-Name: **`org.rebreak.app.url.filtering`** — muss EXAKT mit der iOS-`setConfiguration` matchen, sonst HTTP 404.
- `shardCount`: **4** (215k Domains / 50k pro Shard).
- `users[].tokens`: enthält den `PIR_AUTH_TOKEN`.

**`PIR_AUTH_TOKEN`** — liegt in **Infisical**, Env `staging`, Key `PIR_AUTH_TOKEN`,
Project-ID `14b11b35-ef59-4b8a-a16b-398f0cc3ad93`. App-weit halten (NICHT pro-User —
ein per-User-Token würde die PIR-Nicht-Verkettbarkeit schwächen).

**`process-config.json`** — Parameter für `PIRProcessDatabase`:
`entryCountPerShard 50000`, `rlweParameters n_4096_logq_27_28_28_logt_5`,
`databaseType keyword`.

**`PIR_ISSUER_REQUEST_URI`** — Env-Var am Container (`docker run -e ...`), gesetzt von
`build-and-deploy.sh` auf `https://pir.staging.rebreak.org/issue`. **Zwingend absolut**
(RFC 9578 §6): das Apple-`pir-service-example` advertised von Haus aus nur ein
**relatives** `/issue` im Issuer-Directory (`PrivacyPassController.swift`, hardcodiert) —
der NEURLFilter-Client kann damit keinen Privacy-Pass-Token holen und scheitert mit
`serverSetupIncomplete`. Behoben durch `patches/0001-absolute-issuer-request-uri.patch`,
das die URL aus dieser Env-Var liest.

---

## 5. iOS-Integrationswerte (`NEURLFilterManager.setConfiguration`)

| Parameter | Wert |
|---|---|
| `pirServerURL` | `https://pir.staging.rebreak.org` |
| `pirPrivacyPassIssuerURL` | `https://pir.staging.rebreak.org` (PIRService bedient beides) |
| `pirAuthenticationToken` | Infisical `staging/PIR_AUTH_TOKEN` |
| usecase-Name | `org.rebreak.app.url.filtering` |
| `controlProviderBundleIdentifier` | `org.rebreak.app.URLFilterExtension` (geplant — beim iOS-Scaffolding final festlegen) |

---

## 6. Daten-Pipeline (Blocklist → PIR-DB)

1. `backend/scripts/generate-pir-input.ts` erzeugt `input.txtpb` aus ~215k Gambling-Domains.
   - **DB-Modus** (Default): liest die `BlocklistDomain`-Tabelle — braucht `infisical run` für `DATABASE_URL`.
   - **HaGeZi-Modus** (`--source hagezi`): direkt von HaGeZis `gambling.txt`, kein DB/Infisical
     nötig. **Aktuell verwendet**, weil `infisical` nicht auf dem Dev-Mac liegt. Äquivalent —
     die `BlocklistDomain`-Tabelle wird ohnehin aus genau dieser HaGeZi-Liste gespeist.
   - Output-Format: pro Domain eine Zeile `rows: { keyword: "<domain>" value: "1" }`.
2. `input.txtpb` → per `scp` nach `/srv/pir-server/data/input.txtpb`.
3. `PIRProcessDatabase /data/process-config.json` (im Container) → `url-N.bin` + `url-N.params.txtpb`.

---

## 7. Betrieb / Runbook

**Health-Check:**
```bash
curl -s -o /dev/null -w '%{http_code}\n' \
  https://pir.staging.rebreak.org/.well-known/private-token-issuer-directory   # → 200
ssh rebreak-server 'docker ps | grep pir-service-staging'
```

**Blocklist aktualisieren:**
1. Neue `input.txtpb` generieren (`backend/scripts/generate-pir-input.ts`) + nach
   `/srv/pir-server/data/input.txtpb` scp'en.
2. Alte Shards löschen: `ssh rebreak-server 'rm /srv/pir-server/data/url-*.bin /srv/pir-server/data/url-*.params.txtpb'`.
3. `PIRProcessDatabase` neu laufen lassen (siehe `build-and-deploy.sh` Step 6) — erzeugt neue Shards.
4. `shardCount` in `service-config.json` prüfen + Container neu starten.

**Komplett neu bauen/deployen:**
```bash
ssh rebreak-server 'bash /srv/pir-server/build-and-deploy.sh'
```
Swift-Build ~20–30 Min, RAM-intensiv (~2–3 GB → Swap nötig, off-peak bauen).

---

## 8. Bekannte Issues / TODOs

- ✅ **`build-and-deploy.sh` shardCount-Bug (BEHOBEN 2026-05-21):** Step 3 leitet den
  `shardCount` jetzt aus den vorhandenen `url-*.bin`-Artifacts ab statt ihn fix auf `1`
  zu setzen — kein Regress mehr von 4 Shards auf 1 bei Re-Runs.
- ✅ **Relatives `issuer-request-uri` (BEHOBEN 2026-05-21):** siehe §4 `PIR_ISSUER_REQUEST_URI`.
  War die Ursache für `serverSetupIncomplete` auf der iOS-Seite. Fix via
  `patches/0001-absolute-issuer-request-uri.patch`, von `build-and-deploy.sh` Step 3b
  automatisch auf den `pir-service-example`-Clone angewandt.
- Deploy-Skripte liegen sowohl auf dem Server (`/srv/pir-server/`, `/srv/pir-build/`)
  als auch hier im Repo. Bei Änderungen synchron halten — oder das Repo als
  Single-Source-of-Truth etablieren (empfohlen).
- **App-Store-Distribution:** braucht zusätzlich Apples OHTTP-Onboarding-Formular,
  ein OHTTP-Gateway und einen DNS-TXT-Record `apple-url-filter=org.rebreak.app`.
  Für TestFlight/Dev NICHT nötig. → Phase 2.
- **Entitlement** `com.apple.developer.networking.networkextension.url-filter-provider`:
  Dev-Builds ausgenommen; vor TestFlight/Store über den „Capability Requests"-Tab im
  Apple-Developer-Portal (Certificates, Identifiers & Profiles → Identifiers) beantragen.
- Apple zu `pir-service-example`: „example service, not for production" —
  Produktionshärtung (persistente PrivacyPass-Keys statt ephemerer, Monitoring,
  Ressourcen-Tuning) ist ein eigener späterer Schritt.

---

## 9. Dateien in diesem Verzeichnis

| Datei | Zweck |
|---|---|
| `README.md` | dieses Dokument |
| `Dockerfile` | Multi-Stage Swift-Build (Kopie von `/srv/pir-build/Dockerfile`) |
| `build-and-deploy.sh` | Deploy-Script (Kopie von `/srv/pir-server/`) |
| `gen-test-input.sh` | Test-DB-Generator (10 Domains, Fallback) |
| `process-config.json` | `PIRProcessDatabase`-Parameter |
| `service-config.template.json` | Service-Config-Vorlage (Token-Platzhalter) |
| `nginx-pir.staging.rebreak.org.conf` | nginx-Reverse-Proxy-Config (Referenz) |
| `patches/*.patch` | Quell-Patches für `pir-service-example`, von `build-and-deploy.sh` Step 3b angewandt |

Der `input.txtpb`-Generator selbst liegt bei `backend/scripts/generate-pir-input.ts`.
