Skip to content
C.W.K.
Lesson 04 of 05 · published

Trust on First Use

~16 min · pairing, security, tofu, tokens, ios

Level 0Idle Phone
0 XP0/45 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

The loopback is shared

HomeLink tells my body where a call came from. It cannot tell who sent it. Any app on the phone can open a socket to its own loopback port 4610, and any device on the home network passes the Wi-Fi rule. Behind that door are a camera, a speaker and a GPS fix, so “it came from somewhere nearby” is nowhere near enough. The second check is a shared secret, set the first time Oriel meets the body.

A fresh install answers exactly two routes without it. GET /hello says who answers here: app, version, build, the wire version, and paired. POST /pair takes a token, once. Every other route, from /status to /photo, needs Authorization: Bearer <token>, and the router checks it before any route runs.

Oriel's half: pair on first contact

Oriel probes each body every 10 seconds, and every probe begins with ensure_paired (first code block). It reads /hello, refuses a body that speaks another wire version, and then has three cases. Unpaired: mint a token with secrets.token_hex(32), 32 random bytes written as 64 hex characters, save it to <data>/cordelia/<key>/token with mode 0600 (opened with O_EXCL, so two threads can never both create it), then POST /pair. Paired, and Oriel holds a token: carry on to /status. Paired, but Oriel holds no token: someone else got there first, and Oriel stops with a sentence that says so and names the fix, a reinstall. A later 401 on /status means the same thing and is said the same way.

The body's half: first pair wins

The second block is Pairing in CordeliaKit. pair refuses a second token outright (.alreadyPaired, which the router answers with 409, “this body is already paired; reinstall Cordelia to pair it again”) and refuses anything that is not exactly 64 hex characters (400). Only then does the token reach the store, and only after the store has saved it does the body count as paired. A token that failed to save never half-pairs a body.

admits compares the presented token with the stored one in constant time. The loop XORs every byte pair into one accumulator and looks at the result once, so a caller guessing byte by byte learns nothing from how long a wrong answer took. The length check before it leaks only the length, and every real token has the same one.

RequestUnpaired bodyPaired body
GET /hello200, paired: false200, paired: true
POST /pair200, or 400 for a malformed token409, reinstall to pair again
any other route401, Oriel pairs it first401 without the right token

Where the token lives, and where it must never go

The app keeps the token in a file in its own Application Support folder (third block), written atomically with Data Protection set to complete until first user authentication. After a restart the file opens once the device has been unlocked one time, and stays readable from then on, so a body that rebooted on its charger answers again without anyone opening the app. Deleting the app deletes the folder, which is why re-pairing is a reinstall and why there is no unpair route for a stranger to call.

The last function in that block came from a real mix-up. On 2026-11-03 Pippa's iPhone 2 was set up by Quick Start from the first phone, and it came up answering as paired, with the first phone's token, which Oriel held for a different body. Neither side broke its own rules. The token had simply ridden a device move. Now keepOnThisDevice sets isExcludedFromBackup on every save and on every read of an older file, and a test (theTokenNeverRidesABackupOrAMove) checks the flag on a real file. The code comment says the token never rides one, and Apple's documentation is more careful: the flag is guidance to the system about what it may leave out, not a guarantee that a file never shows up in a backup or on a restored device, and some file operations reset it, one more reason it is set again on every read. So a restored or migrated phone should start unpaired, and Oriel pairs it as a body of its own. If another body's token rides along anyway, the failure is the loud one in the last section: Oriel sees paired: true on a body it holds no token for, says so, and stops with the reinstall fix.

The token appears nowhere else. Architecture invariant 4 says a secret never becomes version metadata, UI, logs or committed content. Oriel's errors name the body, never the token, and the face shows that it is paired, never with what.

The first-use window

Trust on first use has one honest weakness: whoever pairs first wins. Between an install and Oriel's first probe, any caller that passes HomeLink could pair the body. Two things keep that window short and loud. Oriel probes every 10 seconds, so it is normally the first caller. And a body taken by someone else does not fail quietly: the next probe sees paired: true with no token of Oriel's own, and Oriel says so in words and stops, with the same fix, a reinstall. You cannot make the window disappear without a second channel (a code shown on the screen, a cable-only pairing step). What you can always do is make losing the race impossible to miss.

