docs: switch Privacy Monitor default to XposedOrNot (free, keyless)
Email breach endpoints need no API key (2 req/s, cached), so Privacy Monitor ships enabled by default. HIBP kept as a swappable key-based alternative; provider chosen via Privacy:Provider config. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,28 @@
|
||||
# Spec: Privacy Monitor (data-breach checking)
|
||||
|
||||
Checks the user's email address (and optionally addresses they've corresponded with)
|
||||
against known data breaches. Spec'd around **Have I Been Pwned (HIBP)**, shipped behind
|
||||
a config flag that stays **off until an API key is provided** — zero cost until then.
|
||||
Checks the user's email address against known data breaches. **Default provider:
|
||||
[XposedOrNot](https://xposedornot.com/api_doc)** — free, no API key required for email
|
||||
endpoints. HIBP is kept as a swappable alternative behind a key. Because it's free and
|
||||
keyless, Privacy Monitor ships **enabled by default** (it only ever checks the
|
||||
signed-in user's own address).
|
||||
|
||||
## 1. Configuration
|
||||
## 1. Provider details (XposedOrNot)
|
||||
|
||||
No auth for email endpoints. Rate limit: **2 req/sec per IP** (we cache, so this is a
|
||||
non-issue). Two endpoints:
|
||||
|
||||
| Purpose | Request | Returns |
|
||||
|---------|---------|---------|
|
||||
| Quick check | `GET https://api.xposedornot.com/v1/check-email/{email}` | `{ "breaches": [["Name1","Name2",…]], "email", "status":"success" }`; or `{ "Error":"Not found", "email":null }` when clean |
|
||||
| **Rich analytics** (what we use) | `GET https://api.xposedornot.com/v1/breach-analytics?email={email}` | `BreachesSummary`, **`ExposedBreaches`** (entity name, industry, risk level, exposed data types, year, record count), `BreachMetrics`, `ExposedPastes` |
|
||||
|
||||
We call **`breach-analytics`** to populate the rich UI. A `404`/`Error:"Not found"`
|
||||
means "no breaches" → return empty, not an error. We call the REST API directly via a
|
||||
named `HttpClient` (consistent with the existing `ollama`/`openai`/`unsubscribe`
|
||||
clients); the official `XposedOrNot-DotNet` SDK exists but we avoid the extra dependency
|
||||
+ audit surface.
|
||||
|
||||
## 2. Configuration
|
||||
|
||||
`PrivacyOptions` (new, `Configuration/Options.cs`):
|
||||
|
||||
@@ -12,21 +30,35 @@ a config flag that stays **off until an API key is provided** — zero cost unti
|
||||
public class PrivacyOptions
|
||||
{
|
||||
public const string SectionName = "Privacy";
|
||||
public bool Enabled { get; set; } = false; // master flag
|
||||
public string Provider { get; set; } = "Hibp"; // "Hibp" | "None"
|
||||
public string? HibpApiKey { get; set; } // required for Hibp
|
||||
public int CacheHours { get; set; } = 24; // don't hammer the API
|
||||
public bool Enabled { get; set; } = true; // free + keyless → on by default
|
||||
public string Provider { get; set; } = "XposedOrNot"; // "XposedOrNot" | "Hibp" | "None"
|
||||
public string? HibpApiKey { get; set; } // only needed if Provider="Hibp"
|
||||
public int CacheHours { get; set; } = 24; // don't re-check more than daily
|
||||
}
|
||||
```
|
||||
|
||||
`appsettings.json` gains a `Privacy` section with `Enabled: false`, empty key.
|
||||
`IsEnabled => Enabled && Provider == "Hibp" && !string.IsNullOrWhiteSpace(HibpApiKey)`.
|
||||
|
||||
## 2. Provider abstraction
|
||||
`appsettings.json` gains a `Privacy` section: `Enabled: true`, `Provider:
|
||||
"XposedOrNot"`, empty `HibpApiKey`.
|
||||
|
||||
```csharp
|
||||
public record BreachDto(string Name, string Title, string Domain, DateOnly BreachDate,
|
||||
IReadOnlyList<string> DataClasses, bool IsVerified, string? Description);
|
||||
// XposedOrNot needs no key; Hibp does.
|
||||
IsEnabled => Enabled && Provider switch {
|
||||
"XposedOrNot" => true,
|
||||
"Hibp" => !string.IsNullOrWhiteSpace(HibpApiKey),
|
||||
_ => false
|
||||
};
|
||||
```
|
||||
|
||||
> **Privacy note:** this sends the user's own email address to a third-party service.
|
||||
> That's the feature's purpose and it's the signed-in user's own address, but it stays
|
||||
> a single config flag away from off, and we never check anyone else's address.
|
||||
|
||||
## 3. Provider abstraction
|
||||
|
||||
```csharp
|
||||
public record BreachDto(string Name, string Title, string? Domain, int? Year,
|
||||
IReadOnlyList<string> DataClasses, string? RiskLevel,
|
||||
string? Industry, string? LogoUrl, string? Description);
|
||||
|
||||
public interface IBreachProvider
|
||||
{
|
||||
@@ -36,12 +68,20 @@ public interface IBreachProvider
|
||||
}
|
||||
```
|
||||
|
||||
- **`XposedOrNotBreachProvider`** (default) — `GET /v1/breach-analytics?email={url-encoded}`
|
||||
on the `"xposedornot"` named client. Map `ExposedBreaches.breaches_details[]`
|
||||
(`breach`, `xposed_data` → `DataClasses`, `xposed_date`/`year`, `industry`,
|
||||
`risk` → `RiskLevel`, `logo`, `details` → `Description`) into `BreachDto`. Treat
|
||||
`Error:"Not found"` / `404` as clean (empty). `429` → respect backoff, return
|
||||
cached/empty. No key, no `user-agent` requirement.
|
||||
- `HibpBreachProvider` — `GET https://haveibeenpwned.com/api/v3/breachedaccount/{account}?truncateResponse=false`,
|
||||
header `hibp-api-key`, a descriptive `user-agent`. Handle: `404` = no breaches (return
|
||||
empty), `401` = misconfig (log, treat as disabled), `429` = rate-limited (respect
|
||||
`Retry-After`, return cached/empty). Uses a named `HttpClient` "hibp" with a sane timeout.
|
||||
- `NullBreachProvider` — `IsEnabled => false`, returns empty. Registered when the flag is off
|
||||
(mirrors the `NullAiProvider` pattern in `DependencyInjection`).
|
||||
header `hibp-api-key`, descriptive `user-agent`. `404` = clean, `401` = misconfig (log,
|
||||
disable), `429` = back off. Used only when `Provider="Hibp"` + key set.
|
||||
- `NullBreachProvider` — `IsEnabled => false`, returns empty. Registered when
|
||||
`Provider="None"` or the chosen provider isn't usable (mirrors `NullAiProvider`).
|
||||
|
||||
DI selects the provider by `PrivacyOptions.Provider` (switch in `DependencyInjection`,
|
||||
same shape as the AI provider selection).
|
||||
|
||||
## 3. Service
|
||||
|
||||
@@ -85,7 +125,9 @@ never arbitrary addresses (avoids turning the app into a breach-lookup tool for
|
||||
## 6. Security & safety
|
||||
|
||||
- [ ] Only the authenticated user's own address is ever checked (no lookup of others).
|
||||
- [ ] API key read from config/secrets, never logged, never sent to the client.
|
||||
- [ ] Feature fully inert (endpoints return `{ enabled:false }`, nav hidden) until a key is set.
|
||||
- [ ] Rate-limit/backoff respected; failures degrade to cached/empty, never crash.
|
||||
- [ ] HIBP responses cached to minimize external calls and avoid leaking usage patterns.
|
||||
- [ ] Any API key (HIBP path) read from config/secrets, never logged, never sent to the client.
|
||||
- [ ] Feature can be fully disabled via `Privacy:Enabled=false` or `Provider="None"`
|
||||
(endpoints return `{ enabled:false }`, nav hidden).
|
||||
- [ ] Rate-limit/backoff respected (XposedOrNot 2 req/s); failures degrade to cached/empty, never crash.
|
||||
- [ ] Provider responses cached (`CacheHours`) to minimize external calls and avoid leaking usage patterns.
|
||||
- [ ] Email is URL-encoded into the request path/query; no other PII is sent.
|
||||
|
||||
Reference in New Issue
Block a user