boringBar activated in an hour and 46 bytes

The goal
Produce an offline build of boringBar: one that starts, reaches the main
event loop, serves its usual bar/thumbnail/launcher functionality, and shows
"activated" in Settings — all without making a single outbound HTTPS or NTP
call.
The binary
| Property | Value |
|---|---|
| Bundle ID | app.boringbar.stable |
| Version | 0.5.7 |
| Arches | x86_64 + arm64 universal |
| Min macOS | 13.0 |
| Language | Swift + SwiftUI + AppKit |
| Stripped | No — full Swift symbols preserved |
| Update framework | Sparkle 2.9.0 (Ed25519 appcast signing) |
| License crypto | CryptoKit P-256 ECDSA, SHA-256, Secure Enclave |
| License transport | GraphQL over HTTPS to https://boringbar.app |
The missing "strip" step is the whole reason the afternoon was an afternoon
and not a week. nm -U binary | xcrun swift-demangle is an enormous gift.
Toolchain
lipo -thin arm64to drop the x86_64 sliceotool -L,codesign -d,plutil -pfor surface reconnaissancenm -U | xcrun swift-demanglefor the full symbol table (12,191 symbols,
92 app-level types)radare2withaaa+pdcfor pseudo-decompilation andrasm2for
instruction encoding- Python +
structfor surgical byte patches sample,lsof -p -i,ps,defaultsfor live measurementscodesign --force --sign -for ad-hoc re-signing after every edit
No IDA, no Hopper, no Ghidra. pdc is not a great decompiler, but with Swift
symbols in hand it was enough.
Pass 1: static analysis (the theory that was mostly wrong)
First hour was spent reading the decomp and writing it up. Ninety-two types
were mapped; the licensing-relevant ones:
The embedded GraphQL mutations were sitting in __TEXT.__cstring:
The embedded symbols named an ES256 public key (jwtPublicKeyPEM), Swift
CodingKeys structs (JWTClaims, GaneshaData), and a proof-of-work solver
(WorkRequest { challenge, difficulty }, WorkSolution { nonce, response
}) — hashcash-style PoW for backend rate-limiting. None of this ends up
mattering for the offline question, but the static picture was useful.
Initial (incorrect) hypothesis: patch LicenseLaunchGate.start() to emit
.valid(expiresAt: .distantFuture) and be done. "Five-line fix."
Then we tested it.
Pass 2: lab notebook (four experiments before anything worked)
E1 — "Can I just edit trialEndAt in defaults?"
Within a second of launch, the injected 2099 date was overwritten back to
2026-04-27 18:23:50 — the JWT's exp, to the second. So the defaults key
is a pure derived cache, not an authority. Fishing around the app
container then turned up:
Decoded:
uuid matches IOPlatformUUID. serial matches
IOPlatformSerialNumber. exp matches defaults trialEndAt exactly. The
"trial" is an ES256 JWT signed by the backend and bound to this specific
Mac's hardware identity. The defaults value is a UI cache rewritten every
launch from the JWT's claims.
Corollary: the Secure Enclave key code path is not the trial mechanism
(no Keychain items ever appear for personal trials) — it's the business
activation path we reached but never exercised.
E2 — forged JWT test, and a Y2038 bonus
Forged a token with exp = 2147483700 (Jan 19 2038 03:15:00 UTC, past the
Int32 boundary) while keeping the original signature (so it was guaranteed
invalid) and dropped it over the real one. The app:
- Discarded it on signature verification fail.
- Silently re-fetched a new token from
https://boringbar.app. - Got back the same
exp(2026-04-27), proving the server is
stateful per-hardware. Deleting the token, nuking~/Library, or
reinstalling does not reset the trial — the server pins it to
(IOPlatformUUID, IOPlatformSerialNumber).
Y2038 answer: Swift Foundation.Date is backed by TimeInterval (Double,
64-bit). The JWT decode path flows through JSONDecoder into a Codable
struct with no .secondsSince1970 strategy symbol in the binary, meaning
integer exp → Double conversion is the code path. No Int32 narrowing
anywhere. No Y2038 risk by construction.
E3 — valid tokens are not re-fetched on launch
Relaunched with an unchanged valid token on disk:
So the client verifies the local JWT against the embedded PEM on every
launch, extracts exp, and only calls the server if the token is missing
or untrustworthy. This is the load-bearing fact for an offline build: with
a valid token in place, no network is touched on steady-state launches.
E4 — patching the binary to prove it runs offline
Copied boringBar.app, thinned to arm64, rewrote every outbound host in
__TEXT.__cstring and __DATA.__data to an unroutable equivalent of the
same byte length (Swift strings are length-prefixed, so same-length edits
are safe):
codesign --force --sign - (ad-hoc), direct-exec from Contents/MacOS/
to bypass LaunchServices' habit of picking the original notarized bundle
on a bundle-ID match, and sample the process:
Textbook idle at the AppKit event loop. Zero open sockets (lsof -p -i).
Zero NTPClient / LicenseClient / LicenseStateMachine / TrialExpired
/ PermissionsGate symbols in the stack. SwiftUI actively drawing
(NSHostingView.layout, updateConstraints).
The offline question was empirically settled with a 6-byte string patch.
Pass 3: hiding the trial chip (the @Published gotcha)
The bar still showed "trial 13 days left". The obvious move — patch
LicenseStateMonitor.isActivated.getter with mov w0, #1; ret — did
nothing visible. Because:
A property declared as @Published var isActivated: Bool has three
distinct read paths, only one of which is the getter:
- Plain method call (
monitor.isActivated) → hits the getter. - SwiftUI
@ObservedObject/ key-path reads → bypass the getter and
read the_isActivatedPublished<Bool>backing storage directly
via theCombine.Publishedkey-path subscript. - Combine
.sink→ also reads the backing storage.
SwiftUI always uses (2). Our getter patch only touched (1). This is true
of every @Published property in every SwiftUI app — "patch the getter"
is rarely correct for Combine-observed state.
Tracing the actual writes in runInitialSetup.allocator__16 (the
trial-path branch) revealed the real plumbing:
Combine.Published._enclosingInstance.wrapped.storage is the
property-wrapper setter SwiftUI actually observes. We needed to flip
both the UserDefaults mirror and the scratch byte the Combine
subscript reads from.
The patches
Encoded with rasm2 -a arm -b 64, applied, re-signed, launched.
Measured: isActivated holds at 1 across 5 seconds of polling (vs
the original binary, where the state machine overwrites to 0 within ~2
seconds of launch).
Visual result: chip still says "trial 13 days left".
Because the trial chip doesn't read isActivated. It reads
daysRemaining.
Pass 4: the property that isn't @Published
Going back to the symbol table:
…but daysRemaining has no such line. It only has:
No backing-store field. That's the signature of a plain computed
property:
Computed properties are called via the getter every read. No
@Published indirection. Patching the getter reaches every read.
Swift ARM64 ABI for Int? return: x0 = value, x1 = discriminant tag
(0 = .some, 1 = .none). So:
12 bytes, and the remaining 796 bytes of daysRemaining.getter never
execute.
Relaunched. User's reaction: "YES IT WORKED! There's no more trial chip
and in the settings it says 'Your copy of boringBar is activated'!"
Both indicators aligned at once. The Session-3 @Published patches
weren't wasted — they drive the LicenseSection status text ("activated"
vs "not activated"). The daysRemaining stub hides the menu-bar chip.
Two independent UI indicators, two independent state properties, two
independent patches.
Pass 5: fresh-Mac cold-start
Packaged everything as boringBar-0.5.7-full.zip, installed to
/Applications, first launch worked on the dev Mac. One problem: we
still had a valid JWT cached from research on disk, and
boringBar.isActivated = 1 pre-seeded in defaults from Session 3. A
friend installing on a pristine Mac wouldn't have either.
Wiped state:
Relaunched. daysRemaining stubbed → chip still hidden. But
LicenseStateMonitor.init reads the Bool from UserDefaults on startup:
On a fresh Mac, [defaults boolForKey:@"boringBar.isActivated"] returns
NO because the key doesn't exist. x21 = 0. The Combine subscript
writes false into _isActivated. Settings says "Not activated" on
first launch, even with everything else in place.
One-instruction fix:
Re-test on fully-wiped state:
Chip hidden, Settings reads "activated". Fresh-Mac cold-start: done.
A bonus observation from that run: defaults also contained
boringBar.trialEndAt = 2026-04-27 18:23:50 +0000 — i.e. exactly
today + 14 days. Without a server, without a cached token, the state
machine synthesized a local 14-day trial-end date. That's dead-UI
in the offline build because daysRemaining.getter is stubbed, but
it's a useful find: the state machine has a graceful-degradation path
for zero-network bootstrap that the friend can either keep or strip in
his offline flavor.
The final patch set (14 instructions, 46 bytes, 4 MB binary)
Plus Info.plist edits:
CFBundleShortVersionString = 0.5.7-fullSUFeedURL = http://127.0.0.2:1111/appcast.xmlSUEnableAutomaticChecks = falseSUEnableSystemProfiling = falseembedded.provisionprofileremoved
And ad-hoc re-sign (codesign --force --sign -) because any byte change
invalidates the original Developer ID signature.
The equivalent source-level change, for the friend's actual shipping build
Plus removing SUFeedURL from Info.plist to quiet Sparkle on launch if
the offline flavor isn't going to ship auto-updates.
Roughly 20 lines across 3–4 files. Apple review doesn't even blink at
that, and the friend doesn't pay for our mistakes.
Things that didn't end up mattering
- Sparkle's
SUPublicEDKey(NacYvYl/xSRpUrX93ZkfNht+ol2BJyNtvNwBHiDpuqY=):
Ed25519 appcast signing, irrelevant whenSUFeedURLpoints at an
unreachable host and auto-checks are disabled. verifyTokenSignature: ES256 against the embedded PEM. Rock-solid;
we don't need to defeat it because we never need a "valid" token
anymore.- Ganesha proof-of-work (
WorkRequest,WorkSolution,Solver):
hashcash-style backend rate limiter. Irrelevant offline because
LicenseClient.postis never called. DeviceIdentity's Secure Enclave P-256 keypair: used only in the
business activation path (voucher → issue challenge → sign). Never
touched in trial mode or in our offline build.NTPClient's three-layer rollback defense (UDP NTP to
time.apple.com/pool.ntp.org, HTTPSDate:header fallback from
apple/google/amazon,cachedContentTimestamphigh-water mark): a real
defense against clock-rollback cracking, bypassed here by not needing
trusted time at all.
The anti-tamper design is solid. It's just designed against an
adversary who wants to run someone else's boringBar, not against an
adversary with permission and source access.
Takeaways
- Full Swift symbols are a massive leg up.
nm+swift-demangle
made the type inventory trivial. Strip your binaries if you want to
slow this kind of analysis down. @Publishedhas three read paths. SwiftUI uses the one that
bypasses the getter. "Patch the getter" is rarely the right move for
Combine-observed state.- Computed properties remain patchable. The absence of a
direct field offsetline in the symbol table is a fast tell that
a property is computed, not stored. - Trust your measurements.
sampleis a faster ground truth than
reading 808 bytes of async state-machine assembly, andlsof -i
settles "is it actually offline?" in one command. - Server-side statefulness. The backend pins the trial to
(uuid, serial), so deleting the token / reinstalling doesn't reset
the clock. Cleanup routines that "give users a fresh trial" by nuking
~/Librarydon't. - The hard part wasn't the patch, it was not knowing where the UI
reads from. Four wrong bets beforedaysRemaining.getterturned out
to be the visible-chip gate.
Grand total: one afternoon, 46 bytes of machine code, 14 instructions,
4 MB of binary — from "how does the licensing work?" to "fresh-Mac
cold-start with zero network and no trial indicators anywhere in the
UI."