---
phase: 02-authentication
plan: 02
type: tdd
wave: 2
depends_on: [01]
files_modified:
  - backend/internal/auth/password.go
  - backend/internal/auth/password_test.go
  - backend/go.mod
  - backend/go.sum
autonomous: true
requirements: [AUTH-01]
tags: [go, argon2id, password, crypto, tdd]
must_haves:
  truths:
    - "auth.Hash(password, DefaultParams) returns a PHC-formatted argon2id string (D-08)"
    - "auth.Verify(phc, password) returns (true, nil) for the original password and (false, nil) for any other password — in constant time"
    - "Verify rejects malformed PHC, wrong algorithm, incompatible version with sentinel errors"
    - "auth.TestParams exists with reduced memory (8 MiB) so go test wall time stays under 5 seconds (D-26)"
    - "package init() self-tests a hash/verify round-trip and panics on regression (D-08)"
  artifacts:
    - path: backend/internal/auth/password.go
      provides: "Hash, Verify, DefaultParams (OWASP 2024), TestParams"
      contains: "argon2.IDKey"
    - path: backend/internal/auth/password_test.go
      provides: "round-trip, wrong-password, malformed-PHC, version-mismatch tests"
  key_links:
    - from: backend/internal/auth/password.go
      to: "golang.org/x/crypto/argon2"
      via: "argon2.IDKey + argon2.Version"
      pattern: "argon2\\.IDKey"
    - from: backend/internal/auth/password.go
      to: "crypto/subtle"
      via: "constant-time hash compare"
      pattern: "subtle\\.ConstantTimeCompare"
---

<objective>
TDD the argon2id password primitives: PHC-encoded Hash + constant-time Verify, with `DefaultParams` (OWASP 2024 baseline) and `TestParams` (reduced cost). Pure CPU work — zero DB, zero HTTP.

Purpose: every later auth task (signup, login, session creation) calls these two functions; defects here corrupt the entire phase.
Output: `internal/auth/password.go` + tests, with `argon2id` dependency added to `go.mod`.
</objective>

<execution_context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/phases/02-authentication/02-CONTEXT.md
@.planning/phases/02-authentication/02-RESEARCH.md
@.planning/phases/02-authentication/02-01-PLAN.md
@backend/internal/auth/types.go

<interfaces>
This plan adds to package `auth`:

```go
type Params struct {
    Memory      uint32 // KiB
    Iterations  uint32
    Parallelism uint8
    SaltLength  uint32
    KeyLength   uint32
}

var DefaultParams = Params{Memory: 64 * 1024, Iterations: 1, Parallelism: 4, SaltLength: 16, KeyLength: 32} // OWASP 2024 / D-08
var TestParams    = Params{Memory: 8 * 1024,  Iterations: 1, Parallelism: 2, SaltLength: 16, KeyLength: 32}

func Hash(password string, p Params) (string, error)   // returns "$argon2id$v=19$m=...,t=...,p=...$<salt>$<hash>"
func Verify(encoded, password string) (bool, error)    // constant-time; (false, nil) on mismatch; sentinel error on malformed
```

`ErrInvalidHash` and `ErrIncompatibleVersion` (declared in Plan 01) are returned for malformed PHC and version mismatches respectively.
</interfaces>
</context>

<tasks>

