diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-14 04:02:20 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-14 04:02:20 +0200 |
| commit | 8f9ac4d931e43358cb0e4e82085f49e916419a8b (patch) | |
| tree | 74ae521e1f9e49867d9b9adda2386960800af084 | |
| parent | 9f0904247116005bba8e32b9428dc6a2b994e705 (diff) | |
| download | meshbay-8f9ac4d931e43358cb0e4e82085f49e916419a8b.tar.gz | |
fix(client): recover identity keys from what the browser already has
Registering in Firefox and coming back to it said "This browser does not hold
your keys" — while both halves of those keys were on disk a few bytes apart.
_sessionKeys lives in sessionStorage, which dies with the tab. The encrypted
keypair bundle is in localStorage from registration, and the key that opens it is
in IndexedDB from login, but nothing ever put the two together again: only the
login path did, and a returning user is restored from stored auth without logging
in. So closing a tab looked identical to never having registered there.
Recovery now happens before connecting: bundle from localStorage, key from
IndexedDB, public half derived from our own secret rather than read back from the
hub. The bundle is also queued for backup to the node, which is what lets a
second browser recover the same keys with the password.
Not hardening, and not from the invite redesign — an oversight in session
restore that the redesign made visible, because joining is now the first thing
that needs those keys.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 40 |
1 files changed, 40 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 6fefe0f..0aa74b4 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -122,6 +122,45 @@ function _restoreSessionKeys() { } catch {} } +/** Public X25519 key from our own secret — never read back from the hub. */ +async function _pkXFromSk(skPkcs8B64) { + const raw = Uint8Array.from(atob(skPkcs8B64), c => c.charCodeAt(0)); + const sk = await crypto.subtle.importKey('pkcs8', raw, { name: 'X25519' }, true, ['deriveBits']); + const jwk = await crypto.subtle.exportKey('jwk', sk); + const b64 = jwk.x.replace(/-/g, '+').replace(/_/g, '/'); + const pad = b64.length % 4; + return pad ? b64 + '='.repeat(4 - pad) : b64; +} + +/** + * Recover our identity keys from what this browser already holds. + * + * sessionStorage dies with the tab, but the encrypted keypair bundle sits in + * localStorage from registration and the key that opens it is in IndexedDB from + * login. Without this, closing the tab looked exactly like never having + * registered here — "this browser does not hold your keys", while both halves + * were on disk a few bytes apart. + */ +async function _recoverLocalKeys(username) { + if (_sessionKeys || !username) return; + try { + if (!_bundleKey) _bundleKey = await _loadBundleKey(); + if (!_bundleKey || !window.MeshBayKeys) return; + const enc = localStorage.getItem(`meshbay_kp_${username}`); + if (!enc) return; + const keys = await window.MeshBayKeys.decryptBundleWithKey(enc, _bundleKey); + _sessionKeys = { + skXB64: keys.skX, + skEdB64: keys.skEd, + pkXB64: await _pkXFromSk(keys.skX), + }; + _pendingBundlePush = enc; // still to be backed up to a node + _saveSessionKeys(); + } catch (e) { + console.warn('[MeshBay] could not recover local keys:', e); + } +} + function loadAuth() { try { return JSON.parse(localStorage.getItem(AUTH_KEY)); @@ -824,6 +863,7 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { gekRef.current = null; if (!_bundleKey) _bundleKey = await _loadBundleKey(); _restoreSessionKeys(); + await _recoverLocalKeys(username); try { const nodesData = await hubFetch(`/v1/groups/${groupId}/nodes`, { token }); if (cancelled) return; |