Linked-device implementation profile
Document: SPEC-011. Revision: 0.5 Date: 2026-10-25. Lifecycle: implementation profile under the explicit D-059 request to implement code-based linking and history sharing, within D-044–D-046. Supersedes SPEC-009's single-device authentication/recipient restriction for clients advertising linked-devices/1; preserves its existing cryptographic primitives, account UUIDs and legacy ciphertext. SPEC-003 owns trust, SPEC-006 history, SPEC-004 voice, SPEC-005 client workflow and SPEC-008 server authority.
Approved video-delivery successor — D-167
D-167/SPEC-024 requires confirmed managed-presentation closure before completing a conflicting call/device move. A native four-second fence cannot establish managed-provider closure. A failed provider closure leaves the move pending with a reason; it cannot authorize overlapping managed presentations. Native microphone movement retains its current contract. This is a managed integration requirement, not an enabled provider operation in the pre-SFU increment. See SPEC-024.
D-103's login-sessions/1 successor in SPEC-003/SPEC-009 replaces new local opaque login issuance with signed sessions and a separate password/device-proof early-renewal flow. It preserves approved-device rosters, content keys, link/recovery ceremonies and their deadlines. Early renewal keeps live voice; ordinary sign-in and explicit device handover retain their existing semantics.
Scope and authority
The human approved the preceding identity/discovery specification and explicitly requested adding KDE/Windows/web installations to the same account using approval from the signed-in web client. Existing sessions and keys must survive. Account passwords and permissions remain account-wide. Linking generates independent device keys; it must never clone the existing signing key or export the old device's private decryption key.
Technical completion choices below select exact formats/limits within that requested workflow; they are recorded as implementation choices, not separate human preferences. D-060 confirms that all-devices-lost recovery requires both the account password and a separate recovery key. Linking an available device remains a distinct workflow; neither a link code nor the password alone is a recovery key.
Roster and trust
Retain the original account device as its root trust anchor. Each signed roster change binds profile: sloppy-device-roster/1, canonical domain, account UUID, monotonically increasing revision, previous change digest (or the digest of the initial one-device roster), signing device UUID, complete active device list and an operation context. Changes use the existing Ed25519 canonical-JSON signature. An active device from the preceding roster signs each change. Clients validate the complete chain from their previously trusted root and remember the highest seen revision/digest; rollback or same-revision equivocation fails closed. A fresh peer retains the existing explicitly disclosed TOFU limitation.
Admission changes add only the proven request device. Revocation removes exactly the selected device, invalidates its sessions/voice/preparation and pending approvals, and cannot remove the last approved device. Retain signed evidence needed to validate descendants even when their approving device is revoked. The server cannot author a valid new-device certificate from directory data alone. A user with a compromised approved device can authorize another device; signing evidence is not a substitute for endpoint security.
Identity responses add the root anchor and signed roster. The current authenticated device remains explicit in self identity, and the active voice device is explicit in participants. Account role, membership and moderation are looked up on every request rather than copied from client state. The stored root is never overwritten by selecting a different authenticated device.
Linking state machine
- On an approved signed-in client, Link another device creates a random request UUID and 32-byte local secret. The server creates an expiring slot bound to that account, approving device and roster revision. The copyable
SVLINK1.code contains the slot, identity/base URL, username/account, root/approver public keys, pinned roster revision/digest, expiry and local secret. It contains no password or existing private device key. Show the destination/domain and warn that sharing this one-use code permits a device-link request. Clear it after completion/cancel/expiry. - The new client parses the code as data, verifies its canonical account/domain/service, and never navigates to an arbitrary pasted URL. It generates fresh keys for that slot and reuses them only for retries of the same slot; an earlier local account profile is preserved. It supplies the ordinary account password, a fresh challenge and proof of possession by its signing key. A libsodium secretbox encrypted under the code secret binds the slot/account/domain and exact new public keys. The server receives the ciphertext, not the secret, and can only stage a restricted request after password/proof verification. Pending requests have no account token, room history or media rights.
- The source polls its slot, decrypts the binding proof, compares every context/key field, and displays the requesting device for explicit approval. Wrong proofs/changed keys cannot be approved. The source signs the next roster change and an authenticated bootstrap transfer containing previously trusted peer fingerprints, bound to the new device and slot. The transfer uses existing X25519
crypto_box_easy; the signed change binds its digest. No role, password, private key or PTT preference is transferred. - Approval atomically updates the roster, consumes the slot and invalidates affected recipient revisions. The new device proves possession again using its restricted completion token, validates the pinned source/roster evidence and bootstrap transfer, and gets its own login session. A lost completion response is safely retryable during the slot lifetime; it does not add another device. Only that device's sessions are replaced on routine re-login; other devices remain signed in.
- The source transfers readable retained history as below. Report progress and unavailable messages honestly. Closing the source or losing connectivity must not claim a complete transfer. The destination can already receive new messages after approval. Cancelling/expiring before approval grants no device access; revocation cannot be undone by replaying a consumed slot.
Codes remain in memory and copied text only, never URL query parameters, logs or server storage. A manually shared code is not a six-digit guessable PIN. Server password/challenge rate limits remain, with the additional limits in Configuration. Return safe errors and no partial admission on failure.
QR linking successor (D-171)
QR linking supports either device displaying the QR. The human selects explicit approval on an existing device without password entry on the new device. This supersedes the password requirement only for the QR endpoints below; the legacy manual SVLINK1. request remains supported. Existing independent keys, signed roster, bootstrap, completion and history transfer stay authoritative. The five-option review records the earlier alternatives; its open-password wording is historical after D-171.
Approved device displays: render the existing SVLINK1. payload locally as a black/white QR. A new device validates it against its explicitly selected service, generates and persists fresh pending keys, then POSTs /device-link/qr-request. Sign the same binding fields as legacy request, replacing purpose with qr-link-request and omitting challenge/password. The server stages a restricted request with existing submission/account/peer limits and device-proof checks. It cannot verify the secretbox itself; the source must verify it before making explicit approval available. No password or token is issued at submission. Completion uses the existing signed roster/bootstrap and restricted completion-token contract.
New device displays: create fresh keys, UUID, secret and a five-minute absolute expiry. SVPAIR1. plus base64url canonical JSON encodes {profile:"qr-pair/1",id,domain,baseUrl,expires,device,label,secret,signature}. Signature signs {purpose:"qr-pair-create",id,domain,baseUrl,expires,device,label,secretHash:digest(secret)}. Register only this public signed body at POST /device-link/qr-create; never send the secret. Server validates service/key/signature, proposed expiry strictly after now and at most five minutes plus a fixed 1000 ms clock-skew allowance away, then durably creates a restricted rendezvous with deadline min(proposed expiry, server now + 300000 ms) and returns that authoritative deadline. The client re-signs its displayed QR with the returned deadline. The clock-skew allowance never extends server authority beyond five minutes. An identical create retry preserves the deadline and cancellation; changed contents conflict. Generate a new UUID/keys after expiry. Display only after successful registration.
The approved scanner validates the entire QR and its signature locally against the selected canonical service. POST /devices/link/qr-claim authenticates the approved device and atomically claims exactly that target rendezvous, creates an ordinary link slot with the same UUID/deadline, and stages the exact new keys/label. The source generates an ordinary LinkCode, binding, completion token and {nonce,ciphertext} secretbox under the scanned secret. Its plaintext is {profile:"qr-offer/1",code,token,target} with target equal to the new device UUID. Submit public new device/label, binding, token hash and encrypted offer; no scanned secret or LinkCode secret reaches server plaintext. Claim retries require identical content; a competing approver cannot replace the first. Reuse existing slot/device/roster limits. The source still requires the explicit Approve device & copy history action.
POST /device-link/qr-offer supplies {purpose:"qr-pair-poll",domain,id,device,signature} with signature over the fields except signature. Only the registered key may poll. Return pending until claimed, then the encrypted offer. The destination decrypts under its QR secret, validates the LinkCode/service/expiry, target and completion token, persists ordinary restricted completion state without either QR secret, and uses existing completion verification. Possession of the scanned secret authenticates the optical exchange; it does not promise proximity or defeat a compromised approved endpoint. Show the qualified account in the source approval view and the destination's waiting/completion view. Selecting the account on the source determines which account is linked.
POST /device-link/qr-cancel uses the poll proof with purpose qr-pair-cancel. Cancel before approval atomically marks the rendezvous cancelled and deletes its claimed unapproved slot; retain the tombstone until expiry so create retries cannot resurrect it. Source cancellation deletes the slot and cancels its rendezvous. Cancellation after approval returns an explicit already-approved error; removing that approved device requires normal revocation. Revoked source/target or changed roster cannot admit a stale request. New-device secrets live in memory only; closing/reloading before receipt of the encrypted offer requires a new pairing. Persist fresh keys before any request and restricted completion state before polling completion. Lost post-approval completion uses the existing pending-key reconciliation.
All payloads are bounded to 4096 characters and treated as data, never navigation. QR encode/decode happens locally. Clients allow the same 1000 ms skew when checking the upper bound of a scanned reverse QR; authoritative expiry remains the server deadline. SPEC-005 owns fixed scanner processing and native authorization bounds.
No QR, secret, password, session, private key or camera frame enters diagnostics, query strings or analytics. Unsupported QR capability is explicit; legacy manual linking remains available. The human reports a fresh Linux QR for a foreign home account being rejected by the iOS default service and requests minimal input after scanning on 2026-10-25. A new-device scan validates the payload before any dispatch. The iOS bundled client may select the QR home service automatically only after fresh SPEC-010 identity-origin HTTPS discovery verifies its domain and exact canonical base URL; a QR URL alone is never routing authority. Use a fresh unauthenticated API, preserve the bundled executable/native vault, and send no existing account credentials to the selected service. Submit the restricted QR request automatically with the default device label; explicit approval on the existing approved device remains mandatory. Expired/invalid codes, mismatched discovery, failed lookups and cancelled/stale scans make no linking request. Persisted restricted completion restores the same verified service after restart. Other clients that require navigation open only the verified endpoint without QR data in the URL and require rescan. QR linking advertises qr-device-link/1 through /info.
History and future text
Future E2EE text encrypts one box to every active approved device of each admitted account, including offline devices. Account membership and seven-day retention remain unchanged. Preserve original message IDs, author/device signatures, ciphertext and host positions. Recipients validate signed rosters before encrypting. An older client unable to represent the roster must receive an explicit upgrade-required error once multiple devices exist; never accept incomplete recipient sets.
For old ciphertext lacking a destination box, the source decrypts only history it is entitled to retrieve and can actually read. It sends a separate signed transfer envelope containing a new encrypted box for that destination, bound to history-share/1, domain/account, room, message UUID/host position, original envelope digest, source device and destination device. Inside the box repeat the binding and include text. The server checks current source/destination approval, account history eligibility, original retained message identity and bounds, but cannot decrypt the transferred text. It stores at most one identical share per message/destination; an altered retry conflicts. Shares expire with the original message, not seven days after transfer.
The destination verifies the original envelope signature, transfer signature, source roster evidence and all inner/outer bindings. A legacy plaintext export is attested by the approving device; label it as shared history rather than claiming the original sender signed the new plaintext. Missing keys, incomplete copies, expired messages and absence-period messages remain unavailable. Do not upload plaintext transcripts or static device private keys. Later approved devices can share already verified retained history under the same rules.
SPEC-015 adds the destination UUID to new message and history-share contexts. Its selected message-host extension preserves unbound local history, but prevents copying or displaying it as foreign-host history. History copy visits the home connection and saved visitor connections for the same qualified account. Each destination rechecks current home device approval, including freshly approved targets. A failed destination is named in the result and can be retried; it does not imply that its history was copied. Copy requests snapshot the original API base, token, room and request-proof provider; navigating to another server cannot redirect a transfer or substitute its account.
Voice and sessions
One active voice client per account remains fixed. Login or linking elsewhere does not move it. Move voice here explicitly stops source renewal, records a transfer fence and waits for the existing hard lease cutoff before admitting the destination. During the fence neither device receives renewed forwarding authority. Recheck device, membership, mute, room and generation at admission; concurrent/stale moves cannot overlap or extend the shared call deadline. Failure after revocation leaves voice stopped until explicit retry. PTT requires a fresh press. Logout/leave from another signed-in device cannot close a voice session it does not own.
Recovery with password and separate key (D-045/D-060)
An approved device generates a separate random 32-byte recovery seed locally. Use libsodium's existing seed-keypair APIs, with separately domain-labelled generic-hash derivations for the signing and encryption seeds. The SVREC1. recovery code contains that seed, a generation UUID, canonical account/domain/service, root anchor and the expected setup roster revision. Its derived public device uses the generation UUID. The user must acknowledge saving it before the current device signs a recovery-setup roster change containing the public recovery device. The code is shown locally; it is not uploaded or silently saved as an ordinary device preference. Never overwrite an active recovery key silently.
The recovery public key is a separate ciphertext recipient, not an active login or voice device. Future encrypted text includes it for each enabled account, with at most 60 boxes across ten accounts. Approved clients copy eligible retained history to this recipient with the same authenticated history-share format and original retention deadline. Coverage is bounded by existing ciphertext, readable history and quota; setup alone does not guarantee old-history coverage. Peer trust and root evidence are validated as for linking. Passwords and roles are not stored in recovery copies.
After all devices are lost, the new client supplies the password, a one-use login challenge, its fresh device proof and a recovery-key signature bound to the account/domain, challenge and exact new public keys. The server verifies both before returning a five-minute restricted recovery request and the current signed roster. The client verifies that chain against the code's root and setup revision, confirms that this recovery generation is still active, then signs a recover roster change with the recovery signing key. That change is allowed only for the active recovery authority, replaces all active devices with the proven new device and consumes that recovery authority. Commit atomically revokes every old session, pending link and live/prepared voice session, preserves account UUID/current roles/room intervals and issues the new device's session. A concurrent roster change invalidates the pending recovery. Retrying a completed recovery may replace only the new device's session and must not revive a revoked device. Ordinary password-only or administrator operations cannot recover encrypted content.
The restored client keeps the consumed recovery decryption key in its protected vault solely to read retained boxes/copies addressed to that generation. It never uses it as an active device key or transfers it during ordinary linking. Read access still requires the restored account session and current history eligibility. Recovery is explicitly marked unavailable until the user sets up and saves a fresh recovery key; recovery setup is not a password-reset flow. Already lost, expired, uncopied or inaccessible messages remain unavailable. An old recovery code cannot recover a second time.
Recovery attempts use the ordinary 5/account/minute and 20/peer/minute bounds, a 300,000 ms restricted request lifetime and one outstanding request per account. Recovery records and encrypted copies share the existing profile storage/retention bounds. The code is bounded to 4096 characters; malformed, foreign-service or stale-generation codes fail before any admission. No new configurable cryptographic algorithms or server content secrets are introduced.
Configuration
This alpha uses fixed profile limits, not new operator policy switches. They are selected technical bounds for D-059 and must be kept aligned with shared code. Existing secrets, password policy, forwarding deadlines and retention remain owned by SPEC-002/003/006.
| Property | Type, units and selected value | Scope, validation, persistence |
|---|---|---|
| Approved devices | Maximum 5 active devices/account | Fixed; additions above it fail without eviction. Includes the initial device. |
| Roster changes | Maximum 100 signed changes/account in this testing profile | Fixed bounded proof history; exceeding it fails explicitly, never truncates trust evidence. |
| Link expiry | 300,000 milliseconds | Fixed server deadline; a slot expires once; no renewal by polling. |
| Link slots | At most one unexpired unconsumed slot per approving device, at most five/account | Fixed; cancellation removes pending state; consumed slots retained only to their deadline for safe completion and do not block starting another link. |
| Reverse QR rendezvous | Maximum 1000 unexpired records/domain; five-minute absolute deadline | Fixed operational state, including cancelled tombstones; no anonymous account privileges. Creation 3/peer/minute and 30/domain/minute; poll/cancel 120/peer/minute and 3600/domain/minute. Invalid/limited calls do not allocate records. Expired records are purged before the capacity check. |
| Link rate | Creation 3/account/minute; submissions 5/account/minute and 20/peer/minute | Fixed in addition to ordinary authentication limits; no unlimited null/zero behavior. |
| Envelope/body bounds | 524,288 / 786,432 UTF-8 bytes | Fixed expansion for up to 60 text/recovery recipients; original per-box/plaintext limits remain. |
| History-share batch | At most 50 entries/request | Fixed; validate the whole batch before commit. |
| History-share quota | 67,108,864 stored bytes/domain | Fixed independent of original message quota; reject additions on exhaustion and display incomplete coverage. |
| History-share retention | Original message acceptance plus existing seven-day horizon | Derived expiry; room eligibility is checked at read and write. |
| Client capability | linked-devices/1 in X-Sloppy-Client | Fixed version signal, not an authorization credential. Unadvertised old clients cannot operate a multi-device account safely. |
Roster, slot, restricted completion token hash, device labels, sessions, voice fences and encrypted shares are operational state. A label is bounded plain text, at most 60 Unicode code points, defaulting to the client platform label; it grants no authority. Code secrets and plaintext history are client-memory data. Persist client roster pins and peer trust with the encrypted vault, preserve all existing root-vault data during migration, and never silently overwrite a previous account's device keys. SPEC-005 owns the vault bound and local account profiles. Persist new-device keys before submitting a link request and restricted completion state before completing recovery; public code metadata may be stored for reconciliation, but never the link secret. An interrupted approved link can use ordinary password login with those pending keys only after validating its signed approval evidence.
Testing
The foreign-home iOS regression scans a freshly generated Linux-compatible QR while the bundled client starts on a different service. Verify authenticated discovery, exact endpoint binding, automatic restricted submission, no password/extra submit click, no preapproval login, original-session preservation, cancellation/stale-discovery rejection, restart during pending approval and persistence after completion. Units distinguish expired, malformed and foreign-service codes; forged discovery/endpoint substitutions cannot trigger linking or receive existing tokens.
D-171 units cover both signed QR directions, secret/service/key substitution, no-password restricted submission, exact claim retries and competing approvers, cancellation before/after claim/approval, expiry, revocation, roster races, wrong poll key, capacity/rate limits and malformed/oversized codes. Browser integration renders and decodes actual QR images from independent vaults in both directions, verifies explicit approval, retained/future text, unchanged source session and manual fallback. Camera lifecycle/native permission and physical scanning gates belong to SPEC-005.
Unit-test roster chains, root substitution, invalid signer, replay/rollback/equivocation, code parsing, secretbox context substitution and bootstrap/history signatures. Test every bound and wrong-account/domain/device case. Verify transfer hashes and legacy ciphertext signatures before showing text, and preserve original positions/expiry.
Service/SQLite tests must preserve existing owner IDs/keys/sessions/history through migration, independently log in/out three devices, reject password-only/unapproved devices, handle concurrent approval/revocation/expiry/retries, enforce exact recipient fan-out, isolate account permissions and history intervals, and refuse legacy clients once unsafe. Verify a non-voice client's logout cannot close the current call, and competing voice moves are fenced until the lease cutoff.
Browser integration uses independent browser contexts with separate vaults: generate a code in the owner, request/approve the new device, read legacy text, exchange future text from both, revoke a device and reject its stale token. No production owner logout, vault inspection or reset is part of testing. Physical KDE/Windows and human listening remain explicit platform acceptance checks.
Acceptance criteria
- Scanning a valid foreign-home QR in the iOS bundled client discovers and verifies the exact account service and requests approval without another input; the original approved device must still approve before any login/history authority is issued. No QR secrets enter URLs, discovery requests or diagnostics, and pending completion resumes only at its verified bound service.
- D-171: either device can show a QR and the other scan it without new-device password entry; only explicit approved-device confirmation admits the bound fresh keys. Both directions preserve current sessions/voice and reject expired, cancelled, substituted, replayed or revoked requests.
- D-171: anonymous rendezvous stays within documented lifetime/rate/storage bounds; exact retries preserve state/deadlines, unauthorized polling fails and no account/history/media access exists before approval.
- The existing web owner session survives adding KDE and Windows devices with distinct keys and the same account UUID and current server permissions.
- A password alone, a replayed/expired code, substituted key, revoked approver or forged roster cannot approve a device or read history.
- Both old and new clients can read eligible retained transferred text and all subsequent text; missing history is reported and transfers contain no plaintext or old private device key at the service.
- Routine login and logout affect only the requesting device; explicit revocation removes the selected device's active authority and cannot remove the last device.
- Voice remains on its current client until an explicit fenced move; no two devices of the account can forward concurrently and a move does not extend the generation deadline.
- Existing IDs, passwords, owner/bootstrap state, ciphertext, permissions and membership intervals survive restart and migration; old unsupported clients receive a clear upgrade error.
- Configuration, tests and release notes accurately distinguish linked-device delivery from the separate recovery workflow and remaining physical acceptance.
D-063 verification optimization
A process-local cache may reuse a successful complete roster validation only for identical canonical domain, account, root device and signed chain contents. Cache at most 32 entries and 1 MiB of canonical-key characters; oversized entries are verified without caching. Return defensive copies so mutation cannot poison cached authority. Never cache failures or use this cache instead of per-vault rollback/pin checks. These are fixed implementation bounds, not account configuration.