Skip to content

Subscription-access tracking + sharing detection (store-side)#18

Open
Ayymoss wants to merge 2 commits into
RaidMax:masterfrom
Ayymoss:feature/plugin-hardening
Open

Subscription-access tracking + sharing detection (store-side)#18
Ayymoss wants to merge 2 commits into
RaidMax:masterfrom
Ayymoss:feature/plugin-hardening

Conversation

@Ayymoss

@Ayymoss Ayymoss commented May 31, 2026

Copy link
Copy Markdown
Contributor

What this adds

Visibility into how plugin subscriptions are used, so RaidMax can spot people reusing one subscription across multiple installs (paying once, running many) and see which plugins are in play. Lives entirely in the existing ecommerce app, reuses Firebase + ChargeBee — no new infrastructure or dependency — and is a no-op until ECOMMERCE_TRACKING_SECRET is set, so merging/deploying changes nothing on its own.

Scope note: an earlier revision of this branch also added a signed per-instance entitlement gate. It was dropped — the client-side check is a single boolean, patchable in seconds, and adds little over the existing delivery gate (content is already only decryptable by an active subscriber). What's kept is the part with standalone value: usage/sharing visibility.

Access tracking

get_subscribed_content() now records, per content fetch, which instance_id pulled which plugins for a subscription (plugin ids derived from the content_url basename — no extra ChargeBee call). Stored in Firebase under subscription_access/{email_hash}/{instance_id}.

Admin report — GET /plugin/sharing_report

Two views:

  • by_email — one subscription on >= N concurrently active instances (one paid email, many installs), with the plugins in use.
  • by_instance — one instance_id pulling content under more than one subscription email.

Active-concurrent rather than lifetime-distinct, because instance_id regenerates on reinstall / wiped config / ephemeral Docker (lifetime counts would false-flag honest users).

Privacy

  • Emails and IPs are stored only as keyed HMAC-SHA256 (ECOMMERCE_TRACKING_SECRET) — raw values never persisted.
  • Report is admin-only (X-Admin-Key == ECOMMERCE_ADMIN_KEY); the ecommerce app is internet-facing, so it returns aggregates keyed by hashed email, never raw emails.

Actions needed, and why

  1. Set env ECOMMERCE_TRACKING_SECRET (any high-entropy string). Why: tracking is disabled until set; it's the HMAC key so the datastore never holds raw emails/IPs and a leak can't be correlated to customers.
  2. Set env ECOMMERCE_ADMIN_KEY (any high-entropy string). Why: the report endpoint is internet-facing and returns 401 unless X-Admin-Key matches it.

That's it — no keys to mint, no client changes, no infra changes. Details in src/ecommerce/PLUGIN_HARDENING.md.

Infrastructure impact

None. Same Flask app, same Firebase (IA_DATA_*), same ChargeBee. Deploying without the env vars set leaves existing behaviour byte-for-byte unchanged.

Ayymoss added 2 commits May 31, 2026 21:16
Server-side hardening for paid plugins that are streamed to clients (no DLL
transfer), so a dumped/redistributed copy is harder to extract and useless without
an active subscription. Slots into the existing ecommerce app (Firebase + ChargeBee)
with no new infrastructure; all new behaviour is opt-in via env vars and a no-op
until those are set.

Entitlement gate (GET /plugin/entitlement):
- Issues a short-lived ECDSA P-256/SHA-256 signed token bound to instance_id, only
  for plugins the email is actively subscribed to (else 403). The plugin verifies
  the signature with an embedded public key and fails closed when it enforces.
- ECDSA chosen so both `cryptography` and the .NET BCL verify natively (no extra dep);
  signatures are DER (RFC 3279). The exact signed string is returned verbatim so the
  client never re-serializes (no cross-language canonicalization risk).
- signing_helper.py loads the private key from ECOMMERCE_SIGNING_KEY (PKCS#8 PEM) with
  a clearly-marked DEV fallback; generate_signing_keypair.py mints a prod keypair.

Sharing detection (access logging on /plugin_subscriptions + admin GET /plugin/sharing_report):
- Records which instance_id fetches content per subscription, reusing the existing
  Firebase provider. Emails and IPs are stored only as keyed HMACs
  (ECOMMERCE_TRACKING_SECRET); raw values are never persisted.
- Report is admin-only (X-Admin-Key == ECOMMERCE_ADMIN_KEY) and returns aggregates
  keyed by HMAC'd email, never raw emails. Flags concurrently-active instances rather
  than lifetime-distinct (instance_id regenerates on reinstall/ephemeral Docker).

See src/ecommerce/PLUGIN_HARDENING.md for env vars, key generation, and rollout.
Per discussion, the client-side entitlement gate is trivially bypassable (single
boolean, patchable in seconds) and adds little over the existing delivery gate, so it
was dropped. This keeps only the part with standalone value: visibility into how
subscriptions are used.

- Remove the entitlement endpoint + ECDSA signing (signing_helper, key generator,
  plugin_entitlement resource, get_entitled_plugin_ids).
- Access tracking now also records the plugin ids served per fetch (derived from the
  content_url basename, no extra ChargeBee call), so the report shows what each
  account/instance uses.
- Sharing report now returns two views: by_email (one subscription on multiple
  concurrently-active instances) and by_instance (one instance_id under multiple
  subscription emails). Emails/IPs remain HMAC'd; report stays admin-only.

Docs updated (PLUGIN_HARDENING.md). Still a no-op until ECOMMERCE_TRACKING_SECRET set.
@Ayymoss Ayymoss changed the title Plugin entitlement signing + subscription-access tracking (store-side hardening) Subscription-access tracking + sharing detection (store-side) May 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant