feat: store OAuth tokens securely

Add bounded token response parsing with refresh-token retention and a cancellable worker-thread libsecret adapter using versioned credential payloads.

Verify CRUD, isolation, cancellation, corruption, unavailable service, redaction, and cleanup against an isolated D-Bus and gnome-keyring Secret Service in normal, warning-as-error, and sanitizer profiles.
This commit is contained in:
2026-07-18 11:56:04 +01:00
parent 295acbc125
commit 3560a7d23d
13 changed files with 1405 additions and 27 deletions

View File

@@ -215,8 +215,32 @@ the application manifest, and IPv6 loopback redirect addresses are currently
unsupported. The advertised `localhost` URL therefore remains distinct from the
IPv4-only listener binding.
Concrete token JSON parsing, Secret Service storage, Graph requests, account UI,
and usable account setup are still absent. libsecret's synchronous calls may
block; the next adapter isolates them behind an off-render-thread boundary
rather than calling them from the TUI loop. Read-only Graph discovery and delta
follow token parsing and secret persistence.
### Token and Secret Service boundary
The token decoder is strict and
bounded: oversized or structurally invalid JSON, wrong field types, and invalid
token lifetimes fail before any credential is exposed to account state. Initial
authorization requires a refresh token. A later refresh response replaces it
when Microsoft supplies a rotated token and otherwise retains the previously
stored refresh token.
Access and refresh tokens are serialized together in a versioned libsecret
secret payload, addressed by an opaque local account identifier. SQLite stores
neither token material nor a copy of the secret payload. Token values also stay
out of logs, command-line arguments, and environment variables. Schema/version
validation prevents an incompatible secret from being mistaken for current
credentials.
The libsecret adapter exposes synchronous, cancellable operations and is
worker-thread-only because a D-Bus call or keyring unlock may block. Tests run
the real adapter against an isolated D-Bus session and gnome-keyring rather than
claiming integration from an in-memory fake alone. Missing credentials are a
normal disconnected result. A locked collection, cancellation, parse error, or
read/write/delete failure remains an explicit failure; none triggers plaintext
fallback or partial connected state. An account becomes connected only after
the complete versioned payload has been stored successfully.
Graph requests, account UI, and usable account setup remain absent. After this
boundary passes verification, the Microsoft adapter will call `/me`, discover
calendars, and apply read-only delta pages through the durable cache using only
delegated `Calendars.Read`.

View File

@@ -154,12 +154,26 @@ client with `http://localhost/nocal/oauth/callback`. Microsoft ignores the port
when matching localhost redirects. Literal HTTP `127.0.0.1` registration needs
a manifest edit, and IPv6 loopback redirects are currently unsupported.
These adapters still do not make Office 365 usable. They include no token JSON
parser, Secret Service backend, Graph call, or account UI. libsecret exposes
synchronous operations that may block, so the next phase isolates its adapter
off the render thread. Token parsing and secure persistence come before
read-only Graph calendar discovery and delta synchronization. Remote writes
remain gated on the durability and conflict paths being exercised end to end.
The token and Secret Service boundary strictly parses
bounded token JSON and requires a refresh token at initial authorization. A
rotated refresh token replaces the stored value; if a refresh response omits
one, Nocal retains the prior refresh token. Access and refresh tokens are stored
only as a versioned libsecret secret keyed by an opaque local account ID. They
never appear in SQLite, logs, command lines, or environment variables.
Secret Service operations are synchronous but cancellable and run only on a
worker thread so a D-Bus or keyring wait cannot block rendering. The real
adapter is exercised against isolated D-Bus and gnome-keyring services. Missing
credentials leave an account disconnected. Locked-keyring, cancellation,
parsing, and storage failures are reported distinctly, with no plaintext
fallback and no partially connected account. Connection state is published
only after the complete secret store succeeds.
This phase still does not make Office 365 usable. It contains no Microsoft Graph
call, account UI, or usable account setup. After verification, the next product
slice calls `/me`, discovers calendars, and performs read-only Graph delta
synchronization using delegated `Calendars.Read`. Remote writes remain gated on
the durability and conflict paths being exercised end to end.
## Local file interchange

View File

@@ -57,11 +57,23 @@ Completed concrete Linux adapters:
while literal HTTP `127.0.0.1` needs a manifest edit and IPv6 loopback is not
supported
Completed token/secret boundary:
- Strict, bounded token JSON parsing with a required initial refresh token
- Refresh-token rotation with retention of the previous token when a refresh
response omits a replacement
- Versioned libsecret payloads keyed by opaque local account IDs
- Synchronous, cancellable Secret Service calls restricted to worker threads
- Real isolated D-Bus/gnome-keyring integration tests
- Explicit missing, locked, cancelled, and failure results; no connected account
until the complete secret store succeeds
- OAuth secrets only in Secret Service, never SQLite, logs, CLI arguments, or
environment variables
Next provider-boundary slices:
- Token JSON parsing and a Secret Service adapter isolated off the render thread
because synchronous libsecret calls may block
- OAuth secrets in Secret Service, never SQLite
- Microsoft `/me` identity and read-only calendar discovery
- Read-only Graph delta synchronization using delegated `Calendars.Read`
- Generic `SyncProvider` contract and observable sync status
## 0.3 — CalDAV
@@ -78,10 +90,10 @@ Next provider-boundary slices:
- Account/calendar management UI and per-calendar ANSI identity
The cache, generic HTTP/OAuth boundary, and concrete Linux adapters are
prerequisites, not usable Microsoft 365 integration. There is still no token
parser, Secret Service backend, Graph call, or account UI. The immediate sequence is token parsing plus an
off-render-thread Secret Service adapter, then read-only Microsoft Graph
discovery and delta synchronization.
prerequisites, not usable Microsoft 365 integration. The token/Secret
Service phase still provides no Graph call, account UI, or usable account setup.
After it passes verification, the immediate sequence is Microsoft `/me`,
read-only calendar discovery, and Graph delta synchronization.
## 1.0 — distribution quality