Skip to content

harbor: pair a phone or browser with berth pair - #145

Merged
T-Crypt merged 2 commits into
mainfrom
T-Crypt/harbor-pairing
Oct 22, 2026
Merged

T-Crypt merged 2 commits into
mainfrom
T-Crypt/harbor-pairing

Conversation

@T-Crypt

@T-Crypt T-Crypt commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Step 4 of the switchboard plan: device pairing, modelled on the operator's oh-my-pocket sign-in and cut down to one owner. Reading the harbor still needs no login. This PR adds no actions; it lets a browser prove it was paired, and step 5's switchboard builds on that.

Flow

  1. berth pair --url https://box.tailnet.ts.net prints a link (/pair?code=XXXXXXXX) and the same link as a QR code.
  2. GET /pair?code= shows a page with a Pair this device button and a name field. The GET uses nothing up, so a chat app's link preview cannot spend the code.
  3. The button's POST must be same-origin (Sec-Fetch-Site). berthd redeems the code, and the browser gets berth_device: HttpOnly, SameSite=Lax, one year, Secure over https (direct, or X-Forwarded-Proto: https from tailscale serve).
  4. GET /v1/me answers {"paired": bool}, so the page knows whether to show actions. A bearer token works too, for scripts.
  5. berth pair --list, --revoke ID and --revoke-all manage devices.

Design

  • berthd is the only writer of harbor-devices.json, through new socket ops (pair_code, pair_redeem, pair_check, pair_list, pair_revoke). If the CLI and the harbor both wrote the file, it would need a file lock on two OSes; one owner needs none. Access to the socket already means this machine's user, so the CLI needs no extra auth.
  • Secrets:
    • The file keeps only SHA-256 hashes, written atomically with mode 600, and tokens are compared in constant time.
    • Codes are 8 characters from a 30-letter alphabet with no lookalikes, picked by rejection sampling so each is equally likely. They last 15 minutes, and at most 5 are live.
    • Ten wrong codes in a minute stop every redeem for that minute.
    • Codes and tokens are never logged.
  • One new dependency: rsc.io/qr v0.2.0 (BSD, pure Go). It draws the terminal QR with explicit black and white ANSI colors, so it scans on dark and light themes, plus an SVG for the harbor's coming "Pair a phone" view.
  • CLI shape: berth pair is top-level, not berth harbor pair. A command with subcommands can't also run bare, and the installed services run berth harbor --allow-host ....
  • Daemon flag: berth daemon --devices FILE (default in the state dir; "" turns pairing off).

Proof

  • internal/pair (race):
    • a code pairs once and the token checks; nothing secret is in the file; it survives a restart
    • a code expires; the redeem limit applies
    • revoking one device or all of them
    • both QR drawings carry exactly the code's modules
  • TestPairCLI, over a real berthd socket: the link format, a code berthd accepts, list, revoke, the human view with the QR, and a non-http --url refused.
  • TestPairFlow, a harbor in front of a real berthd:
    • the GET does not spend the code, and the page's CSP allows only its own form
    • a cross-site POST gets 403; a same-origin POST gets 303 with the cookie flags
    • /v1/me tells a paired, unpaired and forged cookie apart
    • a spent code gets 403, revoking unpairs, and any other POST stays 405
  • Real binaries: a private berthd and harbor on a temp socket, port and state, driven with curl. I checked the link GET, the redeem (Set-Cookie as above), reusing the code (403), --list, a file without the code or token at mode 600, paired and unpaired, --revoke-all, and that neither log contains a code or token. The live berthd was not touched.
  • build, gofmt, vet, go test ./..., Windows build and vet, golangci-lint (0 issues).

berth pair prints a one-time link and its QR code. The link opens a page
with a Pair this device button, so a chat app's link preview does not
spend the code, and the button's same-origin POST trades it for a device
cookie (HttpOnly, SameSite=Lax, a year, Secure over https). --list,
--revoke and --revoke-all manage devices.

berthd owns the pairing file and is its only writer; the CLI and the
harbor reach it through new socket ops, so no file lock is needed. The
file keeps only hashes, mode 600. Ten wrong codes in a minute stop every
redeem for that minute. QR codes come from rsc.io/qr, drawn in the
terminal with explicit black and white so they scan on any theme.

Reading the harbor still needs no login. Pairing is the groundwork for
switching and stopping engines from the harbor.
@T-Crypt
T-Crypt merged commit c7a0db9 into main Oct 22, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant