boringBar activated in an hour and 46 bytes

activated

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 arm64 to drop the x86_64 slice
  • otool -L, codesign -d, plutil -p for surface reconnaissance
  • nm -U | xcrun swift-demangle for the full symbol table (12,191 symbols,
    92 app-level types)
  • radare2 with aaa + pdc for pseudo-decompilation and rasm2 for
    instruction encoding
  • Python + struct for surgical byte patches
  • sample, lsof -p -i, ps, defaults for live measurements
  • codesign --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:

1
2
3
4
5
6
LicenseClient              GraphQL + JWT verification (namespace enum)
LicenseStateMachine        async reducer over verification outcomes
LicenseStateMonitor        ObservableObject exposing state to SwiftUI
LicenseLaunchGate          launch-time barrier
NTPClient                  network time (time.apple.com, pool.ntp.org)
DeviceIdentity             Secure Enclave P-256 keypair

The embedded GraphQL mutations were sitting in __TEXT.__cstring:

mutation {
  requestBusinessActivation(input: { email: "...", ganesha: { nonce: "..."
  } }) { … }
}
mutation {
  activateBusiness(input: { voucher: "...", ... }) {
    licenseState activatedAt token
  }
}
mutation {
  confirmBusinessActivation(input: { email: "...", code: "..." }) {
    voucher expiresAt
  }
}

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?"

1
2
3
4
defaults write app.boringbar.stable "boringBar.trialEndAt" -date "2099-12-31T23:59:59Z"
open boringBar.app
sleep 1 && defaults read app.boringbar.stable
# → "boringBar.trialEndAt" = "2026-04-27 18:23:50 +0000";

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:

~/Library/Application Support/boringBar/token   (262 bytes, a JWT)

Decoded:

{"typ":"JWT","alg":"ES256"}
{"uuid":"8BD3C60C-…","serial":"JQ47C24TDK","exp":1777314230,"iat":1776104634}

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:

  1. Discarded it on signature verification fail.
  2. Silently re-fetched a new token from https://boringbar.app.
  3. 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 expDouble 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:

1
2
3
iat before: 1776104832
iat after:  1776104832   (unchanged)
file mtime: unchanged

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):

1
2
3
4
5
6
7
8
patches = [
    (0x0010cd10, b"https://www.apple.com",  b"http://127.0.0.2:1111"),   # 21
    (0x0010cd30, b"https://www.google.com", b"http://127.0.0.2:11111"),  # 22
    (0x0010cd50, b"https://www.amazon.com", b"http://127.0.0.2:11112"),  # 22
    (0x0010dcf0, b"https://boringbar.app",  b"http://127.0.0.2:1111"),   # 21
    (0x001502c8, b"time.apple.com",         b"127.0.0.2.null"),          # 14
    (0x00150328, b"pool.ntp.org",           b"127.0.0.2.nn"),            # 12
]

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:

1
2
3
4
5
6
1622/1622 main-thread samples:
  -[NSApplication run]
   nextEventMatchingMask:
   CFRunLoopRunSpecific
   __CFRunLoopServiceMachPort
   mach_msg2_trap

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:

  1. Plain method call (monitor.isActivated) → hits the getter.
  2. SwiftUI @ObservedObject / key-path reads → bypass the getter and
    read the _isActivated Published<Bool> backing storage directly
    via the Combine.Published key-path subscript.
  3. 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:

; (1) UserDefaults mirror
0x1000842f4  mov  x0, x19                ; NSUserDefaults
0x1000842f8  mov  w2, 0                  ; BOOL NO
0x100084300  bl   _objc_msgSend          ; [defaults setBool:NO forKey:@"boringBar.isActivated"]

; (2) In-memory @Published backing-store write via keypath subscript
0x100084284  mov  x24, x22               ; x24 = state machine ctx
0x100084288  strb wzr, [x24, 0xf1]!      ; scratch byte at [ctx+0xf1] = 0
0x100084294  mov  x0, x24                ; x0 = &scratch
0x1000842a0  mov  x3, x23                ; x3 = storage keypath
0x1000842a4  bl   Combine.Published._enclosingInstance  ; subscript setter

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

1
2
3
4
5
6
7
# one-bit flip: UserDefaults mirror writes YES instead of NO
0x000842f8  mov w2, 0                  → mov w2, 1

# three-instruction replacement: scratch byte holds 1, pointer restored
0x00084284  mov  x24, x22              → movz w9, #1
0x00084288  strb wzr, [x24, 0xf1]!     → strb w9, [x22, 0xf1]
0x00084294  mov  x0, x24               → add  x0, x22, 0xf1

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:

0000000100151a80  d  direct field offset for _trialEndAt  : Published<Date?>
0000000100151a88  d  direct field offset for _isActivated : Published<Bool>

