From 0ea41671da49865cb42bd85942c3a48bc663b019 Mon Sep 17 00:00:00 2001 From: Salastil Date: Sat, 12 Sep 2026 10:23:19 -0400 Subject: [PATCH] Setting up the spare device, and showing the code once The window half of #193. Named for what it does rather than for what it is called - "keep a spare device for room keys" says why somebody would want it, where "dehydrated device" only makes sense once you already know. The Element name is given underneath, since that is what they will see there. The field beside it takes an existing recovery code or passphrase, because an account that already has secret storage must be opened rather than replaced: making a second safe would orphan every secret in the first, including a backup key another client is relying on. Where a safe had to be made, the new recovery code is shown once, with a line saying it is not stored anywhere - because it is not, and finding that out later is too late. Closes #193. Co-Authored-By: Claude Opus 5 --- nobilis | 2 +- src/renderer/src/app.css | 17 +++++ .../src/components/MatrixAccountTools.tsx | 70 +++++++++++++++++++ src/shared/wire.ts | 5 ++ 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/nobilis b/nobilis index 2505f58..412c232 160000 --- a/nobilis +++ b/nobilis @@ -1 +1 @@ -Subproject commit 2505f58e15cb4bc6caba05d672d207d4fa38677e +Subproject commit 412c23201d53eb3d70498495867ac6537c2dfe47 diff --git a/src/renderer/src/app.css b/src/renderer/src/app.css index bcfbda1..325f6a5 100644 --- a/src/renderer/src/app.css +++ b/src/renderer/src/app.css @@ -5418,6 +5418,23 @@ video.lightbox-media { flex: none; } +/* A recovery code, shown once. Wide-spaced and selectable because it is meant + to be copied or written out by hand, and a run of base58 read off a screen + is easy to get wrong. */ +.recovery-code { + display: block; + margin-top: 6px; + padding: 8px 10px; + border-radius: 6px; + background: var(--surface-container-lowest); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 13px; + letter-spacing: 0.06em; + line-height: 1.7; + overflow-wrap: anywhere; + user-select: all; +} + /* --- Exporting a conversation ----------------------------------------- */ .export-choice { diff --git a/src/renderer/src/components/MatrixAccountTools.tsx b/src/renderer/src/components/MatrixAccountTools.tsx index ee0ed01..e18dc91 100644 --- a/src/renderer/src/components/MatrixAccountTools.tsx +++ b/src/renderer/src/components/MatrixAccountTools.tsx @@ -57,6 +57,10 @@ export function MatrixAccountTools({ account }: { account: Account }): JSX.Eleme // server of its own. Seeded from the account and only sent when it changes, // so opening this page does not rewrite a setting nobody touched. const [rtcFocus, setRtcFocus] = useState(account.rtcFocusUrl ?? '') + const [dehydrating, setDehydrating] = useState(false) + const [storageSecret, setStorageSecret] = useState('') + // The recovery code, held only long enough to be read once. + const [newCode, setNewCode] = useState('') const [restoreKey, setRestoreKey] = useState('') const [busy, setBusy] = useState('') const [deleting, setDeleting] = useState(null) @@ -115,6 +119,72 @@ export function MatrixAccountTools({ account }: { account: Account }): JSX.Eleme return (
+
+
+
Keep a spare device for room keys
+
+ Leaves an encrypted, unused device on the homeserver so keys sent while you are + signed out are not lost. A new sign-in collects them with your recovery code. + Element calls this a dehydrated device. +
+
+ {account.dehydration ? ( + On + ) : ( + + )} +
+ {!account.dehydration && ( +
+ +
+ setStorageSecret(e.target.value)} + /> +
+
+ )} + {/* Shown once and never again: it is not kept anywhere this daemon can + read it back, which is the point of it. */} + {newCode && ( +
+ +
+
+ Write this down. It is the only way to collect those keys on a new sign-in, and + it is not stored anywhere. +
+ {newCode} +
+ +
+ )}
Sliding sync
diff --git a/src/shared/wire.ts b/src/shared/wire.ts index 7d4ea57..ccb884a 100644 --- a/src/shared/wire.ts +++ b/src/shared/wire.ts @@ -61,6 +61,11 @@ export interface Account { * the account talks to its server, so it is not taken automatically. */ slidingSync?: boolean + /** + * Matrix only: whether this account keeps a dehydrated device, so room keys + * shared while a login did not exist are not simply lost. + */ + dehydration?: boolean } export interface SneedChatRoom {