Skip to content

Password policy

Every password on create / change must satisfy:

  1. Length - at least Z4J_PASSWORD_MIN_LENGTH characters (default 8). Max 256.
  2. Character classes - at least 3 of 4 of: lowercase, uppercase, digit, symbol.
  3. Not in denylist - a ~1,500-entry list of common passwords and breached patterns.
  • Top 1,000 common passwords (rockyou, SecLists breach compilations).
  • Seasonal patterns (Summer2024, Winter24, etc.) with year variants for the past 10 years.
  • Product-name variants for common SaaS (<product>123, <product>!1).

Generated via _generate_patterns() in z4j_brain.auth.common_passwords. Static; baked at build time.

argon2id, OWASP 2024 defaults:

  • time_cost=3
  • memory_cost=64 MiB
  • parallelism=4
  • 32-byte hash, 16-byte salt

Tuneable via Z4J_ARGON2_* env vars. Target: 50–80 ms per verify on a 2024 server CPU.

If stored-hash parameters are weaker than current config, the brain rehashes on successful verify. Silent upgrade for users who were around before a parameter bump.

  • password_too_short (< min length)
  • password_too_long (> 256)
  • password_too_simple (< 3 of 4 classes)
  • password_in_breach_list (matches denylist entry)

All are 422-returned from the password-setting endpoints.

The wrong-username branch of login verifies against a dummy hash generated at brain boot. Takes the same wall time as a real verify, so timing doesn’t disclose account existence.

An audit flagged the old “letter + digit” rule: Summer24 and qwertyui1 both passed, both are in the top 1,000. 3-of-4 classes + denylist knocks out the long tail of dictionary-plus-one-digit passwords.

The dashboard’s password input shows a strength indicator (zxcvbn-like) as guidance - but the server is authoritative. The meter can show “strong” for a password the server rejects because of denylist match, and that’s fine.