feat(security): Phase A — Security Admin gate (Argon2id + session + UI)#74
Merged
Conversation
…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>
There was a problem hiding this comment.
Sorry @thierryvm, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 :
feat(db,deps)feat(security_admin)feat(commands)feat(ui)Security choices
security_password_hash), never SQLite.subtle::ConstantTimeEqcomparison.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
-D warningsclean.npm run typecheckclean.Phase B (deferred to next session)
Agent runner module (parse
.claude/agents/*.md+ invoke Anthropic API with BYOK keychain key + persist tosecurity_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