Device linking (desktop)
A computer that holds the account's identity key shows a code; another computer pastes it and
gets a device key of its own, certified by the identity key, and signs in with it. The contract,
with the code format, the channel cryptography and the known-answer vectors, is
../rim-server/docs/specs/device-linking-v1.md; this page says where the desktop app implements
it.
What lives where #
src-tauri/src/key_identity/device_link.rs: everything secret. The link code (format and the spec's reader rules), HKDF-SHA256 per direction, AES-256-GCM seal and open with the AAD rule, the fingerprint, the offer and answer plaintexts (serdewithdeny_unknown_fields; serde's derive refuses a field named twice), and theKeyIdentityStatemethods behind thekey_identity_link_*commands. Its tests check appendix A byte for byte; the two-device flow tests are inkey_identity/tests.rs.src/features/key-identity/api/deviceLink.ts: the GraphQL documents, the command wrappers, and the two flows.linkNewDeviceis the new device from the code to the session.startLinkingDevice,waitForOffer,confirmLinkandcancelLinkare the old device, driven by the settings panel;resumePendingDeviceLinksfinishes links an earlier run left open. Every way a link can end early has its message inDEVICE_LINK_MESSAGES.components/LinkDevicePanel.tsx: Settings > Security > "Link a device", shown only when this computer holds the head's identity key. QR code (qrcode.react, byte mode, level M, no boost) and the text code with a copy button and the time left, then the confirmation card: the name (editable before signing), the platform (a web device gets the weaker-keys note), the fingerprintXXXX-XXXX, Confirm or Decline.components/LinkThisDevice.tsx: "Link this device" on the sign-in screen: the code, the device name, then the fingerprint with "Confirm this code on your other device" while it waits.runtime/useResumeDeviceLinks.ts: mounted in the app shell; once per signed-in account and run, while online, callsresumePendingDeviceLinks.
The webview sees the code text (to show and copy), sealed bytes, fingerprints, names and link states. The secret, the new device's key and the identity key never leave the Tauri side.
The old device #
The secret is generated by key_identity_link_begin and kept in memory only. When the server
says OFFERED, key_identity_link_review_offer opens the offer and checks it against
myKeyIdentity (its key log replayed here, its devices revoked ones included): a key or device id
already used, one of the account's own keys, a key that is malformed, weak or of an algorithm
this app cannot verify, or a bad name refuses the offer, and the link is cancelled. An offer that
does not open means someone else used the code.
On Confirm, key_identity_link_confirm signs the certificate at the head's keySeq, seals the
answer, and writes the pending link record before the webview sends addDeviceKey:
| Secret vault record | Holds |
|---|---|
device-link-v1:<link id> |
link id, secret, endpoint, principal id, device id and key, certificate, sealed answer (resent byte for byte) |
device-links-v1 |
the list of link ids that have a record |
The record is removed when the link ends CONSUMED, or once a link that ended CANCELLED or
EXPIRED has been settled: a certified device whose lastUsedAt is still empty is revoked with
the identity key. If revoking fails, the record stays for the next start-up. On start-up a record
whose link is still OFFERED is answered again with the same sealed bytes (the device added first if the list lacks it),
ANSWERED waits for the new device, and anything else is settled as above. Records of another
account on the same server wait for that account's sign-in.
Cancelling after Confirm aborts the flow, waits for any addDeviceKey in flight, then cancels
and settles, so an added device is never left unrevoked. Closing the panel after Confirm does
not stop the flow. A network failure past Confirm offers Retry, which sends the same certificate
and answer.
An addDeviceKey refused with CONFLICT while the device list already shows this device id with
this key is a retry after a lost answer and carries on; any other refusal cancels the link.
The new device #
key_identity_link_offer reads the pasted code, refuses a code for another server (compared as
the key store compares endpoints), and makes the device key and a UUID v7 device id in memory.
Nothing is written to the secret vault until key_identity_link_open_answer has found a certificate
for exactly this device id and key, and key_identity_link_accept has verified it against the
account's keyLog at its keySeq. Then the key, id and name are stored like a recovered device's
(no identity key, user id unknown), and the existing signInWithDeviceKey signs in and settles
them. completeDeviceLink is tried twice at most. FORBIDDEN at that sign-in removes the keys;
other failures keep them for the sign-in screen. A computer that already holds keys for the
account refuses to link over them.
Trying it with two app instances #
Both instances must use the same server (the code carries the endpoint and the new device refuses another), and each needs its own secret vault and credential store entry, so run them as two OS users or on two machines:
- On the first, sign in, set up key sign-in in Settings > Security, and choose "Link a device".
- On the second, open the sign-in screen, choose "Link this device with its code", paste the code, and keep the fingerprint in view.
- On the first, compare the fingerprint, adjust the name, and confirm. The second signs in; the first says the device is linked and lists it under Devices.
To try the restart path, quit the first app after confirming and before the second signs in; the next start finishes the link, or revokes the device if the link expired meanwhile.
Not in v1 #
QR scanning, the archive key in the answer, moving the identity key, short typed codes, and a
subscription instead of polling: see the spec's "Deferred" section. A linked device can take the
identity key over from its backup in the history archive instead: identity-key-backup.md.
Web client #
The web app is only ever the new device: a browser with WebCrypto Ed25519 pastes the code, keeps a
non-extractable key in IndexedDB and signs in with it, through the same linkNewDevice and
signInWithDeviceKey behind the KeyBackend interface. See web-linked-device.md.
Limits #
- Polling gives up 25 minutes after it started: a link lives 10 minutes and an answer 10 more.
- The desktop does not scan QR codes; the new device takes the pasted code.