diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-30 15:06:14 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-30 15:06:14 +0200 |
| commit | 91297944791a36f30302ef8c86dd69ebeb177671 (patch) | |
| tree | 568188114baf438059458f1bc87903f4894cec90 /packages/meshbay-hub/src/meshbay_hub/static/transport.js | |
| parent | a55d40b74bda77dff6ec565abdd551607fc665d6 (diff) | |
| download | meshbay-91297944791a36f30302ef8c86dd69ebeb177671.tar.gz | |
feat: bundles sealed per node under the passphrase and the hub's pepper
The session key is M = HKDF(Argon2(passphrase) || pepper, account id); each
node's bundle key and the playlist key derive from it. Bundles are MBK3, bound
to account and node; MBK1/MBK2 are refused by name, never replaced silently.
Playlists move to key v2 and are re-sealed over unreadable node copies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/transport.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/transport.js | 28 |
1 files changed, 22 insertions, 6 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index 04c2d23..b504a28 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -966,15 +966,31 @@ class MeshBayTransport { // that opens nothing anywhere else. let fresh = false; if (!this._sessionKeys && this._bundleKey && window.MeshBayKeys) { + const K = window.MeshBayKeys; + // A bundle is sealed for this account on this node โ the key the node + // just proved above, and no other. + const sealedFor = { userId: this._userId, nodePk: this.nodePk }; const kpResp = await this._sendAndWait({ type: 'keypair_bundle_fetch', v: '0.1', }); let keys = null; let openErr = null; + if (kpResp.type === 'keypair_bundle_resp' && kpResp.found + && K.bundleFormat(kpResp.bundle_enc) === 'retired') { + // Sealed under the passphrase alone, before the pepper. Not opened, + // and not replaced by a new identity behind the member's back: that + // would leave the node pinning a key nobody holds. The operator + // unpins them (which drops this bundle) and sends a new code. + const err = new Error('This node holds your identity in a format this version ' + + 'no longer reads. Ask its operator to run "meshbay-node member unpin" ' + + 'for your account and send you a new invitation code.'); + err.reason = 'bundle_format_retired'; + throw err; + } if (kpResp.type === 'keypair_bundle_resp' && kpResp.found) { try { - keys = await window.MeshBayKeys.decryptBundleWithKey( - kpResp.bundle_enc, this._bundleKey); + keys = await K.decryptBundle(kpResp.bundle_enc, + await K.nodeBundleKey(this._bundleKey, this.nodePk), sealedFor); } catch (e) { openErr = e; // The passphrase key did not open the bundle. If we hold a recovery @@ -983,8 +999,8 @@ class MeshBayTransport { // passphrase, before re-wrapping it under the new one. if (this._recoveryKey && kpResp.bundle_enc_recovery) { try { - keys = await window.MeshBayKeys.decryptBundleWithKey( - kpResp.bundle_enc_recovery, this._recoveryKey); + keys = await K.decryptBundle( + kpResp.bundle_enc_recovery, this._recoveryKey, sealedFor); this._recoveredFromRecovery = true; } catch { /* recovery copy did not open either */ } } @@ -1014,8 +1030,8 @@ class MeshBayTransport { // behind). Mint a fresh identity and let the join path take over; a // successful join overwrites whatever was stored. A recovery-wrapped // copy is left too when a recovery key is in hand (ยง4.3). - const id = await window.MeshBayKeys.generateNodeIdentity( - this._bundleKey, this._recoveryKey); + const id = await K.generateNodeIdentity( + this._bundleKey, this._recoveryKey, sealedFor); this._sessionKeys = { skEdB64: id.skEdB64, skXB64: id.skXB64, pkXB64: id.pkXB64, }; |