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:
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user