Review 3 of 6 — Privacy, data minimization, least privilege

Reviewer lens: what the sidecar stores, who could read it, and whether each permission and dependency is the minimum needed.

Inputs: docs/PRD-original.md, docs/DESIGN-DRAFT.md (2026-10-03).

Verdict: the direction is right: no INTERNET, no bodies by default, Person-uri contact inference, OTP redaction.

But the draft has five real holes, and each one would quietly break a privacy claim the PRD makes:

  1. The HMAC sender key is mostly theater as drafted. The same row also stores a cleartext "display label", which for an unknown sender is the number (android.title). The smsto: action needs the number anyway. (§2)
  2. "No INTERNET permission" holds only until the first Google dependency is added. ML Kit GenAI brings telemetry, and it hands message text to AICore, which is a separate process that does have network access. Nothing in the draft verifies the merged manifest. (§5)
  3. Our own annotation notification re-leaks what Android 15 just redacted. Every other notification listener can read it (watch bridges, Pushbullet-style apps, launchers). So can the lock screen and dumpsys notification. (§7)
  4. "Delete everything" leaves data behind. It misses WAL/journal files, WorkManager's own unencrypted DB, the probe exports, our posted notifications, and Messages notifications we snoozed. Snoozed notifications would be lost, which breaks the PRD's "reversible" principle. (§4)
  5. Backups are not addressed. On Android 12+, allowBackup="false" alone does not stop device-to-device transfer. (§3)

1. Threat model (who are we protecting the data from?)

The design needs to state this explicitly. Each control below should map to one of these rows. Otherwise "encrypt everything" becomes cargo cult.

#AdversaryRealistic for Byron?Primary control
T1Google account / cloud backup compromiseYesBackup exclusion (§3)
T2Another app on the phone (incl. other notification listeners, accessibility apps, keyboards)YesSandbox; minimal content in our notifications (§7); FLAG_SECURE
T3A person holding the unlocked phone, or watching the lock screenYesNotification visibility (§7); optional biometric gate on History/Review screens
T4Forensic extraction / rooted device after first unlock (AFU)LowSQLCipher + Keystore-wrapped key (§3)
T5The developer/agent pipeline during Phase 0 (probe exports pasted into chats, CI logs)Yes, this weekProbe export allowlist (§8)
T6Senders (third parties who never consented)Ethical, minor legalRetention TTLs, no bodies (§9)
T7Google (AICore/ML Kit telemetry)Only if LLM enabledKeep the LLM out of the no-network build (§5)

Out of scope: a compromised OS, and a malicious Google Messages (it already has everything).


2. Sender keys: is HMAC(number) meaningful?

Short answer: it is weak as pseudonymization, useful as an index, and good for crypto-shredding. Only that last one is worth claiming.

Recommendation.


3. Encryption at rest and backups

3.1 Current state of the libraries (verified 2026-10-03)

3.2 Recommended design

3.3 Backups: must exclude everything


4. "Delete everything": define it precisely

The draft says "drop the DB plus rotate the HMAC key". Rotate is the wrong verb: delete it. Because flash storage doesn't securely erase, the real erasure mechanism is crypto-shredding. The full sequence, in order:

  1. Un-snooze every Google Messages notification we snoozed (Quiet-unknowns mode). If we skip this, deleting our data hides the user's messages, which breaks "reversible actions". Also always snooze with a finite duration, so an uninstall or crash can't strand notifications.
  2. cancelAll() our own posted notifications.
  3. Cancel all WorkManager work and delete WorkManager's DB. It is a separate, unencrypted SQLite DB (androidx.work.workdb) that persists Data inputs. Rule: never pass message text, numbers or names as WorkManager input. Pass only an opaque row ID.
  4. Close Room, then delete the DB and the -wal, -shm and -journal files.
  5. Delete the Keystore aliases: DB-wrap key, HMAC key, body-envelope key pair. This step is the actual erasure. Steps 3–4 just reclaim space.
  6. Clear filesDir, noBackupFilesDir, cacheDir, DataStore files, and any local crash ring buffer.
  7. Delete probe/diagnostic exports we created (SAF URIs we hold persisted permission for). Tell the user that copies they shared elsewhere are out of reach.
  8. Drop in-memory caches (sender cache, held contentIntent PendingIntents).
  9. Show what we cannot delete: messages in Google Messages, drafts we opened in Messages, contacts the user added, Google Messages' own block/report state.

