Skip to content

feat(opds): optional Basic auth, wired as the all-networks gate - #161

Merged
phildenhoff merged 1 commit into
mainfrom
rework-opds-auth
Sep 22, 2026
Merged

phildenhoff merged 1 commit into
mainfrom
rework-opds-auth

Conversation

@phildenhoff

@phildenhoff phildenhoff commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Problem

The state machine (#160) left allInterfaces behind an AuthRequired gate with no key: there was no way to configure credentials, and no path for the mode to ever start. Sharing on every network the computer can reach must not be reachable without a password, and the settings UI (#151) needs the credential surface to exist.

What this does

  • Basic auth for the OPDS server: hand-set or generated credentials. Only an Argon2-derived verifier is stored — never the plaintext password — persisted under the user's app-data directory with private permissions, enforced as axum middleware on every route. Verification is cached (bounded, TTL'd, constant-time compare) and parallel Argon2 work is capped (3 permits; overflow gets 503 + Retry-After).
  • Generated passwords have a fixed, chunked shape — word + three digits (2–9) + one URL-safe symbol (! * - = ~ $) + word, e.g. jaw295=art — sized for the online-only attacker (network round trip + a deliberately slow verify), typeable on an e-reader, and displayed once at generation (the plaintext is never stored). Symbols are URL-safe because readers paste credentials into http://user:pass@host/ logins; # ? @ % + get mangled by URL parsing.
  • Completes the gate: BindPolicy.allow_global now derives from credentials configured — AllInterfaces without credentials → AuthRequired. Clearing credentials while sharing is active stops sharing immediately; any credential change while sharing is not Stopped stops sharing (a Waiting poll or in-flight start must not bind with stale credentials).
  • Tauri surface: four new commands (credential status / configure / generate / clear); the service is constructed at setup time so the credential store lives in app-data. Frontend bindings regenerated.
  • Local-network sharing never requires credentials; enabling the checkbox without credentials set fails fast with a clear message.

Testing

56 tests pass. New: the gate (all_interfaces_requires_credentials_until_auth_allows_it), clearing-credentials-stops-sharing, credential-change-while-waiting stops the poll, generated-password shape/charset/blocklist, credential store persistence/permissions/idempotent-clear, and the auth middleware suite (parser, challenge with charset=UTF-8, cache bounding, disabled-bypass). All state-machine and bind tests from #160 unchanged and green.

Notes

  • ADRs in this PR: 0003 (credentials are machine-local — app-data, one set per install, never travel with the library) and 0004 (two-mode sharing model + state machine; supersedes 0002 with a status-line note; body untouched).
  • Follow-ups, in order: global exponential backoff on failed auth attempts (turns the ~32-bit margin into years), stop-on-library-switch, then feat(opds): add headless server on a Tauri-independent runtime #151 builds the settings screen on both — including the copy review and the colour-chunked password display.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@phildenhoff

Copy link
Copy Markdown
Member Author

Two ADRs landed with this PR: 0003 records that OPDS credentials are machine-local (app-data, one set per install, never travel with the library), and 0004 supersedes ADR 0002 — the sharing model is now two modes (Local networks / All interfaces, the latter wildcard + credentials-gated) with an explicit state machine and no interface picker. 0002's status line notes the partial supersession; its body is untouched. Both follow Plato's structural checklist (drivers, rejected alternatives, blunt consequences, reopening conditions).

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

Basic authentication for the OPDS server: generated credentials (username +
password) persisted with the library, optional per share start, enforced as
axum basic-auth middleware on every route.

This PR also completes the all-networks gate the state machine introduced:
- credentials configured is the input to BindPolicy.allow_global, so
  AllInterfaces starts only when a password exists
- clearing credentials while sharing on all interfaces forces sharing to stop
- sharing on the local network never requires credentials
@github-actions

Copy link
Copy Markdown

libcalibre Test Coverage Report

Overall coverage: 80.16%

📊 Download HTML Report

Coverage breakdown available in the artifacts.

@phildenhoff
phildenhoff merged commit 08a5f31 into main Sep 22, 2026
7 checks passed
@phildenhoff
phildenhoff deleted the rework-opds-auth branch September 22, 2026 06:49
phildenhoff added a commit that referenced this pull request Sep 22, 2026
Rebuilds the sharing settings pane on the state-machine service and the
credential surface from #161:

- Mode choice, not a picker: Local network (exact private/ULA binds, never
  needs credentials) vs All networks (wildcard, enabled only once a
  credential set exists). The per-interface picker is gone.
- Credentials: status / configure (hand-set) / generate-and-set / clear.
  Generated passwords are shown once, colour-chunked (word | digits+symbol
  | word, middle chunk bold) so they transcribe cleanly onto e-readers.
- Status comes from the state-machine projection: Running shows catalog
  addresses; Waiting explains that sharing starts when a network appears;
  Failed surfaces the reason and returns to the form.
- Copy per the agreed drafts; settings schema gains the sharing block
  (v2 -> v3 migration); web builds get no OPDS client at all.

The old #151 branch is superseded by this one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant