# Security Policy ## Supported versions Lagebuch is pre-1.0 and moves quickly. Only the **latest release** receives security fixes; because the project has not reached 1.0, breaking changes between versions are expected and older releases are not patched. | Version | Supported | |---------|-----------| | latest release | :white_check_mark: | | earlier releases | :x: | ## Reporting a vulnerability **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** Use [GitHub's private vulnerability reporting](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability) instead: 1. Go to the repository's **Security** tab. 2. Click **Report a vulnerability**. 3. Describe the issue, including: - the affected version (installer file name or tag, e.g. `v0.3.0`) - platform(s) affected — Windows, Linux, macOS, Android - steps to reproduce or a proof of concept - the impact you believe it has Reports are reviewed promptly. Please give maintainers reasonable time to address the issue before any public disclosure. ## Scope notes - **Master data and incident files stay on device**, with one exception: when a device hosts an incident, it serves its full master-data set — including the personnel roster's names and phone numbers — to every device that joins that incident, over the same TLS + share-PIN-gated channel as everything else. A joining device never persists what it receives; nothing is uploaded by the app itself. Note that the roster is standing organizational data (the whole brigade, across every incident), unlike the incident payload, which is a single event — so a compromised PIN exposes something with a longer useful life than the operation the PIN was issued for. `masterdata.db` and each incident's `.fwincident` file otherwise live in local application data. - **The full data-protection picture** — every category of personal data, all storage locations, what the sync transmits, the split of technical and organizational measures, and the deletion checklist — is documented in German in [`docs/datenschutz-und-sicherheit.md`](docs/datenschutz-und-sicherheit.md), written for the Kreisbrandinspektionen and Datenschutzbeauftragte who have to approve a deployment. - **Multi-device sync** runs over LAN/Tailscale via SignalR and requires a share PIN to join an incident. Issues affecting that transport, the PIN gate, or the PDF export pipeline are very much in scope. ## Known limitations The German [`docs/datenschutz-und-sicherheit.md`](docs/datenschutz-und-sicherheit.md) lists the same limits under "Bekannte Grenzen". This is that list, in that order, cut for a researcher rather than for a Datenschutzbeauftragter, plus one note on why the trust model around the middle two is where it is. - **The installers are not code-signed.** Every install path makes the user click past an operating-system warning — SmartScreen on Windows, quarantine on macOS, unknown sources on Android — so there is no OS-level integrity check on the package, and the flow trains users to dismiss exactly the dialog that would flag a tampered one. The SignPath Foundation reviewed the project and declined; OSSign requires six months of account, organisation and project history, which puts the earliest possible free Windows certificate at February 2027. Until then the verifiable substitutes are each release's `SHA256SUMS.txt` and its Sigstore-backed build attestation (`gh attestation verify --repo CodeForFire/lagebuch`). See the [roadmap](ROADMAP.md). - **The first connection to a host is unauthenticated.** Sync pins the host's key Trust-on-First-Use: the joining device records the SHA-256 of the certificate's public key (SPKI) in `trust.json` on first contact and from then on accepts only that key. The host issues a new self-signed certificate per share but keeps one ECDSA P-256 key per install (`host-key.pem` in the app-data folder, owner-only on Unix), so a restarted share, app or laptop is still the same pin. A different key aborts the connection and the error shows its Kennung — the first 60 bits of the SPKI hash, 12 Crockford base32 characters, which the host displays behind its PIN. Trusting it after comparing pins exactly the key that was shown, never whatever answers the retry. 60 bits is a trade between what a person compares during an Einsatz and the cost of grinding a key to match a known host's Kennung (about 2^60 key generations, years on current hardware); a presented key whose Kennung equals the pinned one's while the key differs can only be such a ground key, and is refused with nothing offered to trust. Host certificates carry a fixed subject so a client can tell an older host, which shows no Kennung, from a changed one. Whole-certificate pins from before the persistent key are upgraded only if the presented certificate still matches them; otherwise the Kennung is asked for like any other change. That key comparison is the *whole* client-side trust decision — the certificate is self-signed and its SAN covers only `localhost` and loopback while the host binds every interface, so chain and hostname validation are replaced outright rather than layered on. The first exchange is not compared, though: nothing asks for the Kennung on first contact, so an attacker already positioned on the LAN at that moment can get themselves pinned instead, and every later connection will then look correct. Anyone who can read `host-key.pem` can impersonate that host, which is the same user-profile boundary that already guards the Einsatzdaten: the app-data folder is created with the default umask, and on Windows it is the roaming `%AppData%`, so a roaming profile carries the key to the profile server along with everything else stored there. ([#288](../../issues/288)) - **The share PIN is four digits, and the host budgets wrong guesses.** It is drawn per share from `RandomNumberGenerator` and held in memory only on the host; a joined device keeps it in `last-connection.json` so it can reconnect without asking. The host spends one budget of ten wrong PINs per PIN across *every* address — there is no per-address backoff, which only slowed guessing without capping it and was sidestepped by a peer with many addresses: once it is gone, new joins are refused — even with the right PIN — until the Lagebuchführer draws a new one. Devices already joined keep working. The renewal is deliberately manual: a PIN that renewed itself would only spread the same guesses across rotations. So an attacker gets at most ten guesses in 10,000 per renewal a person has to make, and the closed state is the alarm that someone is guessing. A peer whose remote address does not resolve is refused outright. What remains is that an attacker on the LAN can close joins on purpose with ten wrong PINs, and again after every renewal, which blocks new devices but not the ones already joined ([#288](../../issues/288)). - **The PIN plus network reachability is the entire access-control boundary.** A device that can reach the host over the LAN/Tailscale link and knows the (guess-budgeted, TLS-protected) PIN is already fully trusted to make arbitrary changes to the incident, regardless of what operator name it claims. A compromised or careless device can misattribute its own edits to a different operator, but it could just as easily make those edits under its own name — the trust decision has already been made by that point. The host also listens on every interface (`ListenAnyIP`), with no restriction to particular address ranges. - **Operator identity is self-asserted per device, not verified.** The "Wer dokumentiert?" name/call sign attached to each sync command travels with that command and is trusted by the host as-is — there is no server-side lookup against any registered or authenticated identity. It is used **only** for display and attribution (an ETB entry's "entered by", an incident's "closed by", a file's "added by" metadata), never as an authorization gate: once a device is past the PIN gate, every operator name can perform every mutation. - **This is an accepted trade-off, not an oversight.** The app's threat model is a volunteer fire department's own devices on its own network, not an adversarial multi-tenant system. Verifying operator identity server-side would require inventing a session/identity-binding concept that doesn't exist today — the SignalR hub carries no client-callable methods, and sync commands travel over stateless HTTP POST — for a marginal reduction of an already low-severity risk. If stronger attribution is ever wanted, a cheap next step would be recording the source device/IP alongside the claimed operator name in ETB entries, without requiring a full identity-binding redesign. - **Image metadata is not stripped from attachments.** Image bytes are stored verbatim, so a photo taken on a duty phone keeps whatever the camera wrote into it — GPS coordinates, capture time, device model — inside the incident's `.files` folder, in what the host serves to every joined device, and in the exported PDF report that goes to the Akte. The coordinates are typically a private address, often the same one whose resident is already named in the CO-Messprotokoll. PNG, WebP and GIF metadata chunks are equally untouched. Stripping on ingest is planned ([#384](../../issues/384)). - **Attachment copies outlive the session that created them.** A joining device keeps every attachment it pulled in `attachment-cache/` after the connection ends, and the cache is never cleared automatically ([#382](../../issues/382)); opening an attachment additionally leaves a working copy under `lagebuch/` in the system temp directory, one fresh directory per open, also never cleaned up ([#383](../../issues/383)). Both are plaintext copies of personal data outside the incident file, so both are on the German page's deletion checklist. - **The file format is not frozen before 1.0.** Older `.fwincident` files are migrated forward on open, and the guarantee that a file stays readable indefinitely starts at 1.0, not now. - **There are no log files.** Good for data protection — no second place on disk accumulates personal data — and bad for diagnosis: a crash leaves nothing behind to attach to a report, including a crash in the sync or export path that a reporter would want evidence for. ## Non-security issues Anything that is not a vulnerability (crashes, data-entry problems, feature requests) belongs in the [issue tracker](../../issues) using the regular issue templates.