Code

cwkOriel, backend/cordelia.py: BodyLink.ensure_paired, pair on first contact or stop (verbatim, lines 280-293)·python
    def ensure_paired(self) -> dict[str, Any]:
        """Pair on first contact; a body paired to someone else stops here."""
        hello = self.hello()
        if int(hello.get("wire") or 0) != WIRE:
            raise BodyError(f"{self.body.name} speaks wire {hello.get('wire')}, Oriel speaks {WIRE}", 409)
        if hello.get("paired") and not self._token():
            raise BodyError(f"{self.body.name} is paired with something else (Oriel holds no token for it); "
                            "reinstall Cordelia there to pair it again", 409)
        if not hello.get("paired"):
            token = self._token(create=True)
            status, _, raw = self.request("POST", "/pair", payload={"token": token}, timeout=TIMEOUTS["pair"],
                                          what="pairing", auth=False)
            self._json(status, raw, "pairing")
        return hello
cwkCordelia, CordeliaKit/Sources/CordeliaKit/Pairing.swift: the first pair wins, and the comparison runs in constant time (verbatim, lines 88-113)·swift
  public func pair(_ token: String) -> PairResult {
    lock.withLock {
      guard cached == nil else { return .alreadyPaired }
      guard token.count == Self.tokenLength, token.allSatisfy({ $0.isHexDigit }) else { return .badToken }
      do {
        try store.save(token)
      } catch {
        return .badToken
      }
      cached = token
      return .paired
    }
  }

  /// Whether `Authorization: Bearer <token>` names the paired token.
  public func admits(_ authorization: String?) -> Bool {
    guard let expected = lock.withLock({ cached }) else { return false }
    guard let authorization, authorization.hasPrefix("Bearer ") else { return false }
    let given = Array(authorization.dropFirst("Bearer ".count).utf8)
    let want = Array(expected.utf8)
    guard given.count == want.count else { return false }
    // Constant time, so a wrong guess learns nothing from the clock.
    var diff: UInt8 = 0
    for index in 0..<want.count { diff |= given[index] ^ want[index] }
    return diff == 0
  }
cwkCordelia, CordeliaKit/Sources/CordeliaKit/Pairing.swift: the token file, protected and marked to stay out of backups (verbatim, lines 25-46)·swift
  public func save(_ token: String) throws {
    try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
    #if os(iOS)
      try Data(token.utf8).write(to: url, options: [.atomic, .completeFileProtectionUntilFirstUserAuthentication])
    #else
      try Data(token.utf8).write(to: url, options: [.atomic])
    #endif
    try Self.keepOnThisDevice(url)
  }

  /// The token pairs THIS device with Oriel: it never rides a backup or a
  /// Quick Start move to another phone, which would then answer as paired
  /// with a token Oriel holds for a different body (Pippa's iPhone 2 came up
  /// that way, 2026-11-03). Called on every save and on every read of an
  /// older file.
  public static func keepOnThisDevice(_ url: URL) throws {
    guard FileManager.default.fileExists(atPath: url.path) else { return }
    var values = URLResourceValues()
    values.isExcludedFromBackup = true
    var target = url
    try target.setResourceValues(values)
  }

External links

Exercise

Give a small server of your own a trust-on-first-use pairing. Leave exactly two routes open (an identity route that reports paired or not, and a pair route), require a bearer token on every other route, refuse a second pair with a 409 that says how to pair again, compare tokens in constant time, and store the token in a file excluded from backups. Write tests for: first pair wins, a malformed token is refused, nothing is admitted before pairing, and the stored file carries the backup exclusion.
Hint
Mint the token on the client side with a cryptographic generator (Python's secrets.token_hex(32) gives 256 bits) and create the client's copy with exclusive-create and 0600 permissions. On the server, save first and mark the device paired only after the save succeeds.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.