DEV Community

Cover image for I built a remote desktop for my Macs with no account, no cloud and no open ports
Abdullah Al Fahad
Abdullah Al Fahad

Posted on

I built a remote desktop for my Macs with no account, no cloud and no open ports

I wanted to reach my own Macs from my phone and my laptop. Every tool I tried wanted an account,
sent my sessions through someone else's servers, or both. For machines that hold everything I
work on, that felt like the wrong trade.

So I built OwnDesk: a Mac hosts, and another Mac, an Android phone or an iPhone controls it.
There is no account, no cloud service and no port open to the Internet.

A phone controlling a Mac: tapping into a note, typing, zooming in, trackpad mode

It is open source under the MIT license, and v0.4.0 has a Mac download and an Android APK:
github.com/im-fahad/OwnDesk

This post is about the design decisions, because they are the interesting part.

No server, so who connects the two devices?

On the same network, nobody needs to. The Mac advertises itself over Bonjour, and the controller
connects straight to a WebSocket the Mac serves.

Away from home, I use Tailscale. The Mac has a stable tailnet address,
Tailscale connects the two devices directly when it can, and relays when it cannot. OwnDesk never
runs a server of its own.

One detail made this work in practice. A Mac announces its Tailscale addresses in its Bonjour
record, and every paired device that hears it at home keeps them. So a phone finds the Mac from a
cafe even if the two were paired while Tailscale was off. The controller probes every address it
knows at once and uses the first one that answers, so the same button works at home and away.

Trust comes from one moment: pairing

Pairing happens once, face to face, on the same network:

  1. The Mac shows a QR code holding its device id, its addresses and a one-time pairing code.
  2. The phone scans it and sends a pairing request with an HMAC proving it has the code, without sending the code itself.
  3. Both screens show a short fingerprint, like 25AA-F3B7-4F82. If they match, you click Approve on the Mac.

That comparison is the whole security of pairing. After it, each side stores the other's public key,
and that key is the only thing that can ever start a session.

The keys themselves never leave hardware. On a Mac and an iPhone they live in the Secure Enclave,
and on Android in the Keystore. Both support ECDSA P-256 and neither supports Ed25519, which
decided the curve. A device's id is simply the SHA-256 of its public key, so the id proves which key
it belongs to and nothing has to be registered anywhere.

Signing the session, not trusting the network

Every message that sets up a session is a signed envelope, checked in a fixed order: size, version,
recipient, known sender, signature, clock skew, replay, and finally the JSON Schema. Any failure is
final, and most failures get no reply at all.

The part I like most is how this protects the video itself. The stream is WebRTC, encrypted with
DTLS-SRTP. Normally the weak point is the SDP exchange: if someone in the middle swaps the DTLS
fingerprint in the SDP, they can sit in the middle of the encrypted stream. In OwnDesk the SDP
travels inside a signed envelope, so the DTLS fingerprint is signed too. Swap it, and the signature
fails. Neither the network nor a relay can impersonate a device or read the session.

One protocol, three languages

The Mac apps are Swift, the Android app is Kotlin, and there is a TypeScript reference
implementation. Three implementations of a security protocol is three chances to disagree.

So the protocol is defined once, as JSON Schema. Codegen produces the TypeScript types and the
Swift key table from it, and a set of shared test vectors pins down the exact bytes an envelope
signs, the pairing proof and sixteen receiver cases covering replay, tampering, clock skew and
unknown senders. All three implementations run the same vectors. If the Android app disagrees with
a vector, it disagrees with both Macs, and the test says so.

The iPhone app has no protocol code at all: it links the Mac controller's own core, so pairing,
signing and reconnection are the same code a MacBook runs.

A terminal, without a "run command" message

I also wanted a shell on the Mac, the way Termius gives you one. The obvious way would be a message
type that runs commands, and that is exactly what I refused to add. The protocol has no message that
can run a command or touch a file. There is a list in the spec of messages that must never exist.

Instead, the terminal is the Mac's own SSH server with OwnDesk as the client. Each device has a
second hardware-backed key just for SSH. The only thing OwnDesk carries for the terminal is a
one-time request: "please let this key in". The Mac shows who is asking, someone clicks Allow, and
the Mac writes the key line itself, tagged with the device, then answers with its host keys so the
device can pin them. Unpairing removes the line again.

The phone asks the Mac for terminal access, then runs commands and selects, copies and pastes a line

Clipboard sync, opt-in on both sides

Clipboard sync is useful and dangerous: whatever anyone copies on the Mac, a password included,
could reach a device. So it is off until the person at the device switches it on, and a Mac shares
its own clipboard only with the devices it is told to, a per-device switch that starts off. What a
device copies reaches the Mac without that switch, since it could type the same text anyway. And
clipboard text is never logged.

Things that cost me time

  • macOS ties permissions to the code signature. I sign without an Apple certificate, and an ad-hoc signature changes on every build, so Screen Recording and Accessibility had to be granted again after every rebuild. The install script now clears the stale entries so the new build can ask.
  • Full-size desktops fell back to VP8. At 1920x1200 the H.264 level offered by default is too low for the frame size, so the controller's offer now puts H.264 first and states level 5.2.
  • Phones only read the clipboard when they are in front. On Android and iOS, clipboard sync sends what you copied when you come back to the app, not the moment you copy it.
  • Testing on real devices is where the bugs were. Unit tests passed while a scripted drag on a real phone was landing one row too low. Every feature now has an end-to-end script that drives the real app against a real host.

Where it falls short

It is early, and I would rather say so:

  • Only Macs can be controlled. Phones only control.
  • No file transfer or audio yet.
  • The Mac app is signed without an Apple certificate, so you confirm it once after downloading.
  • The iPhone app must be built from source with your own Apple ID.

Try it

I would especially like feedback on the security design. If you find a hole in it, please report it
privately through GitHub's security advisories rather than in public.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.