<task type="tdd" tdd="true">
  <name>Task 1: RED — failing tests for Hash + Verify (round-trip, wrong password, malformed PHC, version mismatch)</name>
  <read_first>
    backend/internal/auth/types.go
    .planning/phases/02-authentication/02-RESEARCH.md (Pattern 1; Pitfall 4)
    .planning/phases/02-authentication/02-CONTEXT.md (D-08, D-25, D-26)
  </read_first>
  <behavior>
    - TestPassword_HashVerify: Hash(pw, TestParams) returns non-empty PHC starting with `$argon2id$v=19$m=8192,t=1,p=2$`; Verify(phc, pw) returns (true, nil).
    - TestPassword_VerifyWrong: Hash(pw, TestParams); Verify(phc, pw+"x") returns (false, nil) and DOES NOT return an error.
    - TestPassword_VerifyMalformed: Verify("not-a-phc-string", "anything") returns (false, ErrInvalidHash); Verify("$argon2id$v=19$m=8192,t=1,p=2$only-three-segments", "x") returns (false, ErrInvalidHash); Verify("$bcrypt$v=19$...", "x") returns (false, ErrInvalidHash).
    - TestPassword_VerifyVersion: a synthetic PHC string with `v=18` returns (false, ErrIncompatibleVersion).
    - TestPassword_DistinctSaltsPerCall: Hash(pw, TestParams) twice yields two different PHC strings (random salt per call).
    - TestPassword_DefaultParamsShape: DefaultParams equals OWASP 2024 baseline (`Memory == 64*1024`, `Iterations == 1`, `Parallelism == 4`, `SaltLength == 16`, `KeyLength == 32`) — guards against silent param drift.
  </behavior>
  <action>
    Create `backend/internal/auth/password_test.go` with the six tests above as table-driven where natural. Use `auth.TestParams` for all Hash calls to keep wall time low. Tests MUST fail at this step because `password.go` does not exist yet — run `go test ./internal/auth/` and confirm compile failure or test failure. Commit message: `test(02): RED — failing argon2id password tests`.
  </action>
  <verify>
    <automated>cd backend &amp;&amp; ! go test ./internal/auth/ -run TestPassword -count=1 2>&amp;1 | grep -q "PASS:"</automated>
  </verify>
  <acceptance_criteria>
    - File `backend/internal/auth/password_test.go` exists.
    - `grep -c "^func Test" backend/internal/auth/password_test.go` &gt;= 6.
    - `grep -c "TestParams" backend/internal/auth/password_test.go` &gt;= 1 (tests use reduced params).
    - `grep -c "ErrInvalidHash\|ErrIncompatibleVersion" backend/internal/auth/password_test.go` &gt;= 2.
    - `cd backend &amp;&amp; go test ./internal/auth/ -run TestPassword -count=1` exits NON-zero (RED state).
    - One commit on the current branch with message matching `test(02).*RED` and only `backend/internal/auth/password_test.go` in the diff.
  </acceptance_criteria>
  <done>Tests are written and failing for the right reason (missing implementation).</done>
</task>

