Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# The Dockerfile copies only pyproject.toml and src/. Everything listed here is
# The Dockerfile copies only pyproject.toml, src/ and PRIVACY.md. Everything here is
# context uploaded to the Docker daemon on every build for nothing.
#
# app/ is the reason this file exists: it holds the Xcode project and, since the
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ RUN mkdir -p src/flightforms && \

# Copy application source
COPY src/ src/
# Served at /privacy (api/privacy.py resolves it next to src/)
COPY PRIVACY.md .

# Create data directory
RUN mkdir -p /app/data && chown app:app /app/data
Expand Down
55 changes: 47 additions & 8 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ FlightForms handles sensitive personal data — passport numbers, dates of birth

## Core Principle

**Your personal data is yours.** It is stored on your devices, encrypted by Apple, and never retained by the server.
**Your personal data is yours.** It is stored on your devices, encrypted by the operating system, and never retained by the server.

## On-Device Storage (iOS/macOS App)

Expand All @@ -16,6 +16,22 @@ All personal data (crew, passengers, travel documents, flights) is stored locall
- **No access by FlightForms** — we have no copy of, and no way to read, your CloudKit private data. It is held by Apple under your own iCloud account and Apple's terms. By default Apple manages the iCloud encryption keys; if you turn on [Advanced Data Protection](https://support.apple.com/en-gb/102651), the keys are held only on your devices and Apple cannot read the data either
- **Authentication tokens in Keychain** — JWT credentials are stored in the iOS/macOS Keychain, the most secure storage available on Apple platforms

## On-Device Storage (Android App)

All personal data (crew, passengers, travel documents, aircraft, flights) is stored locally in the app's own database on the phone.

- **Encrypted at rest** — Android's file-based encryption protects the app's storage whenever the phone has a screen lock
- **Stays on the phone** — there is no cloud sync, and the app opts out of Android backup and device-to-device transfer, so the data is not copied to your Google account or to a new phone
- **Moving to another device is your choice** — *Settings → Move my data* writes one file, encrypted with a passphrase the app generates and shows you; the file only goes where you send it. A plain, unencrypted export is also available for your own records
- **No access by FlightForms** — we have no copy of, and no way to read, the data on your phone
- **Authentication tokens in the Android Keystore** — sign-in credentials are stored in encrypted preferences backed by the Keystore

## Passport Scanning

Scanning a passport reads the machine-readable zone (the two lines of `<<<` characters) with on-device text recognition — Apple's Vision framework on iPhone, iPad and Mac, Google's ML Kit on Android. The image is processed on the device, is not kept, and is never sent to the FlightForms server or to anyone else; only the text you accept is saved to the person's record.

