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.
Design decisions (flagged per the 5 hard constraints)
Strictly opt-in — unchanged default: empty DEBUG_REPORT_ENDPOINTand 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.
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.
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.
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).
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 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.
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
following these best practices for managing and storing secrets including API keys and other credentials
🦉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&utm_medium=GitHub_checks&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&utm_medium=GitHub_checks&utm_campaign=check_run_comment).
4. If possible, [rewrite git history](https://blog.gitguardian.com/rewriting-git-history-cheatsheet?utm_source=product&utm_medium=GitHub_checks&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&utm_medium=GitHub_checks&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&utm_medium=GitHub_checks&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&utm_source=github&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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #34.
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 thatuploads 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 carryPII: the free-text
userCommentand log lines (emails,host:port, IPv4, JWTs,Bearer/Basic,key=valuesecrets); re-scrubs the stack trace. Logs (never blocks) if a PII shape survives.ReportPayloadEncryptor/HybridReportPayloadEncryptor— envelope encryption: randomAES-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
AppLogat each lifecycle point.DEBUG_REPORT_PUBLIC_KEYplaceholder (empty default, from git-ignoredsecrets.properties);secrets.properties.exampledocuments 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)
DEBUG_REPORT_ENDPOINTand emptyDEBUG_REPORT_PUBLIC_KEY⇒ nothing is ever sent; upload is still only triggered by the user'sSubmit tap (#33). The worker now additionally fails closed without a key.
in the app (deliberate — see scope note). Only a URL + a public key are ever configured.
StackTraceScrubber; this adds afinal redaction pass. One deliberate exception: the user-supplied reply-to
userEmail(#159) isretained, since it is consented follow-up data, not leaked PII — documented and flagged.
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/ReportEncryptionuses a non-exportable Keystore key that a remote reviewer couldnever decrypt with, so it can't be reused here (documented).
DEBUG_REPORT_PUBLIC_KEYmirrors 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 perseal, 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(floor0.84),
compileDebugAndroidTestKotlin,lintDebug,ktlintCheck,detekt. No instrumented surfacewas touched (background worker), so E2E is left to the CI matrix.
⚠️ 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
700ce071ae🛠 Guidelines to remediate hardcoded secrets
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.
Pull request closed