Skip to content
Ritesh FirodiyaGet in touch

Work / Chitragupt / Wiki / Decisions

2026-09-01-broker-token-encryption

Decisioncanonicalverified 2026-09-01

DECISION.2026-09-01.BROKER-TOKEN-ENCRYPTION

Broker access tokens get real Cloud KMS encryption — and a wiki correction: PAN isn't actually encrypted today

Decision

Broker access tokens (linked_broker_accounts/{id}.access_token) are encrypted at rest using Google Cloud KMS envelope encryption (apps/functions/src/_lib/kms.ts, key ring in asia-south1), via a new EncryptedFieldSchema shape ({ ciphertext_base64, kms_key_version }) in packages/shared/src/schemas/linked-broker-account.ts.

Separately, and as part of scoping this: kyc previously claimed "PAN is stored encrypted" — this is false. No encryption utility of any kind exists anywhere in this codebase; PAN is a plain, format-validated string on both identities/self.pan and the legacy users/{uid}.pan. kyc has been corrected to state this accurately. This decision does not fix PAN encryption — that's a separate, real gap, left unaddressed here deliberately to avoid silently expanding this change's scope.

Why

Building the broker-connect feature required "mirror the PAN encryption precedent" per the original scoping — that precedent turned out not to exist when checked against the real code (confirmed via full grep of apps/functions/src and packages/shared/src for encrypt|decrypt|KMS|cipher|aes: zero relevant hits; the only crypto-adjacent utility, _lib/hash.ts, is one-way SHA-256, unusable for a value that must be decrypted back out). Per this repo's own rule ("Drift from wiki is a bug — fix the code or update the page, never let two truths coexist"), the wiki gets corrected rather than left stating something false.

Real KMS (not apps/functions's usual process.env secrets pattern) was chosen because: (1) .claude/rules/backend.md reserves process.env for the app's own shared config secrets (Razorpay, Resend, Anthropic) — a KMS key is structurally different, since the key material never leaves KMS and only IAM access matters; (2) broker tokens are live, third-party financial-account credentials, a materially higher-stakes value than anything currently stored in this codebase; (3) asia-south1 KMS key rings satisfy data-residency-dpdp the same way Firestore/Storage/Functions already do.

Impact

  • New packages/shared/src/schemas/linked-broker-account.ts — LinkedBrokerAccountSchema, EncryptedFieldSchema, BrokerProviderSchema.
  • New packages/shared/src/schemas/broker-api-pull.ts — BrokerApiPullRecordSchema, the source-artefact record required by 2026-09-01-broker-api-sync-as-source.
  • New apps/functions/src/_lib/kms.ts — encryptField/decryptField, with a startup assertion (production refuses to boot without BROKER_TOKEN_KMS_KEY_NAME) and a dev/emulator stub, mirroring _lib/kyc-provider.ts's existing stub-vendor pattern.
  • New apps/functions/package.json dependency: @google-cloud/kms.
  • firebase/firestore.rules — two new collections, linked_broker_accounts and broker_api_pulls, owner+admin read only (deliberately not CA-shared, unlike portfolio_holdings/portfolio_positions), server-only writes.
  • kyc — corrected; last-verified bumped, "PAN is stored encrypted" line fixed.
  • Not addressed: actual PAN encryption (real gap, separate task). The KMS key ring itself is not provisioned by this change — gcloud kms keyrings create/cryptokeys create in asia-south1 is a deploy-time ops step, same as existing Razorpay/Resend key provisioning. The connect/disconnect/sync callables that call encryptField/decryptField are Phase 9 sub-phase 5, not built in this pass.

Status

Active.

Sources

  • No .context/raw/ file — direct code investigation in this chat session (grep of apps/functions/src and packages/shared/src, confirming no encryption utility exists).
  • apps/functions/src/_lib/kyc-provider.ts — the stub/startup-assertion pattern this decision's _lib/kms.ts mirrors.
  • apps/functions/src/_lib/hash.ts — the one existing crypto utility, confirmed to be one-way hashing only, not applicable here.

Every project of mine is written down like this.

Read the résumé