**One disclosure for Android:** ML Kit, which is part of the app, sends Google usage and diagnostic metrics about the text-recognition API — device model and OS version, app version, a per-installation identifier, how long recognition took, image size and format, and error codes. It does **not** send the image or the recognised text ([Google's ML Kit terms](https://developers.google.com/ml-kit/terms), [data disclosure](https://developers.google.com/ml-kit/android-data-disclosure)). The app only starts ML Kit when you open the passport scanner, so if you never scan, none of this is sent; once you have scanned, Google may send the metrics later in the background. Google offers no switch to turn the metrics off. The iPhone, iPad and Mac apps send nothing comparable.

## Server-Side Processing

The FlightForms API server (`forms.flyfun.aero`) is **stateless with respect to personal data**.
Expand All @@ -25,6 +41,7 @@ The FlightForms API server (`forms.flyfun.aero`) is **stateless with respect to
- **No PII in error messages** — error responses contain generic messages, never personal data or internal details.
- **HTTPS enforced** — all communication uses TLS encryption. HSTS headers ensure browsers and clients never downgrade to plain HTTP.
- **Authenticated access** — form generation requires authentication (OAuth or API token). Unauthenticated requests are rejected.
- **Rate limited** — each account can fill a bounded number of forms per hour, which limits what a stolen token could be used for.

## Why Form Generation Uses a Server

Expand All @@ -47,8 +64,25 @@ The server-side approach lets us use mature, well-tested open-source libraries w

The server stores only:

- **User accounts** — email address and OAuth provider identifier, used for authentication
- **Usage records** — which airport, which form, and when (no personal data)
- **User accounts** — email address, display name and sign-in provider identifier, used for authentication. The account is shared with [FlyFun Weather](https://weather.flyfun.aero).
- **Usage records** — which airport, which form, and when. No crew or passenger data.

## Passengers

If you are a passenger or crew member and a pilot has entered your details into FlightForms, this section is for you.

- **Where your details are** — in the FlightForms app on the pilot's own devices. On iPhone, iPad and Mac they sync through the pilot's private iCloud account; on Android they stay on the pilot's phone. FlightForms (the developer) has no copy and cannot see them.
- **What they are used for** — filling in the customs, immigration and airport forms the flight requires. The pilot sends those forms to the authorities that ask for them, from their own email or the airport's own website; FlightForms never sends anything on their behalf.
- **The server keeps nothing** — to fill a form, the details pass through our server for the moment it takes, over an encrypted connection, and are then discarded.
- **Your rights** — the pilot (or the organisation they fly for) decides what is kept, so ask them to show, correct or delete your details. The app lets them delete a person, or everything, in one step.

Pilots can share a short version of this with their passengers from the app's *Settings → Privacy note for passengers*.

## Deleting Your Data

- **Delete Account** (Settings) permanently deletes your FlightForms account and your usage records from our server. It does **not** delete the people, aircraft, flights and trips in the app, because those were never on our server.
- **Delete All Data** (Settings) deletes every person, travel document, aircraft, flight and trip from the app. On iPhone, iPad and Mac this also removes them from your iCloud, and so from all your devices signed in to the same Apple ID. It does not affect your account.
- A small record of form costs is kept for accounting after an account is deleted. It is keyed by a random identifier that no longer maps to anyone once the account is gone.

## Network Security

Expand All @@ -60,7 +94,7 @@ The server stores only:

## Temporary Files

When you generate a form, the filled file is written to the app's temporary directory on your device so it can be saved, shared or emailed. The app does not currently delete this file afterwards; it stays until the operating system clears the temporary directory. The file never leaves your device unless you send it, and it is covered by the same at-rest encryption as the rest of the app's data.
When you generate a form, the filled file is written to the app's temporary storage on your device so it can be saved, shared or emailed. On iPhone, iPad and Android the app deletes it as soon as the share sheet or mail composer closes. On a Mac, if you open the form in another app, reveal it in Finder, or send it with Mail or a sharing service, the file is kept briefly because that app is still reading it; it is deleted when you next generate a form (once it is more than 15 minutes old) or the next time the app starts. The file never leaves your device unless you send it, and it is covered by the same at-rest encryption as the rest of the app's data.

## CLI Tool

Expand All @@ -70,10 +104,15 @@ The command-line tool sends the same data to the server for form generation. If

| Layer | Protection |
|-------|------------|
| On-device storage | Apple Data Protection encryption + SwiftData |
| Cross-device sync | CloudKit private database (encrypted, single-account access) |
| Auth credentials | iOS/macOS Keychain |
| On-device storage (Apple) | Apple Data Protection encryption + SwiftData |
| On-device storage (Android) | Android file-based encryption; no cloud backup |
| Cross-device sync | CloudKit private database on Apple (encrypted, single-account access); none on Android |
| Auth credentials | iOS/macOS Keychain; Android Keystore |
| Network transport | TLS / HTTPS with HSTS |
| Server processing | In-memory only, no persistence of personal data |
| Server logs | Usage metrics only, no PII |
| Temporary files | Kept on device until the OS clears them, encrypted at rest |
| Temporary files | Deleted when sharing ends (briefly kept on Mac for the receiving app), swept at launch, encrypted at rest |

## Security Issues

To report a security problem, or if you think your data may have been exposed, see [SECURITY.md](SECURITY.md).
80 changes: 80 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Security Policy

FlightForms is a personal, open-source project run by a single developer.
This document explains how to report a security problem and how a suspected
personal-data breach is handled. See also [`PRIVACY.md`](./PRIVACY.md),
[`legal/GDPR.md`](./legal/GDPR.md) and [`SECURITY_AUDIT.md`](./SECURITY_AUDIT.md)
(the code-level security review — this file is the process, that one is the findings).

## Reporting a vulnerability or suspected breach

If you believe you have found a security vulnerability, or that personal data
may have been exposed:

- **Preferred:** use GitHub's private **"Report a vulnerability"** button under
the repository's **Security** tab. This opens a private security advisory only
the maintainer can see.
- Please do **not** open a public GitHub issue for a security vulnerability, as
that discloses it to everyone before it can be fixed.

Please include what you found, how to reproduce it, and (if relevant) what data
you think may be affected. I will acknowledge the report as quickly as I can.

## What a breach could look like here

The server stores no crew or passenger data (see `PRIVACY.md`), so the realistic
scenarios are narrower than for most apps — but not empty:

- **Account data exposure** — the shared `users` table (email, display name,
sign-in provider) and the `usage` rows (which airport, which form, when).
Low sensitivity, still reportable if it leaks.
- **Compromise of the running server** — an attacker able to observe request
bodies in flight would see the passport and passenger details being filled
into forms at that moment. This is the severe case: treat it as high risk to
the people on those manifests, even though nothing was stored.
- **Stolen API token or session** — lets someone generate forms as that user and
read their account data; revoke the token and assess what was requested.
- **Client-side issues** — a bug in the iOS, macOS or Android app that exposes
data held on the pilot's device. The data is theirs, not ours, but a flaw in
our code that exposes it is ours to fix and to tell users about.

## Personal-data breach process (UK GDPR Art. 33–34)

The controller is established in the **United Kingdom**, so the lead supervisory
authority is the **Information Commissioner's Office (ICO)** under UK GDPR and
the Data Protection Act 2018. The internal process when a breach is suspected:

1. **Record it.** Log the time it was discovered and keep a running note of the
facts, the data and people potentially affected, and every action taken.
(Art. 33(5) — all breaches are documented, whether or not they are reported.)
2. **Contain and assess.** Stop the exposure, then judge the risk to affected
individuals: what data, how many people, how likely and how severe the harm.
3. **Notify the ICO within 72 hours** of becoming aware — unless the breach is
*unlikely* to result in a risk to people's rights and freedoms. Reported via
the ICO's online breach-reporting service or the personal-data-breach
helpline (0303 123 1113). If the 72-hour deadline cannot be met, notify with
reasons for the delay.
4. **Notify affected users** without undue delay **if the breach is high risk**
to them, in clear plain language: what happened, the likely consequences, the
measures taken, and a contact point.
- This is **not** required if the affected data was encrypted/unintelligible,
or if follow-up measures mean the high risk no longer applies, or if it
would involve disproportionate effort (in which case a public notice is
used instead).
5. **Where we are a processor.** For crew and passenger data we act on the
pilot's instructions (`legal/GDPR.md` §5). If a breach touches that data, the
pilots (or their organisations) are the controllers and must be told without
undue delay, with enough detail for them to meet their own obligations —
including telling their passengers.
6. **Cross-border users.** The app serves pilots across Europe. If a breach
affects users in the EU/EEA, EU GDPR applies by territorial scope (Art. 3(2))
and the relevant EU authority would be notified in addition to the ICO.

## Scope

This policy covers the FlightForms server (`forms.flyfun.aero`), the iOS, macOS
and Android apps, the CLI, and their source code. The account system is shared
with [FlyFun Weather](https://github.com/roznet/flyfun-weather/blob/main/SECURITY.md),
which follows the same process. Infrastructure and platform providers
(DigitalOcean, Apple iCloud, Google and Apple sign-in) run their own security
and breach-notification processes.
35 changes: 17 additions & 18 deletions SECURITY_AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,25 +217,24 @@ Even if a mapping file were compromised with a `../` traversal payload, the serv

## LOW Severity Issues

### 16. ~~Generated PDFs Written to Temp Directory~~ (RESOLVED)
### 16. ~~Generated PDFs Written to Temp Directory~~ (RESOLVED — regressed, fixed again)

**File:** `app/flyfun-forms/flyfun-forms/Views/FlightEditView.swift`
**Files:** `app/flyfun-forms/flyfun-forms/Services/GeneratedFormFiles.swift`, `Views/FlightEditView.swift`

**Status: FIXED** — Temp PDF files (which contain filled passport data) are now deleted as soon as the QuickLook preview is dismissed:
```swift
.onChange(of: previewURL) { oldURL, _ in
if let oldURL {
try? FileManager.default.removeItem(at: oldURL)
}
}
```
Note: This is on the user's iOS device (already protected by iOS Data Protection), so the risk was low. The fix is still good hygiene to minimize the window where passport data exists in plaintext on disk.
**History:** first fixed by deleting the file when the QuickLook preview was dismissed. That cleanup went away on 2026-03-14 (`6d5d0cc`) when QuickLook was replaced by the share sheet, and filled forms were left in tmp until iOS purged it — while this section still said FIXED. Found by the GDPR review (`legal/GDPR.md` §9).

**Status: FIXED (2026-09-26)** — every generated form is written to its own folder under `tmp/forms/`, and:
- **iOS:** deleted when the share sheet closes; for mail, deleted as soon as the attachment has been read into the composer.
- **macOS:** deleted on Done, close, or Save to File (moved out). After Open, Reveal in Finder, a sharing service or Mail the file is kept, because the receiving app reads it after the sheet closes.
- **Sweeps:** each new form first deletes files over 15 minutes old, and launch clears the folder (plus loose forms older builds left at the tmp root). Delete All Data clears it too.

The residual window is therefore macOS-only and bounded by the next form or launch. Still on the user's own device under Data Protection / FileVault, so the risk was always low.

### 17. No Rate Limiting on /generate Endpoint (LOW)
### 17. ~~No Rate Limiting on /generate Endpoint~~ (RESOLVED)

The `/generate` endpoint has no rate limiting. A compromised JWT could be used to make unlimited requests. The `Usage` table tracks calls but doesn't enforce limits.
**File:** `src/flightforms/api/rate_limit.py`

**Recommendation:** Add per-user rate limiting (e.g., via `slowapi` or middleware).
**Status: FIXED (2026-09-26)** — `/generate` and `/prefill` together allow 100 fills per user per rolling hour, counted over the existing `usage` log (the same no-counter-rows strategy as `flyfun_common.auth.rate_limit`). Over the limit the request gets `429` with `Retry-After` before any template is filled. Skipped in dev mode, like the shared limits.

### 18. ~~Server Error Messages May Leak Internal Paths~~ (RESOLVED)

Expand Down Expand Up @@ -297,10 +296,10 @@ These aspects of the architecture are well-designed:
| **P1** | Clear legacy Person ID fields after migration (#5) | **FIXED** |
| **P1** | Certificate pinning (#4) | Accepted risk |
| **P2** | Use separate secret for SessionMiddleware (#10) | Open |
| **P2** | Clean up temp PDF files after preview (#16) | **FIXED** |
| **P2** | Clean up temp form files after sharing (#16) | **FIXED** (re-fixed 2026-09-26) |
| **P2** | Add startup check for placeholder JWT_SECRET (#11) | **FIXED** |
| **P3** | Add HTTP warning in CLI for non-localhost URLs (#7) | Open |
| **P3** | Add rate limiting to /generate endpoint (#17) | Open |
| **P3** | Add rate limiting to /generate endpoint (#17) | **FIXED** |

---

Expand All @@ -317,6 +316,6 @@ The most critical issues have been resolved:
- Dependency versions are pinned to prevent unvetted upgrades
- Legacy Person ID fields are cleared after migration to avoid duplicate PII storage
- Server refuses to start with placeholder JWT_SECRET in production
- Temp PDF files containing passport data are deleted after QuickLook preview
- Temp form files containing passport data are deleted when sharing ends (re-fixed 2026-09-26 after a regression)

Remaining open items (separate session secret, CLI HTTP warning, rate limiting) are lower priority and primarily affect defense-in-depth rather than direct data exposure.
Remaining open items (separate session secret, CLI HTTP warning) are lower priority and primarily affect defense-in-depth rather than direct data exposure.
Loading
Loading