Skip to content

feat(security): Phase A — Security Admin gate (Argon2id + session + UI)#74

Merged
thierryvm merged 4 commits into
mainfrom
feat/security-admin-foundation
May 12, 2026
Merged

feat(security): Phase A — Security Admin gate (Argon2id + session + UI)#74
thierryvm merged 4 commits into
mainfrom
feat/security-admin-foundation

Conversation

@thierryvm

Copy link
Copy Markdown
Owner

Summary

Phase A of the in-app Security Admin dashboard (Settings → Sécurité tab). Foundation for Phase B (agent runner + audit reports persistence), shippable on its own — the user can set a password, unlock the section, and see a live forensic view of unlock attempts.

4 logical commits, ~1500 lignes :

Commit Scope
feat(db,deps) Migration 020 (security_audit_attempts + security_audit_reports tables) + argon2 0.5 + subtle 2 deps
feat(security_admin) Rust module: setup_password / verify_password / session token / lockout policy + 12 tests
feat(commands) 6 Tauri commands wired into invoke_handler + AppState
feat(ui) Settings → Sécurité tab: setup wizard, password gate, unlocked placeholder, forensic log

Security choices

  • Argon2id (m=19MiB, t=2, p=1) — OWASP 2026 interactive params. PHC string stored in OS keychain (provider security_password_hash), never SQLite.
  • Session token — 32 random bytes from OsRng, base64 URL-safe no-pad, 30min lifetime, subtle::ConstantTimeEq comparison.
  • Lockout in-RAM — 3 fails → 5s, 5 → 30s, 10 → 5min, 20+ capped 5min. App restart resets (intentional; mono-user desktop).
  • Forensic audit log — every attempt appended to security_audit_attempts. Best-effort: SQLite failure doesn't fail the verify decision.

Threat model (in / out)

In scope : brute force, hash extraction via DB exfil, side-channel timing on session token.

Out of scope : privileged malware with keychain access, memory dump while unlocked, binary reverse engineering. The gate is friction — accidental clicks, screen sharing, shoulder-surfing, another OS user — not a remote-attacker firewall.

Tests

  • Rust : 281 (269 baseline + 12 new) — lockout escalation, session expiry/token rejection/malformed input, setup→verify roundtrip, lockout-after-3-fails, audit log row insertion. Keychain-touching tests skip gracefully when keychain unavailable (headless CI Linux).
  • Clippy : -D warnings clean.
  • Frontend : npm run typecheck clean.

Phase B (deferred to next session)

Agent runner module (parse .claude/agents/*.md + invoke Anthropic API with BYOK keychain key + persist to security_audit_reports + ReportViewer markdown render + FindingsList aggregator). The table schema lands in this PR (migration 020) so Phase B is purely additive.

🤖 Generated with Claude Code

thierryvm and others added 4 commits May 12, 2026 12:11
…n gate

Foundation commit for the Settings → Security dashboard (Phase A of the
post-#73 hardening roadmap). Two pieces land together because the schema
and the crypto dep are mutually dependent — the module can't compile
without argon2 + subtle, and the module is what writes to the new tables.

Migration 020:
- security_audit_attempts: append-only log of password unlock attempts
  (forensic trail, no PII — mono-user desktop). Feeds the in-RAM lockout
  decision; persistence is for auditing, not enforcement.
- security_audit_reports: landing zone for the LLM auditor outputs.
  Schema declared here so 020 ships as a coherent unit with the gate.
  Empty until Phase B (agent runner) writes the first row.

Dependencies:
- argon2 0.5: Argon2id hashing with OWASP 2026 params (m=19MB, t=2, p=1).
  PHC string stored in the OS keychain — never SQLite.
- subtle 2: constant-time equality for the in-RAM session token. Guards
  against the theoretical side-channel where compare time leaks the
  matched-prefix length.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Core module for the Settings → Security gate. Three pieces of state
on AppState, small async API for the Tauri command layer (next commit).

Password lifecycle:
- setup_password: 12-char min, Argon2id hash, PHC string in OS keychain
  (provider "security_password_hash" added to ai_keys::KNOWN_PROVIDERS)
- verify_password: lockout check → keychain read → Argon2id verify →
  on success mint SessionState + log to security_audit_attempts.
  Typed VerifyOutcome (Ok/LockedOut/NoPasswordSet/Wrong) for the UI.
- reset_password: keychain delete + state reset. #[allow(dead_code)]
  for now, admin-only recovery (Phase B may expose).

Session token:
- 32 random bytes from OsRng, base64 URL-safe no-pad for IPC.
- 30-minute lifetime. Comparison via subtle::ConstantTimeEq — defends
  the theoretical timing side-channel cheaply.

Lockout policy (in-RAM by design — app restart resets, fine because
that is high-friction):
  0-2 fails: free
  3-4: 5s · 5-9: 30s · 10-19: 5min · 20+: capped 5min

Audit log: every attempt appends to security_audit_attempts. Best-
effort, never fails the verify decision (a wedged DB must not lock
the user out of their own app).

12 unit tests (pure logic: lockout escalation, session expiry,
malformed token rejection) + 4 keychain-touching integration tests
that skip gracefully when keychain is unavailable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Thin IPC layer over the security_admin module. No business logic —
each command maps a module call to a serde-friendly response.

Exposed commands:
- is_security_password_set() -> bool
- setup_security_password(plain) -> Result<()>
- verify_security_password(plain) -> Result<VerifyResponse>
- check_security_session(token) -> bool
- end_security_session() -> ()
- list_recent_security_attempts(limit) -> Result<Vec<AttemptRow>>

VerifyResponse is a tagged union (snake_case kind) so the renderer
pattern-matches without parsing strings. AttemptRow is a #[derive(FromRow)]
shape with only the 4 columns the UI renders.

Module registered in commands/mod.rs and wired into lib.rs's
invoke_handler. AppState already grew a security_admin: Arc<...>
field in the previous commit so these handlers can reach the
session/lockout mutexes directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User-facing slice of Phase A. New "Sécurité" tab in Settings exposes
three states driven by the security_admin commands:

1. Setup wizard — when no password is configured. Confirm-match,
   12-char minimum, password fields type="password" + autocomplete
   "new-password" so browser autofill won't leak.
2. Verify gate — once a hash exists, prompts on every fresh tab open.
   Lockout countdown live-decrements on screen. Wrong/locked_out/
   no_password_set/ok branches all rendered explicitly.
3. Unlocked panel — placeholder for the Phase B agent runner +
   reports table, plus a live "Tentatives récentes" forensic view
   reading the new security_audit_attempts log.

Session token cached in sessionStorage (renderer-side) so a renderer
refresh doesn't force re-prompt — but the Rust side still enforces
the 30-minute lifetime, so a stale token gets rejected and we fall
back to the gate cleanly.

All components use shadcn/ui primitives (Card, Input, Button, Alert)
matching the rest of Settings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @thierryvm, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@thierryvm thierryvm merged commit 279ba89 into main May 12, 2026
5 checks passed
@thierryvm thierryvm deleted the feat/security-admin-foundation branch May 12, 2026 13:18
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