Shottr License Validation

A static-analysis writeup of how Shottr (macOS, bundle id cc.ffitch.shottr) validates licenses. Observations are from the shipped Mach-O at /Applications/Shottr.app/Contents/MacOS/Shottr and the user's preferences plist. Architecture only — no bypass.

TL;DR.

Endpoints

PurposeURL
Primary verifyhttps://shottr.cc/licensing/verify.php
Fallback verifyhttps://shottr-verify-license.blimps.workers.dev
Restorehttps://shottr.cc/restore.php
Purchase landinghttps://shottr.cc/purchase.html
Upload (per-user)https://gtfrog…lambda-url.eu-central-1.on.aws/?token=…
Update manifesthttps://shottr.cc/api/version.json?anticache=…
Telemetryhttps://tel.shottr.cc/telemetry.php

The string License checking, first URL failed ( confirms primary-then-fallback. If shottr.cc is down, the Cloudflare Worker still answers, so users do not get locked out of the app due to a single point of failure.

The validation method

Swift symbol: verfiyLicenseOnline(_:type:whenReady:). The misspelling ("verfiy" vs "verify") is baked into the symbol table, so it predates the shipped binary. Called from:

Other relevant Swift symbols: PrefLicenseViewController, KeychainSwift, QueueKeychainAccess, mainAppService.

Local state

Keychain — source of truth

License data is stored via a KeychainSwift wrapper under a mainAppService service identifier. Diagnostic strings tell the story:

Failed to save to keychain
MacOS prevented Shottr from retrieving license information from the keychain.
Lost keychain record, but there's a backup (

Preferences plist — XOR backup

File: ~/Library/Preferences/cc.ffitch.shottr.plist. Relevant keys:

kc-license
Base64 over XOR-obfuscated license bytes. XOR key referenced in the binary as backupXorKey.
token
Short opaque token used by the upload feature.
uploadEndpoint
Per-user signed AWS Lambda URL for screenshot uploads.

Architecture: keychain first, plist as fallback if the keychain read is denied (sandbox lockdown, permission UI dismissed, etc.).

The license payload

Server response is JSON. From the binary you can see the shape:

There are no Paddle / Gumroad / FastSpring / LemonSqueezy / Stripe strings in the binary. There is no StoreKit / IAP. It is a plain license-key model, vendor-hosted.

Flow: Activation

  1. User pastes a key into tfLicense and clicks btnActivate (PrefLicenseViewController).
  2. verfiyLicenseOnline POSTs the key to shottr.cc/licensing/verify.php.
  3. On non-2xx / timeout / network failure, retries against the Cloudflare Worker shottr-verify-license.blimps.workers.dev.
  4. Response JSON yields {active, tier, …}.
  5. License stored to the Keychain, XOR-obfuscated kc-license backup written to the plist.
  6. Logs License is checked, UI shows Thank you for activating Shottr!, gated features unlock.

Flow: Restore

  1. User clicks Restore → clickRestoreLicense: opens shottr.cc/restore.php in the browser.
  2. Web form takes the purchase email; backend emails the license key.
  3. User pastes the key back into the Preferences pane.
  4. Same verfiyLicenseOnlinerestore(json:active:) path as activation.

Flow: Re-check at launch and over time

The app holds enough state in Keychain to self-validate offline at launch. Periodic re-checks reconcile tier changes (License has changed, tier stayed the same) and detect explicit revocations (License was removed, License information lost, Shottr: no valid license provided).

Useful log lines

License checking, first URL failed (
License not provided
Tier not returned
Failed to decrypt
MacOS prevented Shottr from retrieving license information from the keychain.
Lost keychain record, but there's a backup (
License is checked

Design observations