DocsUsing RIMDevice linking (desktop)

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 (serde with deny_unknown_fields; serde's derive refuses a field named twice), and the KeyIdentityState methods behind the key_identity_link_* commands. Its tests check appendix A byte for byte; the two-device flow tests are in key_identity/tests.rs.
  • src/features/key-identity/api/deviceLink.ts: the GraphQL documents, the command wrappers, and the two flows. linkNewDevice is the new device from the code to the session. startLinkingDevice, waitForOffer, confirmLink and cancelLink are the old device, driven by the settings panel; resumePendingDeviceLinks finishes links an earlier run left open. Every way a link can end early has its message in DEVICE_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 fingerprint XXXX-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, calls resumePendingDeviceLinks.

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:

  1. On the first, sign in, set up key sign-in in Settings > Security, and choose "Link a device".
  2. On the second, open the sign-in screen, choose "Link this device with its code", paste the code, and keep the fingerprint in view.
  3. 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.