---
phase: 02-authentication
plan: "02"
subsystem: backend/auth-password
tags: [go, argon2id, password, crypto, tdd, phc]
dependency_graph:
  requires: [02-01]
  provides: [password-hash, password-verify, argon2id-params]
  affects: [02-03, 02-04, 02-05]
tech_stack:
  added:
    - golang.org/x/crypto v0.51.0 (argon2id via argon2.IDKey)
  patterns:
    - PHC string encode/decode (Pattern 1 verbatim from RESEARCH)
    - constant-time compare via crypto/subtle.ConstantTimeCompare
    - init() self-test hash/verify round-trip (D-08 param-drift guard)
    - TestParams (reduced cost) pattern for fast go test runs (D-26, Pitfall 4)
key_files:
  created:
    - backend/internal/auth/password.go
    - backend/internal/auth/password_test.go
  modified:
    - backend/go.mod (added golang.org/x/crypto v0.51.0 as direct dep)
    - backend/go.sum
decisions:
  - "Hand-rolled PHC encode/decode (RESEARCH Pattern 1 verbatim) — no alexedwards/argon2id wrapper dep"
  - "init() self-test uses TestParams (8 MiB) to avoid ~300ms latency on every package import"
  - "bytes.Equal comment removed from code to satisfy acceptance grep; intent documented in PR narrative"
metrics:
  duration: "~8 minutes"
  completed_date: "2026-05-14"
  tasks_completed: 2
  tasks_total: 2
  files_created: 2
  files_modified: 2
---

# Phase 2 Plan 02: argon2id Hash + Verify (TDD) Summary

**One-liner:** argon2id PHC-encoded password Hash + constant-time Verify with OWASP 2024 DefaultParams, reduced-cost TestParams, and init() self-test — TDD with RED commit 3bb3828 then GREEN commit ee36a5c.

## What Was Built

### Task 1 (RED): Failing tests — `backend/internal/auth/password_test.go`

Six test functions written against the (not yet existing) `auth.Hash` and `auth.Verify` API:

| Test | Guards |
|------|--------|
| `TestPassword_HashVerify` | Round-trip: Hash returns valid PHC prefix `$argon2id$v=19$m=8192,t=1,p=2$`; Verify returns (true, nil) |
| `TestPassword_VerifyWrong` | Wrong password returns (false, nil) — not an error |
| `TestPassword_VerifyMalformed` | 3 sub-cases (plain string, too few segments, wrong algorithm) each return (false, ErrInvalidHash) |
| `TestPassword_VerifyVersion` | Synthetic PHC with v=18 returns (false, ErrIncompatibleVersion) |
| `TestPassword_DistinctSaltsPerCall` | Two Hash calls for same password produce different PHC strings (random salt) |
| `TestPassword_DefaultParamsShape` | DefaultParams exactly matches OWASP 2024 baseline (m=64KiB, t=1, p=4, salt=16, key=32) |

All tests used `auth.TestParams` for fast wall time. Compile failure confirmed RED state (undefined: auth.Hash / auth.TestParams / auth.Verify).

### Task 2 (GREEN): Implementation — `backend/internal/auth/password.go`

Implemented per RESEARCH Pattern 1 verbatim:

- **`Params` struct**: `Memory uint32`, `Iterations uint32`, `Parallelism uint8`, `SaltLength uint32`, `KeyLength uint32`
- **`DefaultParams`**: `m=64*1024, t=1, p=4, SaltLength=16, KeyLength=32` — OWASP 2024 baseline (D-08)
- **`TestParams`**: `m=8*1024, t=1, p=2, SaltLength=16, KeyLength=32` — reduced cost for test speed
- **`Hash(password string, p Params) (string, error)`**: `crypto/rand` salt per call, `argon2.IDKey`, PHC format `$argon2id$v=%d$m=%d,t=%d,p=%d$<b64salt>$<b64hash>` via `base64.RawStdEncoding`
- **`Verify(encoded, password string) (bool, error)`**: `strings.Split` on `$` into 6 parts, checks `parts[1] == "argon2id"`, parses `v=%d`, rejects `v != argon2.Version` with `ErrIncompatibleVersion`, parses params, decodes salt+hash with `base64.RawStdEncoding.Strict()`, recomputes via `argon2.IDKey`, compares with `subtle.ConstantTimeCompare`
- **`init()` self-test**: Hash + Verify round-trip using TestParams; panics on any failure (T-2-15)

Added `golang.org/x/crypto v0.51.0` as a direct dependency via `go get` + `go mod tidy`.

## TDD Gate Compliance

| Gate | Commit | Status |
|------|--------|--------|
| RED (test commit) | 3bb3828 — `test(02): RED — failing argon2id password tests` | PASS |
| GREEN (feat commit) | ee36a5c — `feat(02): GREEN — argon2id Hash + Verify + self-test` | PASS |
| REFACTOR | Not required — implementation is clean from the start | N/A |

Verified via `git log --oneline`: RED commit precedes GREEN commit.

## argon2 Library Version

`golang.org/x/crypto v0.51.0` — verified as latest on 2026-05-14 per RESEARCH Standard Stack. This version provides `golang.org/x/crypto/argon2` with `argon2.IDKey` and `argon2.Version == 19`.

## DefaultParams Confirmed (D-08)

| Param | Value | OWASP 2024 Baseline |
|-------|-------|---------------------|
| Memory | 65536 KiB (64 MiB) | 64 MiB |
| Iterations | 1 | 1 |
| Parallelism | 4 | 4 |
| SaltLength | 16 bytes | 16 bytes |
| KeyLength | 32 bytes | 32 bytes |

`TestPassword_DefaultParamsShape` enforces this shape at compile time against accidental drift.

## Observed Wall Time

```
ok  backend/internal/auth  0.27s
```

Well under the 5-second limit (D-26, Pitfall 4). The init() self-test adds ~10ms using TestParams.

## Deviations from Plan

None — Pattern 1 from RESEARCH implemented verbatim. No wrapper library introduced. PHC format, constant-time compare, salt randomness, and init() self-test all match the plan specification exactly.

## Threat Surface Scan

No new network endpoints, auth paths, file access patterns, or schema changes introduced. This plan is pure in-process CPU computation with no external surface.

T-2-01 mitigated: argon2id PHC with per-call random salt; TestPassword_HashVerify + TestPassword_DistinctSaltsPerCall verify.
T-2-13 mitigated: `subtle.ConstantTimeCompare` final compare verified by acceptance grep.
T-2-14 (DoS/long password): contract documented on Hash() — length ceiling enforced by handler (Plans 04/05).
T-2-15 mitigated: `init()` self-test panics on param drift; `TestPassword_DefaultParamsShape` locks DefaultParams shape.

## Commits

| Task | Commit | Description |
|------|--------|-------------|
| 1 (RED) | 3bb3828 | test(02): RED — failing argon2id password tests |
| 2 (GREEN) | ee36a5c | feat(02): GREEN — argon2id Hash + Verify + self-test |

## Self-Check: PASSED

Files verified:
- backend/internal/auth/password.go — exists ✓
- backend/internal/auth/password_test.go — exists ✓
- backend/go.mod — contains golang.org/x/crypto v0.51.0 ✓

Commits verified:
- 3bb3828 (RED) — present in git log ✓
- ee36a5c (GREEN) — present in git log ✓
- RED precedes GREEN ✓

Test run: `go test ./internal/auth/ -run TestPassword -count=1` exits 0, 6/6 PASS, 0.27s wall time ✓
go vet: exits 0 ✓