Note: this is distinct from "revoke Notification Access", which should be offered alongside. Add an instrumentation test: populate everything, delete, then assert that the app's data dir contains only freshly created empty files.


5. The no-INTERNET guarantee: make it a build check, not a sentence

The claim is good, but nothing enforces it yet. A manifest-enforced guarantee only holds if the merged manifest is checked on every build.

The service itself is protected by BIND_NOTIFICATION_LISTENER_SERVICE; that is a permission= attribute, not a uses-permission. Deny explicitly: INTERNET, ACCESS_NETWORK_STATE, READ_CONTACTS, READ_CALENDAR (until §10), READ_SMS, RECEIVE_SMS, SEND_SMS, QUERY_ALL_PACKAGES, BIND_ACCESSIBILITY_SERVICE, READ_PHONE_STATE.

  1. Telemetry. ML Kit's own disclosure says the GenAI APIs send device info, performance metrics, API configuration, event types, error codes and configured languages to Google. It also uses Firebase Remote Config and Firebase Installations. Source: https://developers.google.com/ml-kit/android-data-disclosure. Expect the merged manifest to gain INTERNET + ACCESS_NETWORK_STATE from transitive deps (to verify at scaffold time). We could strip them with tools:node="remove", but whether inference survives that is untested, and the disclosure gives no opt-out.
  2. AICore is a separate system process with its own network access. Even if our APK has no INTERNET, message text crosses an IPC boundary into a Google component. The guarantee would rest on Google's statement that ML Kit "does not send input data", not on our manifest. That is a different, weaker guarantee, and the UI must say so.

Recommendation: keep the LLM out of v1 entirely, as the deterministic engine must stand alone anyway. If it is added later, ship it as a separate build flavor, ideally a separate opt-in APK talking to the core over a signature-protected bound service. The core APK keeps a provable zero-network manifest, and the transparency screen states which build is installed. Prefer a fully local runtime (llama.cpp / a GGUF model loaded through SAF) over AICore if a manifest-provable guarantee is wanted. Check MediaPipe LLM Inference for telemetry before choosing it.

Code rule: the only outbound intent carrying message-derived data is the user-tapped smsto:, and it carries a fixed template, never quoted message text.


6. Logcat, crash reporting, screenshots, dumps


7. Our own notifications (lock screen, watches, other listeners)

The sidecar posts derived content, such as "Likely scam from +1 415… — asks for a code". Android 15's OTP/sensitive redaction doesn't apply to our content. Every other notification listener on the device, paired watches, and the lock screen see it.


8. Phase 0 probe: the export file is a real leak vector

The draft says the probe "logs structure, never message text". Structure still leaks:

FieldWhy it leaksProbe should emit
android.title, android.conversationTitle, android.subText, android.selfDisplayNameContact names, numbers, group names, Byron's own namepresence + type + length bucket
Person.name, Person.key, Person.uriNames; tel:+1…; contacts lookup keysuri scheme only (tel / content / mailto / null); name present y/n
shortcutId, getGroup(), getSortKey(), notification tagGoogle Messages may embed conversation or participant IDspresence + length + charset class (digits / alnum)
android.messages[]Text, timestamps, senders, Byron's own repliescount; per-entry: has text, text length bucket, has sender Person, is-self
android.remoteInputHistory, android.bigText, android.tickerText, android.infoText, android.summaryTextDuplicates of contentpresence + length bucket
Exact timestamps / whenWith lengths, enough to re-identify messages against Byron's inboxrelative order only, or minute-rounded with a per-export random offset
ActionsLabels are fine; RemoteInput keys fineaction titles + RemoteInput present y/n + semanticAction

Probe rules:


9. Retention, and third-party data

Design for that now; it costs nothing.


10. Permissions: READ_CONTACTS and READ_CALENDAR

The manual expected-senders ledger delivers most of the value without it.


11. Proposed data inventory (the "What's stored" screen should render this table live)

