WIP: feat(reporting): anonymize + encrypt debug reports before opt-in upload (#34) #445

Closed
JMR-dev wants to merge 1 commits from feat-34-debug-ingest into main
JMR-dev commented 2026-07-08 15:25:46 +00:00 (Migrated from github.com)

Closes #34.

Held for maintainer review — please read the design decisions below. This PR touches the
privacy-sensitive debug-report path, and there is an important scope judgement call in it.

Important scope note (please confirm)

Issue #34 as written is the server-side Cloudflare Worker (per parent #11: Go + Pulumi, "lives in
separate infrastructure, not the Android app repo"
), and the client upload path already shipped in
the now-closed #33 (ReportUploadWorker, ReportSubmitter, …). So a literal "Android worker that
uploads to R2" would (a) duplicate #33 and (b) require embedding R2/S3 write credentials + SigV4 signing
in a FOSS APK — which violates the F-Droid rule and #11/#34's explicit "secrets stored in
Cloudflare, never in the app."

Rather than do that, this PR implements the client-appropriate, F-Droid-safe slice of #34 that the
5 hard constraints actually call for and that strengthens the privacy posture inside this repo:
a pre-upload anonymization pass + end-to-end payload encryption wired into the existing worker,
all gated off by default. The R2 storage/credentials + Go Worker remain server-side (#11) and out of
this repo. If you'd prefer this land as a doc-only stub or in a different shape, happy to adjust.

What changed

  • ReportAnonymizer — best-effort pre-upload redaction of the two surfaces that can still carry
    PII: the free-text userComment and log lines (emails, host:port, IPv4, JWTs, Bearer/Basic,
    key=value secrets); re-scrubs the stack trace. Logs (never blocks) if a PII shape survives.
  • ReportPayloadEncryptor / HybridReportPayloadEncryptor — envelope encryption: random
    AES-256-GCM content key encrypts the payload, wrapped with RSA-OAEP-SHA256 to a recipient public key;
    compact JSON envelope. JCA only — no new dependency, no GMS.
  • ReportUploadWorker — now anonymizes → encrypts → POSTs the envelope, and fails closed
    (uploads only when both an endpoint and a usable key are configured; never sends plaintext).
    PII-free AppLog at each lifecycle point.
  • BuildConfig DEBUG_REPORT_PUBLIC_KEY placeholder (empty default, from git-ignored
    secrets.properties); secrets.properties.example documents key generation.
  • docs/debug-report-privacy.md — data flow + privacy posture (for F-Droid #16 / README #20).

Design decisions (flagged per the 5 hard constraints)

  1. Strictly opt-in — unchanged default: empty DEBUG_REPORT_ENDPOINT and empty
    DEBUG_REPORT_PUBLIC_KEY ⇒ nothing is ever sent; upload is still only triggered by the user's
    Submit tap (#33). The worker now additionally fails closed without a key.
  2. F-Droid-safe — no proprietary/GMS deps; encryption is pure JCA. No R2/S3 credentials or SigV4
    in the app
    (deliberate — see scope note). Only a URL + a public key are ever configured.
  3. PII-safe — reports are already PII-free by construction + StackTraceScrubber; this adds a
    final redaction pass. One deliberate exception: the user-supplied reply-to userEmail (#159) is
    retained, since it is consented follow-up data, not leaked PII — documented and flagged.
  4. Encryption scheme — hybrid RSA-OAEP-SHA256 (MGF1-SHA256) + AES-256-GCM, end-to-end so even the
    ingest Worker/R2 only see ciphertext. Public key ships in the build (safe); the private key
    stays with the maintainer, off-device. Note: this is asymmetric on purpose — the existing on-device
    KeystoreCrypto/ReportEncryption uses a non-exportable Keystore key that a remote reviewer could
    never decrypt with, so it can't be reused here (documented).
  5. Config placeholders — DEBUG_REPORT_PUBLIC_KEY mirrors the existing endpoint placeholder;
    build works with empty values; no real credentials hardcoded.

Also flagging: (a) wire-format change — the POST body changes from plaintext JSON to the
encrypted envelope (the #33 HTTP test is updated to decrypt-and-verify with a test private key);
(b) no new dependency; (c) fail-closed behavior is new.

Tests / gate

New/updated JVM unit tests: ReportAnonymizerTest (representative PII in → redacted out),
ReportPayloadEncryptorTest (encrypt→decrypt round-trip, envelope never contains plaintext, unique per
seal, GCM-tamper fails, key parsing), and the worker success/retry/failure + fail-closed paths (network
mocked, no real R2).

Local fast gate green: assembleDebug, testDebugUnitTest, jacocoTestCoverageVerification (floor
0.84), compileDebugAndroidTestKotlin, lintDebug, ktlintCheck, detekt. No instrumented surface
was touched (background worker), so E2E is left to the CI matrix.

Closes #34. > **Held for maintainer review — please read the design decisions below.** This PR touches the > privacy-sensitive debug-report path, and there is an important scope judgement call in it. ## Important scope note (please confirm) Issue #34 as written is the **server-side** Cloudflare Worker (per parent #11: Go + Pulumi, *"lives in separate infrastructure, not the Android app repo"*), and the **client** upload path already shipped in the now-closed #33 (`ReportUploadWorker`, `ReportSubmitter`, …). So a literal "Android worker that uploads to R2" would (a) duplicate #33 and (b) require embedding R2/S3 write credentials + SigV4 signing in a FOSS APK — which **violates** the F-Droid rule and #11/#34's explicit *"secrets stored in Cloudflare, never in the app."* Rather than do that, this PR implements the **client-appropriate, F-Droid-safe slice** of #34 that the 5 hard constraints actually call for and that strengthens the privacy posture inside this repo: a pre-upload **anonymization** pass + **end-to-end payload encryption** wired into the existing worker, all gated off by default. The R2 storage/credentials + Go Worker remain server-side (#11) and out of this repo. If you'd prefer this land as a doc-only stub or in a different shape, happy to adjust. ## What changed - **`ReportAnonymizer`** — best-effort pre-upload redaction of the two surfaces that can still carry PII: the free-text `userComment` and log lines (emails, `host:port`, IPv4, JWTs, `Bearer`/`Basic`, `key=value` secrets); re-scrubs the stack trace. Logs (never blocks) if a PII shape survives. - **`ReportPayloadEncryptor` / `HybridReportPayloadEncryptor`** — envelope encryption: random AES-256-GCM content key encrypts the payload, wrapped with RSA-OAEP-SHA256 to a recipient public key; compact JSON envelope. **JCA only — no new dependency, no GMS.** - **`ReportUploadWorker`** — now anonymizes → encrypts → POSTs the envelope, and **fails closed** (uploads only when both an endpoint and a usable key are configured; never sends plaintext). PII-free `AppLog` at each lifecycle point. - **BuildConfig** `DEBUG_REPORT_PUBLIC_KEY` placeholder (empty default, from git-ignored `secrets.properties`); `secrets.properties.example` documents key generation. - **`docs/debug-report-privacy.md`** — data flow + privacy posture (for F-Droid #16 / README #20). ## Design decisions (flagged per the 5 hard constraints) 1. **Strictly opt-in** — unchanged default: empty `DEBUG_REPORT_ENDPOINT` *and* empty `DEBUG_REPORT_PUBLIC_KEY` ⇒ nothing is ever sent; upload is still only triggered by the user's Submit tap (#33). The worker now *additionally* fails closed without a key. 2. **F-Droid-safe** — no proprietary/GMS deps; encryption is pure JCA. **No R2/S3 credentials or SigV4 in the app** (deliberate — see scope note). Only a URL + a *public* key are ever configured. 3. **PII-safe** — reports are already PII-free by construction + `StackTraceScrubber`; this adds a final redaction pass. **One deliberate exception:** the user-supplied reply-to `userEmail` (#159) is retained, since it is consented follow-up data, not leaked PII — documented and flagged. 4. **Encryption scheme** — hybrid RSA-OAEP-SHA256 (MGF1-SHA256) + AES-256-GCM, end-to-end so even the ingest Worker/R2 only see ciphertext. **Public** key ships in the build (safe); the **private** key stays with the maintainer, off-device. Note: this is asymmetric on purpose — the existing on-device `KeystoreCrypto`/`ReportEncryption` uses a non-exportable Keystore key that a remote reviewer could never decrypt with, so it can't be reused here (documented). 5. **Config placeholders** — `DEBUG_REPORT_PUBLIC_KEY` mirrors the existing endpoint placeholder; build works with empty values; no real credentials hardcoded. **Also flagging:** (a) **wire-format change** — the POST body changes from plaintext JSON to the encrypted envelope (the #33 HTTP test is updated to decrypt-and-verify with a test private key); (b) **no new dependency**; (c) **fail-closed** behavior is new. ## Tests / gate New/updated JVM unit tests: `ReportAnonymizerTest` (representative PII in → redacted out), `ReportPayloadEncryptorTest` (encrypt→decrypt round-trip, envelope never contains plaintext, unique per seal, GCM-tamper fails, key parsing), and the worker success/retry/failure + fail-closed paths (network mocked, no real R2). Local fast gate green: `assembleDebug`, `testDebugUnitTest`, `jacocoTestCoverageVerification` (floor 0.84), `compileDebugAndroidTestKotlin`, `lintDebug`, `ktlintCheck`, `detekt`. No instrumented surface was touched (background worker), so E2E is left to the CI matrix.
gitguardian[bot] commented 2026-07-08 15:25:51 +00:00 (Migrated from github.com)

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
34661120 Triggered JSON Web Token 700ce071ae app/src/test/kotlin/org/libremail/reporting/ReportAnonymizerTest.kt View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

#### ⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request. Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components. <details> <summary>🔎 Detected hardcoded secret in your pull request</summary> <br> | GitGuardian id | GitGuardian status | Secret | Commit | Filename | | | -------------- | ------------------ | ------------------------------ | ---------------- | --------------- | -------------------- | | [34661120](https://dashboard.gitguardian.com/workspace/616578/incidents/34661120?occurrence=278925393) | Triggered | JSON Web Token | 700ce071aee886358fc7a0e60de26db2606a3403 | app/src/test/kotlin/org/libremail/reporting/ReportAnonymizerTest.kt | [View secret](https://github.com/JMR-dev/LibreMail/commit/700ce071aee886358fc7a0e60de26db2606a3403#diff-bd0925ad3e006dd3f1ff49a05ff1187b639cadb072acd23975d45e812a62ba2dR62) | </details> <details> <summary>🛠 Guidelines to remediate hardcoded secrets</summary> <br> 1. Understand the implications of revoking this secret by investigating where it is used in your code. 2. Replace and store your secret safely. [Learn here](https://blog.gitguardian.com/secrets-api-management?utm_source=product&amp;utm_medium=GitHub_checks&amp;utm_campaign=check_run_comment) the best practices. 3. Revoke and [rotate this secret](https://docs.gitguardian.com/secrets-detection/secrets-detection-engine/detectors/generics/json_web_token#revoke-the-secret?utm_source=product&amp;utm_medium=GitHub_checks&amp;utm_campaign=check_run_comment). 4. If possible, [rewrite git history](https://blog.gitguardian.com/rewriting-git-history-cheatsheet?utm_source=product&amp;utm_medium=GitHub_checks&amp;utm_campaign=check_run_comment). Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data. To avoid such incidents in the future consider - following these [best practices](https://blog.gitguardian.com/secrets-api-management/?utm_source=product&amp;utm_medium=GitHub_checks&amp;utm_campaign=check_run_comment) for managing and storing secrets including API keys and other credentials - install [secret detection on pre-commit](https://docs.gitguardian.com/ggshield-docs/integrations/git-hooks/pre-commit?utm_source=product&amp;utm_medium=GitHub_checks&amp;utm_campaign=check_run_comment) to catch secret before it leaves your machine and ease remediation. </details> --- <sup>🦉 [GitGuardian](https://dashboard.gitguardian.com/auth/login/?utm_medium=checkruns&amp;utm_source=github&amp;utm_campaign=cr1) detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.<br/></sup>

Pull request closed

Please reopen this pull request to perform a merge.
This pull request is marked as a work in progress.
Sign in to join this conversation.