fix: auth/2FA security — stdout-log code delivery is dev/test-only, production fails closed until email/SMS; verification-code hashing, lockout recovery, sabredav fail-closed

- TWO_FACTOR_ALLOW_LOG_DELIVERY production opt-in REMOVED: plaintext codes are written to the stdout log ([2FA]/[VERIFY]) only in dev/test builds as a local DEV ONLY feature while email/SMS delivery (P6) is implemented. Production builds have no delivery channel and code issuance fails closed (503) under any configuration — no silent log-based code leak
- verification/2FA codes hashed at rest (HMAC-SHA256 via TWO_FACTOR_PEPPER, CHAR(64)); [VERIFY] dev log relay; per-user brute-force budget; password_reset purpose clears lockout for self-service recovery; dummy-bcrypt on login no-user path kills timing oracle
- sabredav weak-password list + entropy gate; .env.example ships fail-closed DAV_ADMIN_PASSWORD
- delete-account re-auth (current_password + fresh 2FA code when enforced)
- prod-tag suite (run-prod-tag-tests.sh) compiles and runs the production 2FA issuance gate: production ALWAYS reports no delivery channel and refuses issuance after the pepper check
- startup_checks_test SNAPSHOT_ENC_KEY values built at runtime so gitleaks sees no secret-shaped literals
- env-docs parity updated (flag removed, 38 vars)
This commit is contained in:
2026-08-22 00:34:50 +01:00
parent 1429eddd34
commit f9e8385d5a
19 changed files with 1047 additions and 472 deletions
+28 -30
View File
@@ -13,67 +13,65 @@ package user
// their assertions ONLY when the prod variant marker reports the real prod
// functions are live; under the test tag they skip with the same documented
// rationale the payments package uses (twofa_delivery_prod_test.go).
//
// CLOSING THE GAP: run-prod-tag-tests.sh (backend/) runs `go test -tags
// "!dev,!test" ./handlers/user/` — the ONLY build configuration where
// twofa_prod.go compiles AND twofaProdVariant is true, so the assertions below
// actually execute there. The `if !twofaProdVariant { t.Skip(...) }` guards
// MUST stay: under the CI "test,!dev" matrix the dev/test variants are still
// the compiled functions (the `test` tag matches `dev || test`), so without
// the guards those runs would FAIL rather than skip.
//
// DELIVERY POSTURE (current): stdout-log delivery of 2FA codes is a
// DEV/TEST-ONLY local feature. A production build has NO delivery channel of
// any kind — email/SMS is not wired yet (P6) and there is deliberately no
// production opt-in to log delivery — so code issuance fails closed
// unconditionally (after the pepper check) and twoFADeliveryAvailable is
// always false.
import (
"os"
"testing"
)
// twoFAAllowLogDeliveryEnv is only defined in twofa_prod.go (!dev && !test);
// use the literal env name so this test also compiles under `test,!dev`.
const allowLogDeliveryEnv = "TWO_FACTOR_ALLOW_LOG_DELIVERY"
// TestTwoFAEnsureIssueAllowed_ProdPredicate pins the production issuance gate:
// it fails closed without TWO_FACTOR_PEPPER (an unsalted digest in the 1M code
// space would be offline-brute-forceable) or without a delivery channel, and
// allows issuance only when both are configured.
// space would be offline-brute-forceable) and, with the pepper set, STILL fails
// closed because a production build has no delivery channel (email/SMS unwired,
// stdout-log delivery is dev/test-only) — issuance can never succeed until a
// real transport lands.
func TestTwoFAEnsureIssueAllowed_ProdPredicate(t *testing.T) {
if !twofaProdVariant {
t.Skip("twoFAEnsureIssueAllowed() is the dev/test build's always-allowed variant (twofa_dev.go, `dev || test`); the prod fail-closed branches are unreachable under the test tag — see the file header for the documented limitation")
}
os.Unsetenv(twoFAPepperEnv)
os.Unsetenv(allowLogDeliveryEnv)
if err := twoFAEnsureIssueAllowed(); err == nil {
t.Error("expected issuance refused without TWO_FACTOR_PEPPER in a production build")
} else if err.Error() != "TWO_FACTOR_PEPPER is not set; refusing to issue a 2FA code (an unsalted digest would be offline-brute-forceable)" {
t.Errorf("expected the pepper-required error without the pepper, got %v", err)
}
// With the pepper set, a production build STILL refuses: there is no
// delivery channel (email/SMS unwired, P6; stdout-log delivery is a
// dev/test-only local feature and there is no production opt-in).
os.Setenv(twoFAPepperEnv, "test-pepper")
os.Unsetenv(allowLogDeliveryEnv)
if err := twoFAEnsureIssueAllowed(); err == nil {
t.Error("expected issuance refused without a delivery channel in a production build")
t.Error("expected issuance refused in a production build with no delivery channel (email/SMS unwired, log delivery dev/test-only)")
} else if err != errTwoFADeliveryUnavailable {
t.Errorf("expected errTwoFADeliveryUnavailable without a channel, got %v", err)
}
os.Setenv(allowLogDeliveryEnv, "true")
if err := twoFAEnsureIssueAllowed(); err != nil {
t.Errorf("expected issuance allowed with both the pepper and a delivery channel, got %v", err)
t.Errorf("expected errTwoFADeliveryUnavailable with no channel, got %v", err)
}
}
// TestTwoFADeliveryAvailable_ProdPredicate pins the production delivery
// predicate: TWO_FACTOR_ALLOW_LOG_DELIVERY unset → no channel (false), exactly
// "true" → channel (true), any other value → no channel.
// predicate: a production build ALWAYS reports no delivery channel — stdout-log
// delivery is a dev/test-only local feature, never a production channel.
func TestTwoFADeliveryAvailable_ProdPredicate(t *testing.T) {
if !twofaProdVariant {
t.Skip("twoFADeliveryAvailable() is the dev/test build's trivially-true variant (twofa_dev.go, `dev || test`); the 503 delivery-unavailable branch is unreachable under the test tag — see the file header for the documented limitation")
t.Skip("twoFADeliveryAvailable() is the dev/test build's trivially-true variant (twofa_dev.go, `dev || test`); the always-false production predicate is unreachable under the test tag — see the file header for the documented limitation")
}
os.Unsetenv(allowLogDeliveryEnv)
if twoFADeliveryAvailable() {
t.Error("production without the explicit opt-in must have NO 2FA delivery channel")
}
for _, v := range []string{"", "1", "yes", "on", "True", "TRUE", "false"} {
os.Setenv(allowLogDeliveryEnv, v)
if twoFADeliveryAvailable() {
t.Errorf("value %q must NOT open the delivery channel (exact 'true' only)", v)
}
}
os.Setenv(allowLogDeliveryEnv, "true")
if !twoFADeliveryAvailable() {
t.Error("the explicit insecure log-delivery opt-in must open the channel")
t.Error("a production build must ALWAYS report NO 2FA delivery channel (email/SMS unwired; stdout-log delivery is dev/test-only)")
}
}