From 0ed56d3a1b4f71cf622d3e27edc87a15ef33c185 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 30 Sep 2026 21:04:39 +0200 Subject: fix(hub): the pepper and a device key take the passphrase, not a token POST /me/bundle-pepper (was GET) and POST /users/devices require auth_key. A refreshed or lifted token could otherwise fetch the pepper, or register a device whose every sign-in carries it. Both callers have just been given the passphrase. Co-Authored-By: Claude Opus 5.5 --- .../src/meshbay_hub/static/keyderive.js | 24 +++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/keyderive.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js b/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js index 9f28b5c..6bd5896 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js @@ -195,14 +195,25 @@ async function nodeBundleKey(sessionKey, nodePkB64) { } /** - * GET the pepper for a session that is already open — a stored session from - * before the pepper existed, a passphrase change. Sign-in carries it already. + * The pepper for a session that is already open but lacks the key derived from + * it — a restored session, a passphrase change. Sign-in carries it already. The + * hub asks for the passphrase proof: a token alone never obtains the pepper. */ -async function fetchBundlePepper(token) { +async function fetchBundlePepper(token, authKey) { const resp = await hubCall('/v1/users/me/bundle-pepper', { - headers: { Authorization: `Bearer ${token}` }, + method: 'POST', + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ auth_key: authKey }), }); - if (!resp.ok) throw new Error(`bundle pepper: ${resp.status}`); + if (!resp.ok) { + // The hub's own words: a wrong passphrase and a locked account are what a + // person can act on, a bare status is not. + let detail = `bundle pepper: ${resp.status}`; + try { const j = await resp.json(); detail = j.detail || j.error || detail; } catch { /* not JSON */ } + const err = new Error(String(detail)); + err.status = resp.status; + throw err; + } const data = await resp.json(); return { pepper: data.bundle_pepper, version: data.bundle_pepper_version }; } @@ -446,6 +457,9 @@ async function loginAndRecover(username, password) { const result = { accessToken: data.access_token, refreshToken: data.refresh_token, + // Kept for the one call that follows a sign-in and must prove the + // passphrase again: registering this device's key. Not stored. + authKey, // The pepper rides on the sign-in response, so this costs no extra call; // it is folded into the key here and not kept. bundleKey: await sessionBundleKey( -- cgit v1.2.3