# 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** — `GetHWHash` deleted; `GetApiEncryptionKey` simplified by AZ-197. > > **Cycle 2 (2026-05-14) note A** — `GetApiEncryptionKey` / `EncryptTo` / `DecryptTo` removed with the encrypted-download endpoint. The `Azaion.Test` project went with them. > > **Cycle 2 (2026-05-14) note B (AZ-536)** — `ToHash` was removed and replaced with `HashPassword` + `VerifyPassword`. Hash format is now PHC: `$argon2id$v=19$m=65536,t=3,p=1$$`. Legacy SHA-384 hashes (64-char Base64, no `$` prefix) are still accepted for verification and the verify path returns `NeedsRehash=true` so `UserService.ValidateUser` can 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=,t=,p=$$ ``` **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.cs` - `RegisterUser` — calls `HashPassword(request.Password)` - `ValidateUser` → `RegisterSuccessfulLogin` — calls `VerifyPassword`; on `NeedsRehash` writes a fresh Argon2id hash back transactionally (conditional on the original hash to avoid clobbering a parallel rehash) - `Azaion.Services/MfaService.cs` - `Enroll` and `Disable` — re-auth via `VerifyPassword(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`/`Argon2Iterations` here 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_leak` is intermittently flaky under suite-level concurrency; widen the assertion bound or warm Argon2 with a non-test login first. - `Azaion.Services` is exercised end-to-end through every login / register / MFA flow in `e2e/Azaion.E2E/Tests/`.