This document describes the threat model NotchIA defends against, the secrets that gate its security, and the rotation procedure for each.
Report security issues via the GitHub Security Advisory
Report a Vulnerability tab,
or by email to support@notchia.app. Don't open a public GitHub issue for security
issues — disclose privately first. We'll acknowledge within 72 hours.
For third-party dependency vulnerabilities, report to the upstream maintainer.
NotchIA is a paid macOS app distributed outside the App Store, with a Cloudflare Worker backend that issues and validates licenses. We defend against:
| Threat | Mitigation |
|---|---|
| Attacker generating valid licenses without paying | Stripe webhook signature + license keys never stored in plaintext |
| User sharing a license across N>3 Macs | Server-side machine binding, capped at 3 per license |
| Reverse engineer extracting embedded secrets | Only the Ed25519 public key + a SHA-256 hash of the admin key are embedded — both useless without their preimage / private counterpart |
| MITM serving fake "license valid" responses | Ed25519 signature verification on every /v1/license/verify response, with anti-replay (issued_at + ttl, ±5 min skew) |
| Bulk DoS of the Worker | Atomic rate limiting via Durable Objects + Cloudflare edge protection |
| Email reset spam DoS'ing a legitimate user | /v1/license/activate capped at 1 reset/hr + 3 resets/day per email |
| D1 database leak revealing license keys | Keys stored as SHA-256 hashes only — DB leak doesn't yield usable keys |
| Stripe API key leak | Stored as Cloudflare Worker secret, never logged or returned in responses |
| Supply-chain attack via mutable GitHub Action tags | All third-party actions pinned to commit SHAs in release.yml |
Used to sign verify responses. If leaked, an attacker can forge "license valid" payloads that any installed app accepts.
Rotation procedure:
- Generate a new keypair locally:
cd /Users/axel/notchia-license npx tsx scripts/generate-keys.ts - Update
LicenseManager.swiftconstantverifyPublicKeyHexwith the new hex public key. - Ship a new app version (
/release X.Y.Z) so installed users update their embedded pub key. - Wait at least 14 days for users to update (the offline grace period).
- Push the new private key as a Worker secret:
echo "new_priv_hex" | npx wrangler secret put ED25519_PRIVATE_KEY_HEX
- Old responses signed with the old private key remain valid offline up to the cache TTL (24h). After that they're rejected.
Backup: keep the current private key in 1Password / Bitwarden / encrypted offline storage.
Master/admin license key for personal use. Bypasses Stripe entirely.
Rotation procedure:
- Generate a new admin key (~32 chars random):
node -e "const c=require('crypto');const b=c.randomBytes(20);const a='0123456789ABCDEFGHJKMNPQRSTVWXYZ';let v=0,bi=0,o='';for(const x of b){v=(v<<8)|x;bi+=8;while(bi>=5){bi-=5;o+=a[(v>>bi)&31]}}if(bi)o+=a[(v<<(5-bi))&31];console.log('nia_admin_'+o);console.log('SHA256:',c.createHash('sha256').update('nia_admin_'+o).digest('hex'))" - Update
LicenseManager.swiftconstantadminKeySha256Hexwith the new SHA-256. - Ship a new app version.
- Update Worker secret:
echo "new_sha256_hex" | npx wrangler secret put MASTER_KEY_HASH
Used to sign Sparkle update DMGs. If leaked, an attacker can sign and distribute a malicious "update" that every installed user auto-installs.
Rotation is destructive — see ~/Downloads/macos-app-publishing-skill/SKILL.md.
The current private key is also at /Users/axel/notchia-license/keys/sparkle_priv.txt
(gitignored). Back this up to 1Password.
Stripe API key for creating Checkout sessions and reading subscriptions.
Rotation procedure:
- In the Stripe Dashboard → Developers → API keys → Create restricted key (RAK).
- Restrict scope to: Checkout Sessions (write), Customers (read), Subscriptions (read), Invoices (read), Billing Portal Sessions (write).
- Push to Worker:
echo "rk_live_..." | npx wrangler secret put STRIPE_SECRET_KEY
- Roll the old key in Stripe Dashboard.
Recommend using Restricted API Keys (RAK, prefix rk_) rather than full secret keys
(prefix sk_) — least privilege.
Per-endpoint signing secret. Rotates only if the webhook endpoint is recreated. Use the Stripe CLI to re-create endpoints if needed:
stripe webhook_endpoints create \
--url https://notchia-license.axel-courty.workers.dev/v1/webhook/stripe \
-d "enabled_events[]=checkout.session.completed" \
...For sending license-delivery emails. Roll any time at https://resend.com/api-keys.
Recommendation: replace the current broad-scope OAuth token (gho_...) with a
fine-grained Personal Access Token scoped to only the two repos you actually need:
- Create new PAT at https://github.com/settings/personal-access-tokens/new
- Repository access:
coaxel2/NotchIAANDcoaxel2/homebrew-notchiaonly - Permissions:
contents: write,pull-requests: write - Expiration: 90 days
- Repository access:
- Set both secrets:
gh secret set RELEASE_TOKEN -R coaxel2/NotchIA -b "github_pat_..." gh secret set HOMEBREW_TAP_TOKEN -R coaxel2/NotchIA -b "github_pat_..."
- Revoke the old token at https://github.com/settings/tokens
Why it matters: the workflow uses third-party actions (pinned to SHAs as defense). If a
malicious action exfiltrated RELEASE_TOKEN, a fine-grained PAT limits blast radius to
those two repos. The current OAuth token has access to every repo on your account.
These are structural choices documented for auditors:
ENABLE_HARDENED_RUNTIME = NO: required for ad-hoc signing on macOS Sequoia/Tahoe.com.apple.security.app-sandbox = false: required for non-App-Store distribution.- Anti-debug
PT_DENY_ATTACHin Release builds (LicenseManager.swift). Bypassable by kernel debugger / pre-main attach — speed bump only. - Constant-time comparisons on all hash equality checks (admin key, etc.).
- Server-side rehash of
machine_id: client value hashed again with a per-app prefix before DB storage. - Atomic rate limiting via Durable Objects (
lib/rateLimiterDO.ts). Replaces the previous KV fixed-window limiter that had a TOCTOU race. - Pinned third-party GitHub Actions: all
xt0rted,peter-evans,softprops,devmasxactions pinned to commit SHAs to mitigate supply-chain takeover. @noble/ed25519for crypto (well-audited library).stripe.webhooks.constructEventAsyncfor webhook signature verification (the Workers-compatible async variant).
- Every 90 days: rotate
RELEASE_TOKENandHOMEBREW_TAP_TOKEN(PAT expiry). - Every 6 months: review pinned action SHAs, bump if upstream releases reviewed updates.
- Every release: walk through
RELEASE_CHECKLIST.mdend-to-end. - Every dependency bump: read changelog for the changed package, especially
@noble/*,stripe,hono.