Skip to content

Encrypt tokens and cached data at rest (DPAPI) #14

Description

@Phoenix-

Local data — OAuth tokens and the calendar/task/calendar-list caches — is stored as plaintext JSON under %APPDATA%\Meridian. Not an active vulnerability, but it is exposed to "casual indexers": Windows Search, antivirus, cloud-sync agents, backup tools, and anyone glancing at the folder. Encrypt it at rest.

Decision (2026-05-29)

  • Mechanism: Windows DPAPI (System.Security.Cryptography.ProtectedData, DataProtectionScope.CurrentUser). Per-user, no key management on our side.
  • Keep JSON as the serialization format — DPAPI wraps the serialized bytes. No SQLite. SQLite was considered and rejected: it is not encryption (a .db is just as indexable/readable as JSON, so it wouldn't address the threat), and it brings no perf win at our data sizes plus NativeAOT-native-lib risk. Threat model is "casual indexers / backups," which DPAPI fully covers.
  • One mechanism covers both tokens (flagged in the OAuth privacy-policy review, Google OAuth verification (exit test mode) #8) and caches.

Implementation sketch

  • Add a small ProtectedFile.ReadBytes/WriteBytes (or ReadText/WriteText) helper wrapping ProtectedData.Protect/Unprotect. DPAPI output is binary, so switch the stores from File.WriteAllText/ReadAllText to byte-based I/O.
  • Route all persistence through it:
    • Auth/GoogleTokenStore.csLoad/Save (token.json).
    • Services/JsonEventStore.cs, Services/JsonTaskStore.cs, Services/JsonCalendarListStore.cs — read/write paths.
  • Migration: on read, try Unprotect; on failure (CryptographicException), fall back to reading legacy plaintext, then immediately rewrite protected. Otherwise existing users lose all cached state + tokens on upgrade.
  • account.txt holds the account email and is used to enumerate accounts before anything is decrypted (chicken-and-egg). Decide: either leave it as the one small plaintext index file, or protect it and derive enumeration differently. Email is low-sensitivity vs. event/task contents, so leaving it is defensible — document the choice.

NativeAOT

  • We publish with PublishAot=true. Confirm ProtectedData survives trimming (it P/Invokes crypt32; should be fine but verify the AOT publish + a round-trip on the published binary, not just the JIT debug build).

Payoff

  • Lets the privacy policy honestly state data is protected at rest using the OS's standard mechanisms (strengthens Google OAuth verification (exit test mode) #8 verification posture).
  • Closes the "plaintext tokens" note from the privacy-policy review.

Notes

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    distributionShipping: installer, signing, store listings

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions