From 5c856fc5775e7649173b267f1950612fab7ab039 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 27 May 2026 16:36:35 +0400 Subject: [PATCH] docs: parental controls design --- plans/2026-05-27-parental-controls-design.md | 161 +++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 plans/2026-05-27-parental-controls-design.md diff --git a/plans/2026-05-27-parental-controls-design.md b/plans/2026-05-27-parental-controls-design.md new file mode 100644 index 0000000000..150f5f0dd5 --- /dev/null +++ b/plans/2026-05-27-parental-controls-design.md @@ -0,0 +1,161 @@ +# Parental controls — high-level design (iOS first) + +Status: design for discussion. Not an implementation plan yet. +Platform: design and build on **iOS first**, then port the same UI to Kotlin +(Android/Desktop). No changes to the Haskell core for the basic version. + +## What we are building + +Three things, matching the requested direction: + +1. A **parental-controls screen**: a list of on/off switches for functions a + parent wants turned off on a child's device. +2. A **separate parent PIN** that protects that screen (and a few sensitive + settings) — independent from the existing app lock. +3. A **"who is this device for?"** step in onboarding — *for me* or *for a + child* — like Android's setup-for-someone-else. + +## Scope and limitations + +The person using the device is the child, and they physically control it, so +this cannot stop a determined, technically capable child. They could reinstall +the app (local settings reset), export-and-import the database into a clean +install, factory-reset, or observe the parent entering the PIN. This is the +same limitation Apple's Screen Time has. + +The goal is therefore **casual-access prevention and a clear signal of intent**, +not a security boundary, and the feature should not be presented as one. A +separate **Hardening** section below adds *obstacles* (level 2), which raise the +bar but remain defeatable. + +## Privacy boundary: disabling, not monitoring + +Parental controls here only **turn capabilities off**. They never read, copy, +or report the child's messages. This matches SimpleX's published position that +privacy is the safety mechanism. A few candidate functions (preventing the +child from deleting messages, forcing disappearing-messages off) sit close to +that line — they are **off by default and flagged for decision**, because they +lean toward "let the parent inspect," which is monitoring. + +## How it works (architecture) + +- **Parent PIN** reuses the existing passcode machinery. The app already has a + self-destruct passcode — "a second passcode protecting a second thing." The + parent PIN is exactly that pattern: a new stored PIN, the existing PIN-pad + screens reused as-is, and a separate "parent unlocked" state. +- **Restrictions are app-level capability gates.** Each switch is a stored + on/off value; the app reads it at the point where the action starts (the scan + button, the "create group" button, the file-download action) and hides or + blocks it. This is the same pattern the app already uses for settings like + "auto-accept images." +- **Not** the per-conversation feature negotiation (`ChatFeature` / + `GroupFeature`). Those are negotiated *with the contact*, are per-chat, and + are the wrong layer for a one-sided parental block. +- **No Haskell core / protocol changes** for the basic set. The only functions + that *would* need the backend are explicitly excluded (see the table notes). +- Switch values are stored where the **notification and share extensions** can + also read them, so blocks like "don't download files" hold everywhere, not + just in the main app. + +## Connecting: a PIN-to-allow-once exception + +If "connect to new people" is off, the child still needs to reach the parent +and any approved new contact. So **only the connect action** has an exception: +when it is blocked, an alert offers to allow it once after the parent PIN is +entered, and that **single** connection then proceeds. Everything else, when +off, stays off — to permit something else temporarily, the parent opens the +PIN-protected settings screen and changes it there. + +We do **not** build a "set up contacts then finalize" wizard: a parent can +connect to the child normally before turning restrictions on, which needs no +new code. + +## The functions a parent can turn off + +Legend — **Complexity**: rough build cost (Low / Med / High). +**Intrusiveness**: how much it limits the child's normal use and privacy +(Low / Med / High) — the autonomy trade-off, for discussion. + +### Protective — the primary purpose + +| Function | What "off" does / how | Complexity | Intrusiveness | +|---|---|---|---| +| Connect to new people (scan QR, open/paste link, create own address, 1-time invite, accept request) | Hide/disable these entry points. Child can still reach parent/approved people via the connect-PIN exception (below). **Highest-value control.** | Med (several entry points + the allow-once PIN piece) | Med | +| Create / join groups & channels | Hide "create"; block joining via link. | Low–Med | Med | +| Audio / video calls | Hide call buttons (outgoing); auto-decline incoming. | Med (incoming needs handling) | Med | +| Send files, images, videos | Remove attach options from the composer. | Low | Low–Med | +| Send voice messages | Remove the voice-record button. | Low | Low | +| Receive / open files & media | Turn off auto-download + block the download/open action — **in the app and the notification/share extensions.** Client-side only. (Truly stopping the file *offer* from arriving = backend + wrong layer = excluded.) | Med–High (multi-surface) | Med | +| Incognito mode | Hide incognito toggles so all connections use the known profile. | Low | Low | + +### Integrity locks — prevent the child from bypassing the controls + +Without these, the child could simply disable the protective switches, defeating +their purpose. These stay reachable to the parent but are **gated behind the +parent PIN** (the parent can still use them). + +| Function | Why it's locked | Complexity | +|---|---|---| +| The parental-controls screen + parent PIN | The control itself. | Low (it's the gate) | +| Create / hide additional profiles | A hidden profile bypasses everything. | Low–Med | +| Database export / import | Bypasses the controls; data exfiltration. | Low | +| Network / server settings, custom servers, developer tools | Ways to bypass restrictions; debug access. | Low | +| App lock / passcode settings | So the child can't lock the parent out. | Low | + +### For decision — high intrusiveness, off by default + +These edge toward monitoring. Listed for the decision, not recommended for +"basic." + +| Function | Note | Complexity | Intrusiveness | +|---|---|---|---| +| Prevent deleting messages (local + delete-for-everyone) | Lets a parent inspect history → monitoring-adjacent. | Med | High | +| Force disappearing-messages off | Also a *negotiated* feature → would need the backend, wrong layer. | Med (backend) | High | +| Prevent clearing / deleting chats | Monitoring-adjacent. | Low | High | + +*Anything not in these tables that is wanted, we add as a row with the same +three judgements before building it.* + +## Onboarding "who is this for?" + +A new early onboarding step with two choices. Picking **for a child** routes +the parent into the parental setup (set the parent PIN, confirm a pre-checked +default set of restrictions), then continues the normal flow and hands the +device over. **for me** is the normal flow unchanged. The same setup is also +reachable later from Settings → Privacy & security → Parental controls, so +existing installs and later changes are covered. The onboarding step stays +small because it reuses the settings setup screen. + +*(Working assumption — change to a lighter "configure in Settings" nudge if +preferred.)* + +## Hardening (level 2 — separate, optional) + +Obstacles that raise the bar further. Still defeatable; build after the basic +version works. + +- Persist a "parental mode was on" marker that **survives reinstall**, so a + fresh install re-prompts for the parent PIN before allowing unrestricted use. +- Block database export while restrictions are on. +- (Future, out of scope) A real boundary would need authority held on a + **separate parent device** via SimpleX's remote-control/linking — a new + subsystem, worth its own RFC if ever wanted. + +## Suggested build order + +1. Parent PIN + PIN-gated parental-controls screen (reusing existing passcode + UI) + the integrity locks. +2. Protective switches and their gates — connect first (highest value), then + the rest. +3. The connect "allow-once" PIN exception. +4. Onboarding "who is this for?" step. +5. Hardening (level 2). +6. Port everything to Kotlin (pure UI; no core changes). + +## Open decisions + +- The contents of the **default restriction set** pre-checked for "for a child." +- Which **for-decision** rows (if any) are in scope. +- Onboarding behaviour: **(a)** route into setup *(assumed)* vs **(b)** light + nudge. +- Confirm connect exception = **PIN-to-allow-once, connect only** *(assumed)*.