diff --git a/docs/health-connect-play-declaration.md b/docs/health-connect-play-declaration.md index 6044c11048..4d872bcd6a 100644 --- a/docs/health-connect-play-declaration.md +++ b/docs/health-connect-play-declaration.md @@ -40,6 +40,7 @@ Related code: - `service/workouts/health/HealthConnectManager.kt` — the only place the app touches Health Connect. - `ui/screen/loggedIn/workouts/health/HealthConnectRationaleActivity.kt` — the in-app rationale screen. - `PRIVACY.md` § "Health and fitness data (Health Connect)". +- `docs/play-data-safety.md` — the Data safety form, which Google cross-checks against this one. ## Rejection 2 — the app contradicted this declaration, and what changed diff --git a/docs/play-data-safety.md b/docs/play-data-safety.md new file mode 100644 index 0000000000..4c4e8f84fa --- /dev/null +++ b/docs/play-data-safety.md @@ -0,0 +1,134 @@ +# Play Console — Data safety form + +Source text for the **Data safety** form (Play Console → *Monitor and improve → Policy and +programs → App content → Data safety*). Keep this file and the form in sync, the same way +`health-connect-play-declaration.md` tracks the Health apps declaration. Google cross-checks the +two against each other and against `PRIVACY.md`; a disagreement between them is what a reviewer +finds first. + +## Why this file exists + +The form previously said **"App doesn't collect or share data"** and **"Data isn't encrypted"**. +Both are wrong, and the pair is self-contradictory — if nothing is collected there is no +encryption answer to give. + +The error came from reading "collect" as "the developer receives it". Google does not define it +that way: + +> **"Collect" means transmitting data from your app off a user's device.** This includes data +> transmitted by libraries/SDKs and webviews controlled by your app. + +Amethyst has no server, and that remains true and worth saying in the listing — but the test is +whether data *leaves the phone*, not who receives it. `PRIVACY.md` § "Data sent off-device" +already lists six categories that do. + +**The trap:** Google's *user-initiated transfer* and *service provider* carve-outs are exceptions +to **sharing**, not to **collection**. A user tapping Post is a transfer they expect, so it need +not be declared as sharing — but it is still collection and must be declared. + +## Two exemptions Amethyst genuinely earns + +- **Private DMs — end-to-end encryption.** "User data that is sent off device, but that is + unreadable by you or anyone other than the sender and recipient as a result of end-to-end + encryption does not need to be disclosed." NIP-17/NIP-44 qualifies. Do not declare DM contents. +- **The My Fitness dashboard — on-device only.** "User data accessed by your app that is only + processed locally on the user's device and not sent off device does not need to be disclosed." + The dashboard reads Health Connect, computes on device, displays, and transmits nothing. Only a + workout the user *publishes* is collected. + +## 1. Overall questions + +| Question | Answer | +| --- | --- | +| Does your app collect or share any of the required user data types? | **Yes** | +| Is all of the user data collected by your app encrypted in transit? | **See § 4 — decide before submitting** | +| Do you provide a way for users to request that their data is deleted? | **Yes** — see § 5 | + +## 2. Data types — collected + +Everything below is **optional** (the user chooses to post, to upload, to enable push, to connect +Health Connect) and its purpose is **App functionality** only. Amethyst ships no analytics, crash +reporting, advertising or attribution SDK — verified: no Crashlytics, Firebase Analytics, AppsFlyer, +Adjust, Sentry or Bugsnag anywhere in `libs.versions.toml` or any module's `build.gradle.kts`. So +never tick Analytics, Advertising or marketing, Fraud prevention, or Personalization. + +| Category | Type | Collected | Shared | Why | +| --- | --- | --- | --- | --- | +| Location | Approximate location | Yes | No | A geohash the user attaches to a post or a location chat. `ACCESS_COARSE_LOCATION` only — never precise. | +| Personal info | Name | Yes | No | Display name in the user's published kind-0 profile, if they set one. | +| Personal info | User IDs | Yes | No | The Nostr public key accompanies every published event and the push registration. | +| Personal info | Other info | Yes | No | Profile bio, picture and website, if set. | +| Financial info | Other financial info | Yes | No | Zap amounts appear in published zap events; NWC relays payment instructions to the user's own wallet. | +| Health and fitness | Health info | Yes | No | Heart rate, **only** in a workout the user chooses to publish. | +| Health and fitness | Fitness info | Yes | No | Exercise, distance, steps, calories, elevation, **only** in a published workout. | +| Messages | Other in-app messages | Yes | No | Public posts, replies, articles. **Private DMs are excluded** — E2EE exemption. | +| Photos and videos | Photos / Videos | Yes | No | Uploads to the media server the user selects. | +| Audio | Voice or sound recordings | Yes | No | Voice notes, and speaking in a NIP-53 audio room. | +| Calendar | Calendar events | Yes | No | NIP-52 calendar events the user publishes. | +| Device or other IDs | Device or other IDs | Yes | **See § 3** | *(Play build, push enabled)* FCM registration token. | + +## 3. Data types — not collected + +**Contacts** (follow lists are Nostr public keys, not device contacts) · **Web browsing history** · +**App info and performance** (no crash or analytics SDK) · **Files and docs** (covered by photos, +videos and audio) · **Personal info → Email address** · **Financial info → payment info or purchase +history** · **Precise location** · **App activity**. + +### The one item needing your decision + +The **push token** path is the only place a third party plausibly receives data outside a +user-initiated publish: `PRIVACY.md` says the token, public key and a preferred relay are +"registered with Google Firebase Cloud Messaging so a notification proxy can wake the app." + +- If that proxy processes the data **on your behalf**, the *service provider* exception applies → + **Shared: No**. +- If it is an independent operator, it is a third party → **Shared: Yes** for *Device or other IDs*. + +Decide this from how the proxy is actually operated. Everything else in the table is No because +publishing to relays is a user-initiated transfer the user reasonably expects. + +> **This is the judgment call in the whole form.** Answering "shared with third parties" for +> *Health and fitness* would make the public label read that Amethyst shares health data with third +> parties — which is precisely the prohibited use the Health Connect policy names, and would +> undercut the declaration. "Collected, not shared" is both accurate under Google's definition and +> the answer to defend. Be ready to defend it: the user takes a deliberate action, sees the post, +> and confirms it. + +## 4. Encrypted in transit — resolve before submitting + +Relays are `wss://`, media is HTTPS, DMs are NIP-44. **But** `amethyst/src/main/res/xml/network_security_config.xml` +sets `cleartextTrafficPermitted="true"` on the global `base-config` so user-configured `ws://` +relays keep working. Google allows "yes" only when encryption covers *all* collected data, so as +the app stands today that cannot be claimed unconditionally. + +Two honest ways forward: + +1. **Narrow the config** to loopback and `.onion` only, and drop the global permit. Loopback never + leaves the device and `.onion` is encrypted by Tor, so "encrypted in transit" becomes truthfully + **Yes** — and it is a real security improvement. Cost: a user with a plain `ws://` relay breaks. +2. **Leave it** and answer **No**. Accurate, but the public label then reads "Data isn't encrypted" + next to a health-permission request — the same contradiction that has already cost two + submissions. + +Option 1 is the better outcome. It is a behaviour change for `ws://` relay users, so it is a +maintainer's decision, not a form-filling one. + +## 5. Data deletion + +The developer runs no server and holds nothing, so there is no developer-held copy to request +deletion of. Users can: + +- wipe everything local by clearing app storage or uninstalling (`PRIVACY.md` § "Data stored on + your device"); +- delete an account's data in-app; +- request deletion of published events with NIP-09 — noting, as `PRIVACY.md` already says, that + relays may not honour it and public content should be assumed permanent. + +Say this plainly rather than claiming deletion guarantees the protocol cannot give. + +## 6. Keep these three in sync + +The Health apps declaration, this form, and `PRIVACY.md` are one statement split across three +places. A reviewer reads all three. Before any resubmission, check that each data type here also +appears in `PRIVACY.md`, and that nothing here contradicts +`health-connect-play-declaration.md` § 4.