Files
admin/_docs/02_document/modules/services_security.md
T
Oleksandr Bezdieniezhnykh a77b3f8a59 [AZ-529] [AZ-530] Cycle-2 documentation refresh
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>
2026-05-14 09:22:53 +03:00

4.8 KiB
Raw Blame History

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) noteGetHWHash deleted; GetApiEncryptionKey simplified by AZ-197.

Cycle 2 (2026-05-14) note AGetApiEncryptionKey / 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$<salt-b64>$<hash-b64>. 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=<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.cs
    • RegisterUser — calls HashPassword(request.Password)
    • ValidateUserRegisterSuccessfulLogin — 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 ~50200 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/.