Refreshes _docs/02_document/ to reflect the cycle-2 auth-modernization
+ CMMC hardening landings (AZ-531..AZ-538). Authoritative source for
the ripple set is ripple_log_cycle2.md.
Covered:
- architecture.md (section 1 rewritten, ADRs 6-9 added)
- data_model.md (sessions, audit_events, user columns, migrations)
- system-flows.md (F1 rewritten; F11-F17 added; F2/F7/F9 minor)
- module-layout.md (cycle-2 sub-component table)
- diagrams/flows/flow_login.md (dual-token + MFA)
- components/{01_data_layer,03_auth_and_security,05_admin_api}
- modules/ (12 new, 8 modified — full Argon2id/ES256/MFA/refresh
/mission/session/audit/jwks rollup)
- tests/{blackbox,security,traceability-matrix}
Step 13 (Update Docs) output for cycle 2.
Co-authored-by: Cursor <cursoragent@cursor.com>
4.8 KiB
Module: Azaion.Services.Security
Purpose
Static utility class providing password hashing and verification. As of cycle 2, hashes new passwords with Argon2id (RFC 9106) and transparently re-hashes legacy SHA-384 entries on the next successful login.
Cycle 1 (2026-05-13) note —
GetHWHashdeleted;GetApiEncryptionKeysimplified by AZ-197.Cycle 2 (2026-05-14) note A —
GetApiEncryptionKey/EncryptTo/DecryptToremoved with the encrypted-download endpoint. TheAzaion.Testproject went with them.Cycle 2 (2026-05-14) note B (AZ-536) —
ToHashwas removed and replaced withHashPassword+VerifyPassword. Hash format is now PHC:$argon2id$v=19$m=65536,t=3,p=1$<salt-b64>$<hash-b64>. Legacy SHA-384 hashes (64-char Base64, no$prefix) are still accepted for verification and the verify path returnsNeedsRehash=truesoUserService.ValidateUsercan rewrite them on the success path. Epic AZ-530, CMMC IA.L2-3.5.10.
Public Interface
| Method | Signature | Description |
|---|---|---|
HashPassword |
static string HashPassword(string plaintext) |
Generates a 16-byte salt, computes Argon2id with the conservative defaults below, returns a PHC string. |
VerifyPassword |
static VerifyResult VerifyPassword(string plaintext, string stored) |
Detects format by prefix. Argon2id PHC → re-derives + constant-time compare; legacy SHA-384 → re-hashes + constant-time compare. Returns Valid, plus NeedsRehash=true when (a) the stored hash is legacy SHA-384, or (b) the stored Argon2 parameters are weaker than current defaults. |
record VerifyResult(bool Valid, bool NeedsRehash)
Carries the verification outcome. NeedsRehash is the trigger for UserService.RegisterSuccessfulLogin to write a fresh Argon2id hash back to the row.
Internal Logic
Defaults (RFC 9106 §4 conservative profile):
- Memory: 65536 KiB (64 MiB)
- Iterations: 3
- Parallelism: 1
- Salt: 16 bytes (128 bits) per RFC §3.1 minimum
- Hash output: 32 bytes (256 bits)
Format detection:
- Argon2id PHC string starts with
$argon2id$. - Legacy SHA-384: exactly 64 base64 characters and does NOT start with
$. - Anything else fails verify with
Valid=false, NeedsRehash=false.
PHC encoding uses base64 without padding (PHC convention):
$argon2id$v=19$m=<KiB>,t=<iters>,p=<lanes>$<salt-b64-nopad>$<hash-b64-nopad>
Constant-time comparison uses CryptographicOperations.FixedTimeEquals for both formats — addresses AZ-536 AC-5 (no remotely-observable timing leak).
Dependencies
Konscious.Security.Cryptography.Argon2(Argon2id implementation, pure C#)System.Security.Cryptography.SHA384(legacy verify path)System.Security.Cryptography.RandomNumberGenerator(salt entropy)System.Security.Cryptography.CryptographicOperations(constant-time compare)
Consumers
Azaion.Services/UserService.csRegisterUser— callsHashPassword(request.Password)ValidateUser→RegisterSuccessfulLogin— callsVerifyPassword; onNeedsRehashwrites a fresh Argon2id hash back transactionally (conditional on the original hash to avoid clobbering a parallel rehash)
Azaion.Services/MfaService.csEnrollandDisable— re-auth viaVerifyPassword(password, user.PasswordHash)
Data Models
None.
Configuration
None directly. The defaults are class-level constants. Bumping them later automatically surfaces NeedsRehash=true for any older stored hash, so the upgrade is lazy and transparent.
External Integrations
None.
Security
- Argon2id memory cost (64 MiB) makes GPU bruteforce attacks orders of magnitude slower than the previous SHA-384 path. Each verify costs ~50–200 ms on commodity hardware (intentional latency floor).
- Legacy SHA-384 hashes are migrated on next successful login (lazy migration). Service accounts that never log in interactively (CompanionPC devices) need an admin-side bulk-reset rotation cycle to upgrade.
- The verify path is constant-time end-to-end via
FixedTimeEquals— defends AZ-536 AC-5. - The "needs rehash" flag also covers future parameter bumps: raising
Argon2MemoryKib/Argon2Iterationshere will make all weaker stored hashes upgrade themselves on the next login.
Tests
e2e/Azaion.E2E/Tests/PasswordHashingTests.cs— AC-1 (PHC format), AC-2 (legacy SHA-384 still validates), AC-3 (transparent re-hash), AC-4 (wrong password fails for both formats), AC-5 (constant-time verify).- Known follow-up (carried from cycle 2 batch 4 review) —
PasswordHashingTests.AC5_Verify_uses_constant_time_comparator_no_obvious_timing_leakis intermittently flaky under suite-level concurrency; widen the assertion bound or warm Argon2 with a non-test login first. Azaion.Servicesis exercised end-to-end through every login / register / MFA flow ine2e/Azaion.E2E/Tests/.