Files

127 lines
8.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 02-bot-skeleton-initdata-security
plan: 02
subsystem: security
tags: [telegram, hmac, subtlecrypto, initdata, cloudflare-kv, ttl, privacy-by-construction, tdd]
# Dependency graph
requires:
- bot/ npm workspace + config.ts Env contract (plan 02-01)
- bot/test/initdata.test.mjs RED vector suite T1–T7 (plan 02-01) — GREENed here
provides:
- `bot/src/security/initdata.ts` — validateInitData: pure SubtleCrypto HMAC-SHA256 validator with rejection classes (malformed/tampered/stale) + hand-rolled WebAppInitUser/WebAppInitChat/ParsedInitData types
- `bot/src/kv/sessions.ts` — TTL-only KV access layer: SESSION_TTL_SECONDS=900, SessionClaim (D-08 exact surface), mintSession (opaque UUID + rotate-on-new), getTokenForChat, getClaimByToken
- `bot/test/sessions.test.mjs` — D-07 rotate invariant + D-08 key-set-equality + TTL assertions with in-memory KV fake
- `bot/test/privacy.test.mjs` — PRIV-02 source-introspection gates (PG1 same-line expirationTtl, PG2 no persistence-shaped exports)
affects: [02-03 /api/session endpoint + wrangler KV id, phase 3 mini app shell, phase 5 guidance_sessions envelope]
# Tech tracking
tech-stack:
added: [] # zero added deps — global crypto.subtle + crypto.randomUUID only (D-11/D-12)
patterns: [RED-suite-first TDD (GREEN against pinned vectors), source-introspection privacy gates (grep-as-test), timing-safe hex compare, two-step Telegram HMAC key derivation, TTL-paired session envelope (initData window == KV TTL)]
key-files:
created:
- bot/src/security/initdata.ts
- bot/src/kv/sessions.ts
- bot/test/sessions.test.mjs
- bot/test/privacy.test.mjs
modified: []
key-decisions:
- "Secret key is the raw HMAC digest bytes, never the UTF-8 of its hex string — caught pre-run by mirroring the RED suite's node:crypto .digest() byte semantics"
- "Undecodable percent-escapes keep their raw value instead of dropping the field, so an unparseable payload fails the HMAC compare (tampered) rather than silently validating"
- "Privacy gate PG1 checks the TTL is a named constant reference (regexp on the identifier), not an inline literal — prevents expirationTtl: 1 drift"
- "PG1 auto-includes src/webhook.ts when it exists, so plan 03's endpoint inherits the gate without editing the privacy suite"
patterns-established:
- "Rejection precedence: malformed (structural) → tampered (HMAC) → stale (D-13 window) — callers log categories only (D-14)"
- "KV write discipline: expirationTtl visible on the same call line, enforced by PG1 against every storage source file"
requirements-completed: [PRIV-02]
# Metrics
duration: 10min
completed: 2026-10-22
---
# Phase 2 Plan 2: initData Validator + TTL-Only KV Sessions Summary
**HMAC-SHA256 initData validator with typed rejection classes GREENs the plan-01 vector suite, and a TTL-only KV session layer (opaque tokens, rotate-on-new, same-line expirationTtl) makes privacy-by-construction testable — 18/18 automated cases pass, zero runtime deps added**
## Performance
- **Duration:** ~10 min
- **Started:** 2026-10-22T03:36Z
- **Completed:** 2026-10-22T03:46Z
- **Tasks:** 2
- **Files created:** 4 (all under `bot/src/security`, `bot/src/kv`, `bot/test`)
## Accomplishments
- **TDD RED→GREEN:** the plan-01 RED suite (8 vectors at RED, commit `fc5001a`) is fully GREEN — T1 known-good, T2 tampered, T3 stale (3600 s), T4 boundary (899 ok / 901 stale), T5 out-of-order fields, T6a–c malformed, T7 RFC-4231 pin — 10 initdata cases passing against the implementation under test only (test signing stays on node:crypto, independent of the SubtleCrypto path)
- `validateInitData(raw, botToken, { nowSeconds?, maxAgeSeconds?=900 })` implements the Telegram-verified algorithm: `&`-split parse with single URL-decode (`+` kept literal — no URLSearchParams), check-string over fields minus `hash`/`signature` sorted by field name, two-step key derivation per RESEARCH Pitfall 2, and constant-time hex compare (grammY-mirrored byte-XOR loop)
- `src/kv/sessions.ts`: `mintSession` mints `crypto.randomUUID()` (T-02-05), reads the `chat:<chat_id>` pointer, deletes the previous `session:<token>` (D-07 rotate-on-new — structural close of the stale-replay mint window), and writes both keys with `expirationTtl: SESSION_TTL_SECONDS` on the call lines (PRIV-02)
- `SessionClaim` is exactly D-08 (`user_id`, `chat_id`, `issued_at`, `language_code`) — SM1 asserts key-set equality, so any drift toward profiles fails the suite
- `privacy.test.mjs`: PG1 greps every `.put(` line in storage sources for same-line `expirationTtl` and requires a named TTL constant (auto-extends to `src/webhook.ts` when plan 03 adds it); PG2 rejects persistence-shaped exports (saveProfile/storeLocation/saveLocation/…)
- `npm test -w bot` → **18 passed, 0 failed** across initdata + sessions + privacy suites; `npm run bot:typecheck` clean; no network/account/provider dependencies
## task Commits
1. **task 1: validateInitData — HMAC core with rejection classes (D-13, D-14; GREEN the RED suite)** — `54bffe7` (feat; RED counterpart `fc5001a` from plan 01)
2. **task 2: TTL-only KV sessions with rotate-on-new + PRIV-02 storage gates (D-06/D-07/D-08/D-14)** — `e9aec58` (feat)
## Files Created
- `bot/src/security/initdata.ts` — pure validator, zero imports, exports `validateInitData`, `InitDataRejection`, `WebAppInitUser`, `WebAppInitChat`, `ParsedInitData`, `ValidateInitDataOptions`, `InitDataResult`
- `bot/src/kv/sessions.ts` — exports `SESSION_TTL_SECONDS`, `SessionClaim`, `mintSession`, `getTokenForChat`, `getClaimByToken`; no grammy import (D-11)
- `bot/test/sessions.test.mjs` — SM1/SM2 mint shape+TTL, SR1/SR2 rotate invariant, SG1 language_code "ms" and null round-trips, SG2 expired class
- `bot/test/privacy.test.mjs` — PG1/PG2 source gates
## Decisions Made
- **Raw digest bytes for the derived secret key:** an early draft encoded the hex-string form of step-1 HMAC; the RED suite's node:crypto helper uses `.digest()` bytes, so the implementation was corrected to decode-into-bytes semantics *before* the first run — and passed all vectors first try as a result
- **Undecodable escapes stay raw:** a value with a broken percent-escape is not silently dropped; it flows into the check string and produces a hash mismatch (tampered) — classification over silent data loss
- **PG1 literal discipline:** comments and identifiers were trimmed so that `"WebAppData"` appears exactly once (the derivation call) and `grammy` zero times, satisfying the plan's literal grep acceptance checks while keeping the docs honest ("platform salt constant")
## Deviations from Plan
None - plan executed exactly as written. (Test-side fix of a self-inflicted fixture bug in sessions.test.mjs — destructuring a non-existent `sessions` export — was corrected before commit; no scope changes.)
## Threat Model Dispositions Applied
- **T-02-01** (forged initData): mitigated — HMAC-SHA256 over the data-check-string with the two-step derived key + timing-safe compare, pinned by T2/T5
- **T-02-02** (stale-but-valid replay): mitigated — D-13 900 s window (T3/T4) + rotate-on-new deletes the prior token (SR1/SR2)
- **T-02-05** (token prediction): mitigated — `crypto.randomUUID()`, asserted UUID-shaped in SM1
- **T-02-06** (persistent claim drift): mitigated — D-08 key-set-equality (SM1) + PG1/PG2 source gates
## Known Stubs
None. Every export is fully wired; the suites exercise real behavior (no placeholder data, no TODO/FIXME).
## User Setup Required
None for this plan. (Cloudflare/bot credentials remain plan 03's deploy checkpoint per D-15/D-16.)
## Next Phase Readiness
- Plan 03's `/api/session` endpoint consumes `validateInitData` → rejection class → mint via `mintSession`; `InitDataRejection` maps 1:1 onto D-14 category-only logs
- Plan 03 must keep the PG1 gate green: any new `.put(` in `src/webhook.ts` needs `expirationTtl: <named constant>` on its line (PG1 auto-picks the file up once it exists)
- KV namespace id (replace `KV_ID_PLACEHOLDER` in `bot/wrangler.jsonc`) remains plan 03's task
## Self-Check: PASSED
- All 4 created files verified present on disk
- Commits verified in history: `54bffe7` (task 1), `e9aec58` (task 2); no file deletions in either commit
- `npm test -w bot` → `[runner] 18 passed, 0 failed`; `npm run bot:typecheck` exit 0
- Acceptance greps: `"WebAppData"` ×1 and `grammy` ×0 in initdata.ts; both `.put(` lines in sessions.ts carry `expirationTtl: SESSION_TTL_SECONDS`
- STATE.md / ROADMAP.md untouched (orchestrator-owned)
## TDD Gate Compliance
Plan frontmatter type is `execute` (not `tdd`), but both tasks honor the cycle: RED exists from plan 01 (`fc5001a`, `test(...)`), GREEN follows in `54bffe7` (`feat(...)`). No refactor-gate commit needed — no post-GREEN restructuring was undertaken.
---
*Phase: 02-bot-skeleton-initdata-security*
*Completed: 2026-10-22*