Does Keylight Work in Sandboxed Mac Apps?
Adding licensing to a Mac App Store app means working under the App Sandbox, and the first question every developer asks is whether their licensing SDK will survive that. Keylight does — the answer comes down to where it stores state and what the sandbox actually restricts. Since SDK 0.6.0 the default storage backend is a device-bound encrypted file under Application Support, which the sandbox relocates into your app’s own container and grants unconditionally; network calls for activation and lease renewal require one standard entitlement you almost certainly already have.
The sandbox question developers ask before adopting Keylight
The three concerns that come up most often are variations of the same fear: something the developer was relying on will silently stop working after sandboxing.
“Does the sandbox wipe UserDefaults on update?” Sometimes — not always. When you migrate an unsandboxed app into the sandbox, the defaults domain changes and existing UserDefaults entries become invisible. If your license state lived there, the customer looks unlicensed after the update even though they paid. This is a real problem for apps that store license data in UserDefaults, but Keylight does not use UserDefaults for the lease. The SDK writes an encrypted file into your app’s Application Support directory — a location the sandbox always grants your app, because under the sandbox it is your container.
“Will the SDK trigger a Keychain permission prompt?” Not by default. This is the reason the default changed: a Keychain-authoritative store made macOS show a permission prompt on first launch, which is a terrible first impression for an app the customer just paid for. As of 0.6.0 the Keychain is never read or written unless you explicitly opt in, so there is no prompt to explain away. If you do opt in — see below — the sandbox permits it: the App Sandbox allows Keychain reads and writes against the app’s own access group without any additional entitlement.
“Are network calls denied in a sandboxed app?” By default, yes — outbound connections require the com.apple.security.network.client entitlement. Keylight makes network calls only for activation and for the periodic lease revalidation check, so you need to enable this entitlement. The good news is that com.apple.security.network.client is the standard entitlement every sandboxed app that fetches anything from the network already has; it is not Keylight-specific.
What the SDK actually stores, and where
The storage layer is deliberately narrow: one encrypted blob per tenant-and-product combination, at
<Application Support>/.keylight/<tenantId>/.kl
Outside the sandbox, <Application Support> is ~/Library/Application Support. Inside it, the same API returns ~/Library/Containers/<bundle-id>/Data/Library/Application Support — your container. You do not configure this and you do not need an entitlement for it; it is the one directory the sandbox exists to give you.
The blob is not merely obfuscated. It is a 4-byte KL01 magic followed by an AES-256-GCM sealed box, under a key derived with HKDF-SHA256 from the device identity — the IOPlatformUUID on macOS — with your tenantId and productId folded into the HKDF info. Two Keylight-powered apps on the same Mac therefore derive different keys, and a blob copied to a second Mac does not open on it. Tampering fails the GCM authentication tag rather than silently decoding.
The stored payload is the full signed lease — the same Ed25519-signed document the server issues on activation. Every time the SDK reads the cached lease from disk it re-verifies the signature locally before trusting the contents, so even a successful decryption of a modified lease is caught. For more on how the signed lease itself works, see where to store license data on macOS.
When the Keychain is still involved
Two cases, both deliberate:
- You asked for it.
storage: .encryptedFile(keychainMirror: true)keeps the file authoritative and maintains a Keychain copy for recovery;storage: .keychainrestores the pre-0.6.0 behavior where the Keychain is authoritative and the file is the crash-recovery fallback. Use the mirror if you read license state outside the SDK, or want the state to ride along with Keychain sync and restore. - The file is not available. On a platform with no Application Support directory (watchOS) or when no stable device ID can be read, the SDK falls back to the Keychain rather than failing. It never locks a paying customer out to protect a storage preference.
When the Keychain is used, it holds one generic-password item keyed by kSecAttrService — a service prefix scoped to dev.keylight.<tenantId>, so apps never collide on service names — and kSecAttrAccount, the product identifier. Accessibility is kSecAttrAccessibleAfterFirstUnlock: readable after the device has been unlocked once since boot, which covers an app launched at login by a Launch Agent before the user has logged in interactively. kSecAttrAccessibleWhenUnlocked would be unavailable in that scenario.
On iOS and the other Apple platforms the SDK opts into the Data Protection Keychain (kSecUseDataProtectionKeychain). On macOS it uses the standard Keychain Services API instead and does not set that flag, because the Data Protection Keychain on macOS requires the keychain-access-groups entitlement that not every build configuration has. The SDK will not break your build by demanding entitlements you may not have.
If you are upgrading an app that shipped a pre-0.6.0 SDK
Existing on-disk state migrates automatically and popup-free: on first load the SDK reads the legacy files, re-seals them into the encrypted file, and deletes the originals. Migrated installs keep their license with no re-activation.
The case that needs your attention is an app that was Keychain-authoritative — either a pre-0.6.0 build, or one that explicitly shipped storage: .keychain. Ship a bare .encryptedFile() into that install base and the SDK finds nothing on disk, isEntitled goes false, and paying customers land on a paywall on update. Ship this instead for one release:
storage: .encryptedFile(keychainMirror: true)
The composite reads the license and trial out of the Keychain on first launch, heals them into the file, and the customer sees nothing. Once your install base has moved you can drop back to a bare .encryptedFile().
Required entitlements for direct distribution vs Mac App Store
For direct distribution — downloading a .dmg or .zip from your own site — the sandbox is optional. You can ship without sandboxing and no special entitlements are required. Keylight works out of the box: the SDK reads and writes its own file under Application Support, and the network calls use URLSession.
For Mac App Store distribution, the sandbox is mandatory, and you need at minimum two entitlements:
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<!-- only if you opted into Keychain storage AND share licenses across bundle IDs -->
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)dev.acme.shared</string>
</array>
com.apple.security.app-sandbox enables the sandbox itself — App Review requires it. com.apple.security.network.client allows outbound TCP connections so the SDK can reach the Keylight API for activation and lease refreshes. Without it, the first activation attempt returns a network error.
keychain-access-groups is needed only in a narrow case: you opted into a Keychain backend and you have a helper tool, an XPC service, or a separate companion app under a different bundle ID that also needs to read the license state. The access group string must match a prefix in your provisioning profile (the $(AppIdentifierPrefix) macro expands to your Team ID at build time). On the default file backend, and on the Keychain backend when your licensing lives entirely in the main app bundle — the common cases — you do not need this entitlement at all.
For a deeper look at the storage question generally, see store a signed license token in the macOS Keychain and the broader discussion in where to store license data on macOS.
Edge case: Mac App Store apps that also accept direct license keys
Some developers want a single binary that ships on the App Store and accepts a direct license key entered by customers who bought from the developer’s own site. This is technically possible — the Keylight SDK has no hard block on running inside a sandboxed MAS build — but it is an edge case with real consequences.
Apple’s App Store Review Guidelines discourage in-app purchase alternatives for digital goods. If your app accepts a license key to unlock features that could be sold via StoreKit, a reviewer may flag it as circumventing in-app purchase. The outcome depends on how your app is categorized and how the flow is presented, but the risk is non-trivial. App Review has rejected apps for exactly this pattern.
The cleaner pattern — and the one that avoids ambiguity with Apple — is to ship two separate binaries from the same codebase:
- An App Store build that conditionally compiles away the Keylight license-key input flow and routes upgrades through StoreKit.
- A direct build with no StoreKit dependency, distributed via your own site, that uses Keylight license keys as the sole purchase path.
In both cases the guarantee is cryptographic: the signed lease is Ed25519-signed by the server and re-verified locally on every read — not bundle-identity-based. A dual-binary approach works because the Keylight API does not care which binary is making the call; it cares about the SDK key, the license key, and the tenant. You can share a Keylight product and key prefix across both builds and the customer’s license covers both.
The in-app purchase restriction applies to the App Store build; the direct build has no such constraint. Keeping the two binaries separate is cleaner than building a single binary that conditionally hides purchase UI from the App Review team.
For the full treatment of how Keylight issues and verifies license keys, including the signed lease format and activation mechanics, the features page covers it.
Whether you are shipping under the sandbox, via direct download, or both, the advice is the same: pick the distribution channel and build the right binary for it. If you run into anything the entitlement guidance above does not cover, send us your feedback and we will extend this post.
Frequently asked
Does the Keylight Swift SDK work in a sandboxed Mac app?+
Yes. By default the SDK seals its lease into a device-bound encrypted file under Application Support, which resolves to the app container under the sandbox and needs no extra entitlement. You need the App Sandbox entitlement itself plus com.apple.security.network.client for activation.
What entitlements does my Mac app need to use Keylight?+
For direct distribution outside the App Store, no special entitlements are required. For Mac App Store distribution under the sandbox, enable com.apple.security.app-sandbox and com.apple.security.network.client. Add keychain-access-groups only if you opt into Keychain storage and share license state across bundle IDs.
Can a Mac App Store app also accept a direct license key?+
Technically yes, but Apple discourages it and may reject the build. The cleaner pattern is to ship two binaries: an App Store build that uses StoreKit, and a direct build that uses Keylight license keys.
Ready to ship?
Create your account and start licensing your apps in under a minute. Free forever tier included.
Start Free