diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 142 |
1 files changed, 25 insertions, 117 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 300059d..3087e0e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -65,9 +65,10 @@ async function getAllCachedIndexes() { // ── Auth persistence ───────────────────────────────────────────────────────── -let _sessionKeys = null; +// The key that opens a node's keypair bundle, derived once at sign-in. There is +// no global identity to keep: identity keys belong to a node and are fetched from +// it (transport.js), so nothing of that kind lives here. let _bundleKey = null; -let _pendingBundlePush = null; // A one-time pairing code the user just typed, consumed by the next connection // attempt. Deliberately not persisted: it is single-use and short-lived. let _pendingJoinCode = null; @@ -108,11 +109,6 @@ async function _clearKeyDB() { db.close(); } catch {} } -function _saveSessionKeys() { - try { - if (_sessionKeys) sessionStorage.setItem('meshbay_sk', JSON.stringify(_sessionKeys)); - } catch {} -} /** * Rough passphrase strength, in bits, and what it is up against. * @@ -144,35 +140,6 @@ function passwordBits(pw) { const PASSWORD_MIN_BITS = 60; // refuse below this const PASSWORD_MIN_LEN = 12; -/** - * Re-encrypt a bundle written under the old KDF before it is stored again. - * - * PBKDF2 bundles are still readable, but leaving one on a node keeps the weak - * protection alive for as long as it sits there. Any backup is an opportunity to - * replace it with the Argon2id form, and it costs nothing the user notices. - */ -async function _upgradedBundle(bundleEnc) { - try { - if (!window.MeshBayKeys || !_sessionKeys || !_bundleKey) return bundleEnc; - if (window.MeshBayKeys.bundleVersion(bundleEnc) === 2) return bundleEnc; - const b64 = (s) => Uint8Array.from(atob(s), c => c.charCodeAt(0)); - return await window.MeshBayKeys.encryptBundleWithKey( - b64(_sessionKeys.skEdB64), b64(_sessionKeys.skXB64), _bundleKey.v2); - } catch (e) { - console.warn('[MeshBay] bundle upgrade skipped:', e.message); - return bundleEnc; - } -} - -function _restoreSessionKeys() { - try { - if (!_sessionKeys) { - const sk = sessionStorage.getItem('meshbay_sk'); - if (sk) _sessionKeys = JSON.parse(sk); - } - } 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)); @@ -183,35 +150,6 @@ async function _pkXFromSk(skPkcs8B64) { 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)); @@ -225,11 +163,8 @@ function saveAuth(auth) { localStorage.setItem(AUTH_KEY, JSON.stringify(auth)); } else { localStorage.removeItem(AUTH_KEY); - _sessionKeys = null; _bundleKey = null; - _pendingBundlePush = null; _clearKeyDB(); - try { sessionStorage.removeItem('meshbay_sk'); } catch {} } } @@ -932,8 +867,6 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { setError(''); 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; @@ -942,14 +875,9 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { return; } - // Session keys for the P2P key exchange. skEdB64 belongs here too: the - // node identifies us by the Ed25519 identity, and join_request signs both - // public keys with it — without it we can neither join nor pair. - const sessionKeys = _sessionKeys ? { - skXB64: _sessionKeys.skXB64, - skEdB64: _sessionKeys.skEdB64, - pkXB64: _sessionKeys.pkXB64, - } : null; + // No keys are carried in: the transport fetches this node's identity + // from the node, or creates one there on a first join. + const sessionKeys = null; setStatus('connecting'); const nodeId = nodesData.nodes[0].node_id; @@ -963,34 +891,15 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { if (cancelled) return; setIsNodeAdmin(!!ack.is_node_admin); - // If transport recovered different session keys from node during handshake - if (transport.sessionKeys) { - const recovered = transport.sessionKeys; - if (!_sessionKeys || recovered.skXB64 !== _sessionKeys.skXB64) { - _sessionKeys = recovered; - if (!_sessionKeys.pkXB64) { - const pubkeys = await hubFetch( - `/v1/users/${username}/pubkeys`, { token }); - _sessionKeys.pkXB64 = pubkeys.pk_x25519; - } - _pendingBundlePush = null; - try { localStorage.removeItem(`meshbay_kp_${username}`); } catch {} - _saveSessionKeys(); - } - } - - // Back the encrypted keys up to the node. This is what lets any other - // browser recover them with the passphrase, which is the ordinary - // expectation; the protection that matters is the KDF guarding the - // bundle, not withholding the bundle. - if (transport.connected && _pendingBundlePush) { + // A first join to this node generated an identity for it; leave it with + // the node so any other browser can become the same person here with the + // passphrase. It is this node's key and no other's. + if (transport.connected && transport.newNodeBundle) { try { - await transport.storeKeypairBundle( - await _upgradedBundle(_pendingBundlePush)); - try { localStorage.removeItem(`meshbay_kp_${username}`); } catch {} - _pendingBundlePush = null; + await transport.storeKeypairBundle(transport.newNodeBundle); + transport.newNodeBundle = null; } catch (e) { - console.warn('[MeshBay] Bundle push to node deferred:', e.message); + console.warn('[MeshBay] could not leave our key with the node:', e.message); } } @@ -1134,8 +1043,11 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { try { // Signs an explicit transcript built by transport.js, not opaque bytes from // the node — see MeshBayCrypto.adminTranscript and finding H5. - const signFn = (_sessionKeys && window.MeshBayKeys) - ? (transcript) => window.MeshBayKeys.signBytes(_sessionKeys.skEdB64, transcript) + // Signed with the identity this node pinned for us — the only one it + // will accept, and the only one we hold here. + const sk = transport.sessionKeys && transport.sessionKeys.skEdB64; + const signFn = (sk && window.MeshBayKeys) + ? (transcript) => window.MeshBayKeys.signBytes(sk, transcript) : null; await transport.deleteFile(entry.id, signFn); const indexMsg = await transport.fetchIndex(); @@ -1601,8 +1513,11 @@ function MembersPanel({ groupId, group, token, transportRef, gekRef, // code it never learns — the code goes to a human, out of band. const account = await hubFetch(`/v1/users/${username}/pubkeys`, { token }); - const signFn = (_sessionKeys && window.MeshBayKeys) - ? (transcript) => window.MeshBayKeys.signBytes(_sessionKeys.skEdB64, transcript) + // Signed with the identity this node pinned for us — the only one it + // will accept, and the only one we hold here. + const sk = transport.sessionKeys && transport.sessionKeys.skEdB64; + const signFn = (sk && window.MeshBayKeys) + ? (transcript) => window.MeshBayKeys.signBytes(sk, transcript) : null; const result = await transport.createInvite( account.user_id, groupId, username, signFn); @@ -2748,12 +2663,10 @@ function App() { const data = await window.MeshBayKeys.loginAndRecover(username, password); token = data.accessToken; refreshToken = data.refreshToken; + // The only thing sign-in produces: the key that opens a node's bundle. + // Which identity we use is decided per node, when we get there. _bundleKey = data.bundleKey; await _storeBundleKey(_bundleKey); - if (data.skXB64) { - _sessionKeys = { skXB64: data.skXB64, skEdB64: data.skEdB64 }; - _pendingBundlePush = data.keypairBundleEnc; - } } else { const data = await hubFetch('/v1/users/login', { method: 'POST', @@ -2763,11 +2676,6 @@ function App() { refreshToken = data.refresh_token; } const me = await hubFetch('/v1/users/me', { token }); - if (_sessionKeys) { - const pubkeys = await hubFetch(`/v1/users/${username}/pubkeys`, { token }); - _sessionKeys.pkXB64 = pubkeys.pk_x25519; - _saveSessionKeys(); - } const u = { username, userId: me.user_id, token, refreshToken, role: me.role }; setUser(u); saveAuth(u); |