Skip to content

Stop storing passwords reversibly and fail fast on weak JWT secrets (audit #2 & #3)#728

Merged
KrzysztofPajak merged 4 commits into
developfrom
security/crypto-secrets-hardening
Jul 5, 2026
Merged

Stop storing passwords reversibly and fail fast on weak JWT secrets (audit #2 & #3)#728
KrzysztofPajak merged 4 commits into
developfrom
security/crypto-secrets-hardening

Conversation

@KrzysztofPajak

Copy link
Copy Markdown
Member

Stacked on #727 (PBKDF2 password hashing) — review/merge that first. Base will be retargeted to develop once #727 lands.

Audit #2 — reversible password storage (reframed from the original "3DES → AES-GCM")

Investigation showed DecryptText is unused in production; EncryptText (TripleDES) is used only as a deterministic, keyed password verifier compared with == — there is no encryption-at-rest that gets decrypted. So swapping the cipher to AES-GCM would break the equality checks and fix nothing. The real issue is passwords stored reversibly, so this hashes them instead:

  • Installer: the default administrator is now created with a PBKDF2 hash (IEncryptionService.HashPassword) instead of the TripleDES Encrypted format. (Adds a lightweight Grand.Business.Core reference to the installer so the hash format has a single source of truth.)
  • API users (UserApi): create/update now store a PBKDF2 hash. Login verifies PBKDF2 for new records and falls back to the legacy TripleDES check for existing ones (re-saving the user migrates it). This removes the previous design where the reversible ciphertext and its per-user key were stored in the same record — effectively plaintext.
  • EncryptText/DecryptText are documented as legacy verification-only; the algorithm is intentionally left unchanged so existing stored values still verify. The path phases out via the on-login upgrade added in Hash customer passwords with PBKDF2 and upgrade legacy hashes transparently #727.

Audit #3 — weak/placeholder JWT signing keys

  • New ApiSecurityStartup (IStartupApplication in the API module) validates BackendAPI and FrontendAPI signing keys at startup. For an enabled API with a missing / placeholder / <32-char secret it aborts startup outside Development (fail-fast — a weak key lets anyone forge valid tokens) and only warns in Development so local setups keep working.
  • appsettings.json comments now state the secrets are required and should be provided via BackendAPI__SecretKey / FrontendAPI__SecretKey (env var) or a secret store.

Not changed (deliberately)

  • The TripleDES algorithm itself — can't be changed without breaking verification of existing reversibly-stored values; it is now verification-only and phasing out.
  • ValidateIssuer / ValidateAudience defaults — left configurable (enabling them requires configured issuer/audience); documented rather than forced to avoid breaking existing deployments.

Verification

🤖 Generated with Claude Code

KrzysztofPajak and others added 2 commits July 5, 2026 17:01
…rently

Replaces the fast SHA1/SHA-x password hashing with PBKDF2 (HMAC-SHA256,
210k iterations, per-password 16-byte salt) and an optional server-side
pepper from configuration. No database schema change.

- New/changed passwords are stored as a self-describing value in the
  existing Customer.Password field: PBKDF2$1$<iter>$<salt>$<hash>, so the
  algorithm/parameters are known per record and never depend on a global
  setting. PasswordSalt is left empty (salt is embedded).
- Verification is format-aware and constant-time (VerifyPassword): legacy
  SHA hashes still verify via the existing HashedPasswordFormat; PBKDF2 is
  detected by prefix.
- Legacy hashes are transparently re-hashed to PBKDF2 after a successful
  login (LoginCustomer), so no password reset or data migration is needed.
- Centralizes the previously duplicated hash-compare logic across the
  customer manager and the login/change-password/delete-account validators.
- Adds Security:PasswordHashKey (pepper) and Security:PasswordHashIterations
  to configuration; both optional (empty pepper = no pepper).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit #2 (crypto) - eliminate reversible/deterministic 3DES "encryption"
used as a password verifier; hash instead:
- Installer now creates the default administrator with a PBKDF2 hash
  (via IEncryptionService) instead of the legacy TripleDES "Encrypted"
  format.
- API users (UserApi) are stored as PBKDF2 hashes on create/update; login
  verifies PBKDF2 for new records and falls back to the legacy TripleDES
  check for existing ones (re-saving the user migrates it). Removes the
  reversible password + per-user key stored side by side.
- EncryptText/DecryptText are documented as legacy verification-only; the
  algorithm is left unchanged so existing values still verify.

Audit #3 (JWT secrets) - add ApiSecurityStartup that validates BackendAPI
and FrontendAPI signing keys at startup: outside Development it aborts
startup when an enabled API has a missing, placeholder or <32-char secret
(a weak key allows forging valid tokens); in Development it only warns.
appsettings comments updated to require setting the secrets via env/secret
store.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Base automatically changed from security/password-hashing-pbkdf2 to develop July 5, 2026 16:19
…crets-hardening

# Conflicts:
#	src/Business/Grand.Business.Common/Services/Security/EncryptionService.cs
#	src/Business/Grand.Business.Core/Interfaces/Common/Security/IEncryptionService.cs
#	src/Business/Grand.Business.Customers/Services/CustomerManagerService.cs
#	src/Tests/Grand.Business.Common.Tests/Services/Security/EncryptionServiceTests.cs
Copilot AI review requested due to automatic review settings July 5, 2026 16:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses security audit findings by moving API user/admin credential storage away from reversible encryption and by enforcing stronger JWT signing secrets at application startup, reducing the risk of credential disclosure and token forgery.

Changes:

  • Update API user creation/update and installer default admin creation to store passwords as PBKDF2 hashes via IEncryptionService.HashPassword(...) instead of reversible TripleDES encryption.
  • Add ApiSecurityStartup to fail fast (outside Development) when Backend/Frontend API JWT secrets are missing/placeholder/too short.
  • Document EncryptText/DecryptText as legacy verification-only in IEncryptionService.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/Web/Grand.Web/App_Data/appsettings.json Adds guidance/comments about required strong JWT secrets and startup fail-fast behavior.
src/Web/Grand.Web.Admin/Controllers/ApiUserController.cs Switches API user password storage to PBKDF2; stops writing per-user encryption keys for new records.
src/Modules/Grand.Module.Installer/Services/InstallationService.cs Hashes the installer-created default admin password using PBKDF2 and clears salt.
src/Modules/Grand.Module.Installer/Grand.Module.Installer.csproj Adds Grand.Business.Core reference to access IEncryptionService for hashing.
src/Modules/Grand.Module.Api/Validators/Common/LoginValidator.cs Adds PBKDF2 verification path for API-user login with fallback to legacy encrypted comparison.
src/Modules/Grand.Module.Api/Infrastructure/ApiSecurityStartup.cs New startup validation to block weak/placeholder API JWT secrets outside Development.
src/Business/Grand.Business.Core/Interfaces/Common/Security/IEncryptionService.cs Clarifies EncryptText/DecryptText are legacy-only and recommends modern alternatives.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Modules/Grand.Module.Api/Validators/Common/LoginValidator.cs Outdated
Review feedback: choosing the PBKDF2 vs legacy path from an empty PrivateKey
is fragile - a PBKDF2 record that still carries a PrivateKey (imported data
or a future update that only rewrites Password) would take the legacy path
and always fail to authenticate.

Add IEncryptionService.IsHashedPassword to detect the modern self-describing
hash from the stored value, and use it as the discriminator. The legacy path
now also verifies in constant time and is guarded against a missing key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Jul 5, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
5.2% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@KrzysztofPajak KrzysztofPajak merged commit 1964f8a into develop Jul 5, 2026
7 of 8 checks passed
@KrzysztofPajak KrzysztofPajak deleted the security/crypto-secrets-hardening branch July 5, 2026 17:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants