Android phone ↔ Mac · local network only
Your phone and your Mac. Nothing in between.
Linked Mac connects an Android phone to a Mac directly over your Wi-Fi. No cloud, no account. One mutually authenticated TLS channel carries notifications, messages, calls, photos, files, clipboard and camera, and nothing is trusted until you have checked a six-digit code with your own eyes.
Live: the pairing check, computed in your browser
Confirm the pairing code
Check that your phone shows the same six digits. If they differ, even by one digit, another device is between you.
SHA-256(ctx ‖ mac, phone ‖ nonce) → first 4 bytes mod 10⁶
Do the codes match?
Compare with the code on your Mac.
They matchThey don’tSHA-256(ctx ‖ mac, phone ‖ nonce) → first 4 bytes mod 10⁶
This runs the project’s real derivation: SHA-256("linked-mac-SAS-v1" ‖ lo ‖ hi ‖ nonce), with the first 4 bytes read as a big-endian integer, mod 1,000,000. The keys and nonce are random demo bytes made in this page. A relay has to swap in its own key, and that changes the code the Mac shows.
Watch
A pairing in twenty seconds
How it works
From a QR code to an open channel
Pairing takes one scan and one comparison. Both machines check each other before either shows a code, and the link opens only after both people confirm.
- 1Shows a QR codeIt carries the Mac’s public key, address, port and a fresh nonce:──▶
{ v, pk, host, port, nonce }Scans and validates itThe scanned key is the only Mac the phone will accept. - 2TLS 1.3, mutual, with throwaway self-signed certificatesThis keeps the traffic private and tamper-proof. It does not yet prove who is on the other end.
- 3Says hello, looks up the pinAn unknown key is rejected. There is no trust on first use.◀─▶Says hello, checks the pinThe Mac’s key must equal the one it scanned.
- 4Answers a challengeSigns a fresh nonce bound to the hash of its own TLS certificate.◀─▶Answers a challengeSame proof, checked against the certificate its session actually received.
- 5Both screens show the same six digitsEach device derives the code from both keys and the nonce. You compare them and confirm on each side.
- 6Sends──▶
link.readyOnly once the person at the Mac has agreed too.Shows “Linked”The authenticated channel opens and features start.
Why the code appears after the handshake
The Mac learns the phone’s key only at hello, and the code needs both keys. Connecting first is what puts the same digits on both screens at the same moment. The final link.ready exists because each person confirms on their own device, and neither can see the other’s answer. The phone waits for it so it never reports a link the Mac is about to drop.
Trust model
No certificate authority, so three checks of its own
On a home network no authority can vouch for 192.168.1.20. There is no name to validate and nothing to revoke. So TLS here provides confidentiality and integrity, and Linked Mac proves identity itself. Every check runs before a single app message is delivered, and any failure closes the connection.
Identity pinning
Each device has a long-lived Ed25519 key. The peer’s key is compared in constant time against the one confirmed at pairing. Unknown keys are refused.
Channel-bound challenge
Each side signs a fresh nonce together with the SHA-256 of its own certificate. A relay must present a different certificate, so a signature replayed from the real device fails.
The six-digit code
Derived from both keys and the pairing nonce. Anyone in the middle has to substitute a key, which changes the code, which you see.
Certificate verification is switched off on purpose, and the checks above are the only reason that is safe. Never copy that setting into code that talks to a normal internet server: there it removes the only protection you have.
Wire format · both apps must agree byte for byte
| SAS input | "linked-mac-SAS-v1" ‖ lo ‖ hi ‖ nonce, keys sorted ascending as unsigned bytes |
|---|---|
| SAS output | First 4 digest bytes as a big-endian uint32, mod 1_000_000, zero-padded to 6 |
| Auth transcript | "linked-mac-auth-v1" ‖ len32(challenge) ‖ challenge ‖ len32(binding) ‖ binding |
| Channel binding | SHA-256 of the peer’s certificate DER |
| Pairing offer | JSON { v, pk, host, port, nonce }, base64 with padding |
| Frame | [4-byte big-endian length][JSON], max 1 MiB |
| Pairing verdict | A data frame on channel link.ready |
Features
Seven features, one link
Every feature is a module that rides the same authenticated channel. There is no second connection. Nothing runs until you switch it on, and each one can be turned off on its own.
- NotificationsMirror phone notifications on the Mac; reply, dismiss or tap an action.on by default
- MessagesRead SMS threads and reply from the Mac.on by default
- Call controlSee call state; dial and answer from the Mac. No audio.off by default
- PhotosBrowse the phone’s gallery; new shots sync over.off by default
- FilesBrowse the folders you grant access to.off by default
- ClipboardSend a clip either way from the menu-bar panel, ⇧⌘V.off by default
- CameraUse the phone as a webcam preview while the app is open.off by default
Switched on is not the same as allowed
The app shows its own toggle and Android’s permission side by side. Turning a feature off stops it but keeps the permission, so turning it back on is instant.
Your clipboard moves only when you say so
Clipboards hold passwords and one-time codes, so nothing is mirrored in the background. Copy on the phone and it asks “Send to your Mac?” without reading what you copied.
Your SMS app stays your SMS app
Messages reads and sends alongside your default messaging app instead of replacing it, so your texts are always stored by the app you already use.
Stays connected in your pocket
A foreground service keeps the link alive through Android’s battery saving. Camera and clipboard reads pause when the app is in the background.
Where keys live
The private key stays in the device’s vault
macOS
Stored in the Keychain under com.linkedmac.identity.
Android 13 and later
An Ed25519 key generated inside the Android Keystore.
Android 8 to 12
A 32-byte seed sealed with AES-GCM under a hardware-backed Keystore key.
The private key never reaches the app’s interface code and never appears in a log. Only public keys and fingerprints are shown on screen.
Boundaries
What it deliberately doesn’t do
- No call audioCall control is state and buttons only.
- No hanging upAndroid lets only the default phone app end calls. The phone says so instead of showing a dead button.
- No system webcamThe camera shows inside Linked Mac; it does not appear in other Mac apps yet.
- No MMSMessages handles SMS only.
- No large file transfersFiles shows up to 512 KiB per file; larger ones are marked as truncated.
- No backdoorsNo ADB, no root, no accessibility-service control of your phone.
Get started
Build and pair
Install and test
pnpm install pnpm test
Start the Mac app
pnpm --filter @linked-mac/shared build pnpm --filter @linked-mac/mac start
Install on your phone
pnpm --filter @linked-mac/android start # Metro pnpm --filter @linked-mac/android android # build + install
Then open the app on your phone, scan the code on your Mac, and compare the six digits. Installers build without any account: package makes a universal DMG, and package:apk makes an APK for sideloading.