…but daysRemaining has no such line. It only has:

000000010007d104  t  LicenseStateMonitor.daysRemaining.getter : Int?

No backing-store field. That's the signature of a plain computed
property
:

1
2
3
4
5
6
// inferred
var daysRemaining: Int? {
    guard let end = trialEndAt else { return nil }
    let comps = Calendar.current.dateComponents([.day], from: Date(), to: end)
    return comps.day
}

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:

1
2
3
0x10007d104  stp x28, x27, [sp, -0x60]!  → movz x0, #0    ; 00 00 80 d2
0x10007d108  stp x26, x25, [sp+0x10]     → movz x1, #1    ; 21 00 80 d2
0x10007d10c  stp x24, x23, [sp+0x20]     → ret            ; c0 03 5f d6

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:

1
2
3
4
5
pkill -9 -f boringBar
rm -rf ~/Library/Application\ Support/boringBar
rm -rf ~/Library/Caches/app.boringbar.stable
rm -rf ~/Library/HTTPStorages/app.boringbar.stable
defaults delete app.boringbar.stable

Relaunched. daysRemaining stubbed → chip still hidden. But
LicenseStateMonitor.init reads the Bool from UserDefaults on startup:

1
2
3
4
5
0x10007ce04  bl   _objc_msgSend     ; [defaults boolForKey:@"boringBar.isActivated"]
0x10007ce08  mov  x21, x0           ; x21 = result (NO on fresh Mac)

0x10007ce58  bl   Combine.Published._enclosingInstance
                                    ; writes x21 into _isActivated backing

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:

0x0007ce08  mov x21, x0    → movz w21, #1

Re-test on fully-wiped state:

defaults read app.boringbar.stable (2s after fresh launch)
→  "boringBar.isActivated" = 1;

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)

# URL/NTP kill switches (6 string patches, same-length)
0x0010cd10  "https://www.apple.com"      "http://127.0.0.2:1111"
0x0010cd30  "https://www.google.com"     "http://127.0.0.2:11111"
0x0010cd50  "https://www.amazon.com"     "http://127.0.0.2:11112"
0x0010dcf0  "https://boringbar.app"      "http://127.0.0.2:1111"
0x001502c8  "time.apple.com"             "127.0.0.2.null"
0x00150328  "pool.ntp.org"               "127.0.0.2.nn"

# init: force _isActivated=true regardless of defaults
0x0007ce08  mov x21, x0                  movz w21, #1

# trial branch: UserDefaults mirror
0x000842f8  mov w2, 0                    mov w2, 1

# trial branch: @Published backing store
0x00084284  mov  x24, x22                movz w9, #1
0x00084288  strb wzr, [x24, 0xf1]!       strb w9, [x22, 0xf1]
0x00084294  mov  x0, x24                 add  x0, x22, 0xf1

# daysRemaining: computed property  Optional<Int>.none
0x0007d104  stp x28, x27, [sp, -0x60]!   movz x0, #0
0x0007d108  stp x26, x25, [sp+0x10]      movz x1, #1
0x0007d10c  stp x24, x23, [sp+0x20]      ret

Plus Info.plist edits:

  • CFBundleShortVersionString = 0.5.7-full
  • SUFeedURL = http://127.0.0.2:1111/appcast.xml
  • SUEnableAutomaticChecks = false
  • SUEnableSystemProfiling = false
  • embedded.provisionprofile removed

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

// LicenseStateMonitor.swift — offline flavor
@Published var isActivated: Bool = true           // was: false + read from defaults
@Published var trialEndAt:  Date? = nil           // was: read from defaults
var daysRemaining: Int? { nil }                    // was: Calendar-based compute

// LicenseLaunchGate.swift — offline flavor
func start() async {
    monitor.apply(.valid(expiresAt: .distantFuture))
    // no LicenseClient.fetchToken, no NTPClient, nothing
}

// LicenseClient.swift — offline flavor
// delete the entire GraphQL round-trip; guard the whole file with #if OFFLINE.

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 when SUFeedURL points 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.post is 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, HTTPS Date: header fallback from
    apple/google/amazon, cachedContentTimestamp high-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.
  • @Published has 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 offset line in the symbol table is a fast tell that
    a property is computed, not stored.
  • Trust your measurements. sample is a faster ground truth than
    reading 808 bytes of async state-machine assembly, and lsof -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
    ~/Library don't.
  • The hard part wasn't the patch, it was not knowing where the UI
    reads from.
    Four wrong bets before daysRemaining.getter turned 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."

Edit

Pub: 13 Apr 2026 21:40 UTC

Edit: 13 Apr 2026 22:02 UTC

Views: 84