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 <[email protected]>
This commit is contained in:
+1
-1
Submodule nobilis updated: 2505f58e15...412c23201d
@@ -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 {
|
||||
|
||||
@@ -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<string | null>(null)
|
||||
@@ -115,6 +119,72 @@ export function MatrixAccountTools({ account }: { account: Account }): JSX.Eleme
|
||||
|
||||
return (
|
||||
<div className="matrix-tools">
|
||||
<div className="setting-row">
|
||||
<div className="setting-text">
|
||||
<div>Keep a spare device for room keys</div>
|
||||
<div className="small muted">
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
{account.dehydration ? (
|
||||
<span className="small muted">On</span>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="button subtle"
|
||||
disabled={dehydrating}
|
||||
onClick={() => {
|
||||
setDehydrating(true)
|
||||
void rpc('enableMatrixDehydration', {
|
||||
accountId: account.id,
|
||||
unlockWith: storageSecret.trim() || undefined
|
||||
})
|
||||
.then((a: { recoveryCode?: string }) => {
|
||||
setStorageSecret('')
|
||||
if (a.recoveryCode) setNewCode(a.recoveryCode)
|
||||
else store.toast('info', 'Spare device set up')
|
||||
})
|
||||
.then(() => void store.refreshAccounts())
|
||||
.finally(() => setDehydrating(false))
|
||||
}}
|
||||
>
|
||||
Set up
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{!account.dehydration && (
|
||||
<div className="device-row">
|
||||
<Icon name="key" size={18} />
|
||||
<div className="field-row setting-text">
|
||||
<input
|
||||
className="text-field"
|
||||
type="password"
|
||||
placeholder="Recovery code or passphrase, if this account already has one"
|
||||
value={storageSecret}
|
||||
onChange={(e) => setStorageSecret(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Shown once and never again: it is not kept anywhere this daemon can
|
||||
read it back, which is the point of it. */}
|
||||
{newCode && (
|
||||
<div className="device-row">
|
||||
<Icon name="lock" size={18} />
|
||||
<div className="setting-text">
|
||||
<div className="small warn-text">
|
||||
Write this down. It is the only way to collect those keys on a new sign-in, and
|
||||
it is not stored anywhere.
|
||||
</div>
|
||||
<code className="recovery-code">{newCode}</code>
|
||||
</div>
|
||||
<button type="button" className="button subtle" onClick={() => setNewCode('')}>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="setting-row">
|
||||
<div className="setting-text">
|
||||
<div>Sliding sync</div>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user