aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/transport.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-30 15:06:14 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-30 15:06:14 +0200
commit91297944791a36f30302ef8c86dd69ebeb177671 (patch)
tree568188114baf438059458f1bc87903f4894cec90 /packages/meshbay-hub/src/meshbay_hub/static/transport.js
parenta55d40b74bda77dff6ec565abdd551607fc665d6 (diff)
downloadmeshbay-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.js28
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,
};