Files
Arah2/.planning/phases/02-bot-skeleton-initdata-security/02-02-SUMMARY.md
T

8.7 KiB
Raw Blame History


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