Commit Graph

5 Commits

Author SHA1 Message Date
0ad9ef1bb4 Fix F-08, F-12, F-13: Implement account lockout, username validation, and password minimum length enforcement
- Added failed login tracking for account lockout enforcement in `db` and `ui` layers; introduced `failed_logins` table to store attempts, window start, and attempt count.
- Updated login checks in `grpcserver/auth.go` and `ui/handlers_auth.go` to reject requests if the account is locked.
- Added immediate failure counter reset on successful login.
- Implemented username length and character set validation (F-12) and minimum password length enforcement (F-13) in shared `validate` package.
- Updated account creation and edit flows in `ui` and `grpcserver` layers to apply validation before hashing/processing.
- Added comprehensive unit tests for lockout, validation, and related edge cases.
- Updated `AUDIT.md` to mark F-08, F-12, and F-13 as fixed.
- Updated `openapi.yaml` to reflect new validation and lockout behaviors.

Security: Prevents brute-force attacks via lockout mechanism and strengthens defenses against weak and invalid input.
2026-03-11 20:59:26 -07:00
6e690c4435 Fix F-07: pre-compute real Argon2 dummy hash via sync.Once
- auth/auth.go: add DummyHash() which uses sync.Once to compute
  HashPassword("dummy-password-for-timing-only", DefaultArgonParams())
  on first call; subsequent calls return the cached PHC string;
  add sync to imports
- auth/auth_test.go: TestDummyHashIsValidPHC verifies the hash
  parses and verifies correctly; TestDummyHashIsCached verifies
  sync.Once behaviour; TestDummyHashMatchesDefaultParams verifies
  embedded m/t/p match DefaultArgonParams()
- server/server.go, grpcserver/auth.go, ui/ui.go: replace five
  hardcoded PHC strings with auth.DummyHash() calls
- AUDIT.md: mark F-07 as fixed
Security: the previous hardcoded hash used a 6-byte salt and
  6-byte output ("testsalt"/"testhash" in base64), which Argon2id
  verifies faster than a real 16-byte-salt / 32-byte-output hash.
  This timing gap was measurable and could aid user enumeration.
  auth.DummyHash() uses identical parameters and full-length salt
  and output, so dummy verification timing matches real timing
  exactly, regardless of future parameter changes.
2026-03-11 20:37:27 -07:00
bf9002a31c Fix F-03: make token renewal atomic
- db/accounts.go: add RenewToken(oldJTI, reason, newJTI,
  accountID, issuedAt, expiresAt) which wraps RevokeToken +
  TrackToken in a single BEGIN/COMMIT transaction; if either
  step fails the whole tx rolls back, so the user is never
  left with neither old nor new token valid
- server.go (handleRenewToken): replace separate RevokeToken +
  TrackToken calls with single RenewToken call; failure now
  returns 500 instead of silently losing revocation
- grpcserver/auth.go (RenewToken): same replacement
- db/db_test.go: TestRenewTokenAtomic verifies old token is
  revoked with correct reason, new token is tracked and not
  revoked, and a second renewal on the already-revoked old
  token returns an error
- AUDIT.md: mark F-03 as fixed
Security: without atomicity a crash/error between revoke and
  track could leave the old token active alongside the new one
  (two live tokens) or revoke the old token without tracking
  the new one (user locked out). The transaction ensures
  exactly one of the two tokens is valid at all times.
2026-03-11 20:24:32 -07:00
f34e9a69a0 Fix all golangci-lint warnings
- errorlint: use errors.Is for db.ErrNotFound comparisons
  in accountservice.go, credentialservice.go, tokenservice.go
- gofmt/goimports: move mciasv1 alias into internal import group
  in auth.go, credentialservice.go, grpcserver.go, grpcserver_test.go
- gosec G115: add nolint annotation on int32 port conversions
  in mciasgrpcctl/main.go and credentialservice.go (port validated
  as [1,65535] on input; overflow not reachable)
- govet fieldalignment: reorder Server, grpcRateLimiter,
  grpcRateLimitEntry, testEnv structs to reduce GC bitmap size
  (96 -> 80 pointer bytes each)
- ineffassign: remove intermediate grpcSrv = GRPCServer() call
  in cmd/mciassrv/main.go (immediately overwritten by TLS build)
- staticcheck SA9003: replace empty if-body with _ = Serve(lis)
  in grpcserver_test.go
0 golangci-lint issues; 137 tests pass (go test -race ./...)
2026-03-11 15:24:07 -07:00
59d51a1d38 Implement Phase 7: gRPC dual-stack interface
- proto/mcias/v1/: AdminService, AuthService, TokenService,
  AccountService, CredentialService; generated Go stubs in gen/
- internal/grpcserver: full handler implementations sharing all
  business logic (auth, token, db, crypto) with REST server;
  interceptor chain: logging -> auth (JWT alg-first + revocation) ->
  rate-limit (token bucket, 10 req/s, burst 10, per-IP)
- internal/config: optional grpc_addr field in [server] section
- cmd/mciassrv: dual-stack startup; gRPC/TLS listener on grpc_addr
  when configured; graceful shutdown of both servers in 15s window
- cmd/mciasgrpcctl: companion gRPC CLI mirroring mciasctl commands
  (health, pubkey, account, role, token, pgcreds) using TLS with
  optional custom CA cert
- internal/grpcserver/grpcserver_test.go: 20 tests via bufconn covering
  public RPCs, auth interceptor (no token, invalid, revoked -> 401),
  non-admin -> 403, Login/Logout/RenewToken/ValidateToken flows,
  AccountService CRUD, SetPGCreds/GetPGCreds AES-GCM round-trip,
  credential fields absent from all responses
Security:
  JWT validation path identical to REST: alg header checked before
  signature, alg:none rejected, revocation table checked after sig.
  Authorization metadata value never logged by any interceptor.
  Credential fields (PasswordHash, TOTPSecret*, PGPassword) absent from
  all proto response messages — enforced by proto design and confirmed
  by test TestCredentialFieldsAbsentFromAccountResponse.
  Login dummy-Argon2 timing guard preserves timing uniformity for
  unknown users (same as REST handleLogin).
  TLS required at listener level; cmd/mciassrv uses
  credentials.NewServerTLSFromFile; no h2c offered.
137 tests pass, zero race conditions (go test -race ./...)
2026-03-11 14:38:47 -07:00