Licensing
A paid tweak has to decide, on its own, whether it is licensed. It runs inside someone else's process with no network guarantee and no app running alongside it, so the decision is made from a file on disk that was signed by the store.
The receipt
After a purchase is activated, the store writes a receipt to
/Library/TweakInject/Receipts/Entries/<packageID>.receipt.
It names the package, the machine it was issued for, and when it expires, and
it carries a P-256 ECDSA signature over those three values.
The tweak checks three things, and all three must hold:
- The signature verifies against a public key compiled into the tweak. The private half never leaves the store, so a receipt cannot be written by anything else.
-
The machine matches. The receipt binds to a hash of this
Mac's
IOPlatformUUID, so copying the file to another machine produces a receipt that verifies but does not apply. - It has not expired. Receipts last 60 days and are renewed weekly by a background agent. A refunded or deactivated licence simply stops being renewed, and the tweak goes inert when the lease runs out.
That last point is worth being plain about: someone can copy a tweak's files before refunding. The 60 day lease is what handles it, not the file permissions. The copy keeps working until the receipt expires, and then stops.
The simple check
Include the header, and call TILicenseCheck
with your package identifier.
#import "TILicense.h"
__attribute__((constructor))
static void init(void) {
if (!TILicenseCheck("com.example.mytweak")) return;
// ... install hooks here
}
This is correct, and it is also the first thing anyone patching your tweak will look for. One branch decides everything, and inverting a single conditional defeats it.
Making the check harder to remove
TI_Support ships macros that spread the decision through the work itself rather than gating it behind one branch. They are not unbreakable, and nothing running on someone else's machine can be. What they do is remove the single obvious place to cut.
Dispatch instead of branching
TIL_DISPATCH and
its variants compute the address to call arithmetically, from the result of
the licence check. A licensed run computes your function; an unlicensed one
computes an inert stub. There is no conditional jump to invert and no direct
reference to your function for a cross-reference search to land on.
TIL_DISPATCH("com.example.mytweak", install_my_hooks);
Hooks that only install when licensed
TIL_HOOK wraps a
hook installation. When the licence checks out it forwards to
MSHookFunction. When it does not, the replacement pointer is
poisoned into non-canonical address space and the call goes to a stub, so the
hook is never installed rather than being installed and then disabled.
TIL_HOOK("com.example.mytweak", target_fn, my_replacement, &orig_fn);
Guarding data pointers
TIL_GUARD_PTR
passes a pointer through unchanged when licensed, and XORs it into an
unmapped address when not, so an unlicensed path fails at the point of use
rather than silently continuing with real data.
Where to put the check
Spread it. A single call in your constructor is one place to remove. The macros are cheap enough to use at several points, and a check inside the work is harder to excise than a check at the door.
The full reference for each macro, including the exact signature, is under TI_Support.