forked from c-fu/arah2
docs(02-02): complete initData validator + TTL-only KV sessions plan
This commit is contained in:
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
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*
|
||||||
Reference in New Issue
Block a user