<task type="tdd" tdd="true">
  <name>Task 2: GREEN — implement Hash + Verify + Params + init self-test</name>
  <read_first>
    backend/internal/auth/password_test.go (created in Task 1)
    backend/internal/auth/types.go
    .planning/phases/02-authentication/02-RESEARCH.md (Pattern 1 verbatim; Pitfall 4; "Anti-Patterns")
  </read_first>
  <action>
    Add the argon2 dependency: `cd backend &amp;&amp; go get golang.org/x/crypto@v0.51.0`. Create `backend/internal/auth/password.go` per RESEARCH Pattern 1 verbatim:
    - Imports: `crypto/rand`, `crypto/subtle`, `encoding/base64`, `errors`, `fmt`, `strings`, `golang.org/x/crypto/argon2`.
    - Types: `Params` struct with `Memory`, `Iterations`, `Parallelism`, `SaltLength`, `KeyLength` fields per the interfaces block above.
    - Vars: `DefaultParams` (OWASP 2024 — D-08) and `TestParams` (reduced).
    - `Hash(password string, p Params) (string, error)`: read salt via `rand.Read`, call `argon2.IDKey([]byte(password), salt, p.Iterations, p.Memory, p.Parallelism, p.KeyLength)`, format PHC string `$argon2id$v=%d$m=%d,t=%d,p=%d$<b64salt>$<b64hash>` using `base64.RawStdEncoding`.
    - `Verify(encoded, password string) (bool, error)`: split on `$` into 6 parts (per RESEARCH note: 6 segments because the leading `$` produces an empty first element), check `parts[1] == "argon2id"` else return `ErrInvalidHash`, parse `v=%d` and reject if `!= argon2.Version` with `ErrIncompatibleVersion`, parse params, decode salt+hash with `base64.RawStdEncoding.Strict()`, recompute via `argon2.IDKey` using `uint32(len(want))` as key length, compare with `subtle.ConstantTimeCompare(want, got) == 1`.
    - `init()` self-test (D-08): `phc, err := Hash("self-test-password-12chars", TestParams); if err != nil { panic(err) }; ok, err := Verify(phc, "self-test-password-12chars"); if err != nil || !ok { panic("argon2 self-test failed") }` — guards against param drift / build corruption.
    - Anti-pattern guard: NEVER use `bytes.Equal` for hash compare. NEVER use `sha256.New().Sum(...)` anywhere. NEVER reuse a salt across calls.
    Run tests; they must pass. Commit message: `feat(02): GREEN — argon2id Hash + Verify + self-test`.
  </action>
  <verify>
    <automated>cd backend &amp;&amp; go test ./internal/auth/ -run TestPassword -count=1 -v 2>&amp;1 | tee /tmp/p02.log | tail -30 &amp;&amp; grep -q "PASS" /tmp/p02.log &amp;&amp; ! grep -q "FAIL" /tmp/p02.log</automated>
  </verify>
  <acceptance_criteria>
    - File `backend/internal/auth/password.go` exists.
    - `grep -c "func Hash" backend/internal/auth/password.go` == 1; `grep -c "func Verify" backend/internal/auth/password.go` == 1.
    - `grep -c "argon2.IDKey" backend/internal/auth/password.go` &gt;= 2 (Hash + Verify).
    - `grep -c "subtle.ConstantTimeCompare" backend/internal/auth/password.go` &gt;= 1.
    - `grep -c "bytes.Equal" backend/internal/auth/password.go` == 0 (anti-pattern guard).
    - `grep -c "sha256.New().Sum" backend/internal/auth/password.go` == 0 (Pitfall 6 anti-pattern guard).
    - `grep -c "func init" backend/internal/auth/password.go` == 1 (self-test).
    - `go.mod` contains `golang.org/x/crypto v0.51.0` (or higher patch in v0.51.x line).
    - `cd backend &amp;&amp; go test ./internal/auth/ -run TestPassword -count=1` exits 0 with all 6+ tests PASS.
    - `cd backend &amp;&amp; go test ./internal/auth/ -run TestPassword -count=1` completes in &lt; 5 seconds wall-time (Pitfall 4 guard).
    - One commit on the branch with message matching `feat(02).*GREEN.*argon2` and diff limited to `password.go` + `go.mod` + `go.sum`.
  </acceptance_criteria>
  <done>All password tests pass; init self-test runs on every package import; reduced-cost test path keeps `go test` fast.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| In-process | Plaintext password lives in memory only between request parsing and the `Hash`/`Verify` call. Never persisted, never logged. |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-2-01 | Information disclosure | `password_hash` storage | mitigate | argon2id (D-08); per-password 16-byte salt; PHC string self-describes params so future cost tuning is non-breaking. Verified by TestPassword_HashVerify + TestPassword_DistinctSaltsPerCall. |
| T-2-13 | Information disclosure | Timing side-channel on Verify | mitigate | `subtle.ConstantTimeCompare` final compare; argon2 itself is constant-time. Verified by Task 2 acceptance grep for `subtle.ConstantTimeCompare`. |
| T-2-14 | DoS | Very long passwords | mitigate (in caller) | Length ceiling (≤128 chars per D-25) enforced by handler before reaching `Hash` — verified in Plan 04 (signup) and Plan 05 (login). This plan documents the contract via comment on `Hash`. |
| T-2-15 | Tampering | argon2 param drift across deploys | mitigate | `init()` self-test panics on regression; `DefaultParams` shape locked by TestPassword_DefaultParamsShape. |
</threat_model>

<verification>
- `cd backend &amp;&amp; go test ./internal/auth/ -run TestPassword -count=1` exits 0.
- `cd backend &amp;&amp; go vet ./internal/auth/...` exits 0.
- Two commits on branch: one `test(02): RED` then one `feat(02): GREEN`.
</verification>

<success_criteria>
- Plaintext passwords can be hashed to PHC strings and verified back in constant time.
- Verifier rejects malformed input, foreign algorithms, and version mismatches with the documented sentinel errors.
- Test wall time stays under 5 seconds thanks to `TestParams` — unblocks high-frequency test runs in later plans.
</success_criteria>

<output>
Create `.planning/phases/02-authentication/02-02-SUMMARY.md` recording: argon2 library version pinned, exact `DefaultParams` chosen (D-08 baseline confirmed), wall-time observed for the test suite, and any deviations from RESEARCH Pattern 1.
</output>
