Skip to content

Security: coaxel2/NotchIA

Security

SECURITY.md

Security policy & key rotation procedures

This document describes the threat model NotchIA defends against, the secrets that gate its security, and the rotation procedure for each.

Reporting a vulnerability

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.

Threat model

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

Critical secrets and rotation

ED25519_PRIVATE_KEY_HEX (Worker secret)

Used to sign verify responses. If leaked, an attacker can forge "license valid" payloads that any installed app accepts.

Rotation procedure:

  1. Generate a new keypair locally:
    cd /Users/axel/notchia-license
    npx tsx scripts/generate-keys.ts
  2. Update LicenseManager.swift constant verifyPublicKeyHex with the new hex public key.
  3. Ship a new app version (/release X.Y.Z) so installed users update their embedded pub key.
  4. Wait at least 14 days for users to update (the offline grace period).
  5. Push the new private key as a Worker secret:
    echo "new_priv_hex" | npx wrangler secret put ED25519_PRIVATE_KEY_HEX
  6. 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_KEY_HASH (Worker secret) + admin key

Master/admin license key for personal use. Bypasses Stripe entirely.

Rotation procedure:

  1. 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'))"
  2. Update LicenseManager.swift constant adminKeySha256Hex with the new SHA-256.
  3. Ship a new app version.
  4. Update Worker secret:
    echo "new_sha256_hex" | npx wrangler secret put MASTER_KEY_HASH

PRIVATE_SPARKLE_KEY (GitHub secret)

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_SECRET_KEY (Worker secret)

Stripe API key for creating Checkout sessions and reading subscriptions.

Rotation procedure:

  1. In the Stripe Dashboard → Developers → API keys → Create restricted key (RAK).
  2. Restrict scope to: Checkout Sessions (write), Customers (read), Subscriptions (read), Invoices (read), Billing Portal Sessions (write).
  3. Push to Worker:
    echo "rk_live_..." | npx wrangler secret put STRIPE_SECRET_KEY
  4. Roll the old key in Stripe Dashboard.

Recommend using Restricted API Keys (RAK, prefix rk_) rather than full secret keys (prefix sk_) — least privilege.

STRIPE_WEBHOOK_SECRET (Worker secret)

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" \
  ...

RESEND_API_KEY (Worker secret)

For sending license-delivery emails. Roll any time at https://resend.com/api-keys.

RELEASE_TOKEN and HOMEBREW_TAP_TOKEN (GitHub secrets)

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:

  1. Create new PAT at https://github.com/settings/personal-access-tokens/new
    • Repository access: coaxel2/NotchIA AND coaxel2/homebrew-notchia only
    • Permissions: contents: write, pull-requests: write
    • Expiration: 90 days
  2. 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_..."
  3. 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.

Defense-in-depth controls (no rotation needed)

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_ATTACH in 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, devmasx actions pinned to commit SHAs to mitigate supply-chain takeover.
  • @noble/ed25519 for crypto (well-audited library).
  • stripe.webhooks.constructEventAsync for webhook signature verification (the Workers-compatible async variant).

Ongoing audit cadence

  • Every 90 days: rotate RELEASE_TOKEN and HOMEBREW_TAP_TOKEN (PAT expiry).
  • Every 6 months: review pinned action SHAs, bump if upstream releases reviewed updates.
  • Every release: walk through RELEASE_CHECKLIST.md end-to-end.
  • Every dependency bump: read changelog for the changed package, especially @noble/*, stripe, hono.

There aren't any published security advisories