FieldPurposeRetentionLocationProtection
Raw notification extras (title, text, messages[], people)ScoringIn memory only, discarded after scoringProcess memoryNever logged / persisted / passed to WorkManager
sender_key = HMAC(E.164 / short code / alpha ID)DB lookup key; export-safe IDWith sender rowRoom/SQLCipherKeystore non-exportable HMAC key; key deletion = unlink
Sender number / display labelsmsto: draft, UIWith sender row (90 d idle; 30 d if never acted on)Room/SQLCipherDB encryption; never in notifications/logs
Sender trust state (Trusted / Not legit / none) + set-atUser decisionUntil user removesRoom/SQLCipherDB encryption
Verdict history: (verdict, reason codes, 5 dimension scores, completeness, timestamp)Trajectory, explanationsLast 10 per sender; 90 dRoom/SQLCipherDB encryption; codes only, no spans
URL evidenceExplain BRAND_DOMAIN_MISMATCH etc.With verdictRoom/SQLCiphereTLD+1 + flags only, never full URL
Matched text spansLive explanationIn memory onlyProcess memoryOnly shown in our FLAG_SECURE UI / our notification as template text
Message body excerpt (opt-in)Review48 h default, ≤7 dRoom/SQLCipher+ Keystore asymmetric envelope, private key unlocked-device-required
OTP / financial / health spans(none)Never stored—Detectors run before persistence, LLM and notification composition
Context ledger (expected senders, notes)Raise context_confidenceMandatory expiry (14 d default, ≤1 y)Room/SQLCipherDB encryption; may contain health data
Calendar match (if v1.x opt-in)ContextTransient; persist code onlyMemoryNext-7-days query, selected calendars only
Contact statusidentity_confidenceBoolean per verdictRoom/SQLCipherDerived from Person.uri scheme; no READ_CONTACTS
contentIntent PendingIntent"Open conversation"Until notification removed / process deathMemoryNever persisted (can't be)
Snoozed-notification keysUn-snooze on delete / reversibilityUntil un-snoozedRoom/SQLCipherFinite snooze duration
Per-code weight multipliers (feedback)LearningUntil resetRoom/SQLCipherBounded; no sender data
Settings (toggles, thresholds, chosen packages)ConfigUntil resetDataStore (plain)No personal data allowed here
DB passphrase (wrapped)Unlock DBUntil delete-allnoBackupFilesDirWrapped by Keystore AES-GCM (StrongBox if present)
Scrubbed crash signaturesDebuggingRing buffer, 50 entries / 30 dnoBackupFilesDirNo exception messages; manual export only
Probe export (Phase 0)Feasibility spike14 d auto-delete; user-held copies out of scopeSAF user-chosenAllowlist serializer + canary test
WorkManager DBPurge/heartbeat jobsSystem-managedApp DB (unencrypted)Inputs must be opaque IDs only
Our posted notificationsAnnotationUntil dismissed / delete-allSystem (notification manager)PRIVATE/SECRET, public version, setLocalOnly, no text/number

12. Concrete changes to DESIGN-DRAFT

  1. §G Storage: replace "SQLCipher or Keystore-wrapped key" with "Room + net.zetetic:sqlcipher-android, passphrase wrapped by a Keystore AES-GCM key; no security-crypto (deprecated 2025)". Add the asymmetric envelope for opt-in bodies.
  2. §G Storage: reword the HMAC claim. Say the number is stored encrypted, and the HMAC is the export/log-safe key whose deletion unlinks history.
  3. §G Storage: "Delete everything" = the 9-step sequence in §4. "Rotate" → "delete".
  4. §G Storage: persist reason codes + eTLD+1 only; spans in memory only.
  5. New §G.1 Backups: allowBackup=false + dataExtractionRules excluding cloud and device-transfer + fullBackupContent.
  6. §G no-INTERNET line: add the CI merged-manifest permission allowlist and dependency deny-list as Phase 0 deliverables, in the core CI job.
  7. §F LLM: state that ML Kit GenAI/AICore is incompatible with the manifest guarantee. Move it to a separate optional flavor/APK, or use a fully local runtime. v1 = no LLM.
  8. §E Annotate: add the notification visibility / public version / setLocalOnly / no-quoting rules from §7. Make snoozes finite, and un-snooze on delete/uninstall-prep.
  9. §H Probe: allowlist serializer, package filter first, SAF export with preview, canary-string test, uninstall-after-spike step.
  10. §C: do not rely on Android 15 OTP redaction as "free" protection. It is performed by Android System Intelligence on Pixel-class builds, so verify it on Byron's Samsung. It covers OTPs only, not bank balances or health messages, so our own detectors remain mandatory.
  11. §B: the context ledger needs mandatory expiry. READ_CALENDAR deferred to v1.x, transient-match only.
  12. New §G.2 Logging: R8 log stripping, constant exception messages, FLAG_SECURE, non-debuggable release, no crash reporter.

Sources

← Back to the proposal