aboutsummaryrefslogtreecommitdiffstats
path: root/docs/MESHBAY_NODE_PROTOCOL.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-30 17:26:59 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-30 17:26:59 +0200
commitd692db441680eef8969573047cf5da00cfb61362 (patch)
tree4a67e4bd2a6421a154c706d2aeb8cc7bfd548187 /docs/MESHBAY_NODE_PROTOCOL.md
parentb1ebcdeb9082457972c41a47e77494902335d262 (diff)
downloadmeshbay-d692db441680eef8969573047cf5da00cfb61362.tar.gz
docs: state the pepper, MBK3, the desktop keyring and browser access as they are
Design §2.2-§3.7, §4, §5.6, §7.7, §8, §9.10 and the registers; protocol §7, §7.1, §7.1a and §13; the user guide; CLAUDE.md's parity rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'docs/MESHBAY_NODE_PROTOCOL.md')
-rw-r--r--docs/MESHBAY_NODE_PROTOCOL.md65
1 files changed, 49 insertions, 16 deletions
diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md
index b760354..9efef08 100644
--- a/docs/MESHBAY_NODE_PROTOCOL.md
+++ b/docs/MESHBAY_NODE_PROTOCOL.md
@@ -636,10 +636,12 @@ Exceeding the fetch budget is audited as `pre-proof fetch flood` and answered
`Too many requests`. Every fetch in this window is written to the audit log with the
message type, because this is a disclosure surface a hub that forges a JWT can reach:
the hub mints the tokens, so it can present one for any account, and what it can then
-ask for is that account's *encrypted* keypair bundle. The bundle is useless without the
-account passphrase, which is why the window is bounded and audited rather than closed
-— and it closes for good when clients stop storing keypair bundles on other people's
-nodes.
+ask for is that account's *sealed* keypair bundle. The bundle opens only with the
+account passphrase and the account's pepper (`MESHBAY_DESIGN.md` §3.7) — and the hub
+holds the pepper, so this is the one place where a hub can still search a passphrase
+offline, which is why the window is bounded and audited rather than closed. It is
+empty for an account whose browser access is off: the desktop application keeps its
+identities and leaves no bundle on any node.
### 7.1 Identity bundles
@@ -649,7 +651,7 @@ nodes.
|<- keypair_bundle_resp {v, found, |
| [bundle_enc], [bundle_enc_recovery]} ----------|
| |
- | decrypt bundle_enc with the passphrase-derived bundle key,
+ | open bundle_enc with this node's bundle key (K_node),
| or bundle_enc_recovery with the recovery key
| |
|-- keypair_bundle_store {v, bundle_enc, | after minting or re-wrapping
@@ -659,26 +661,52 @@ nodes.
|-- keypair_bundle_delete {v} ---------------------->| withdraw the backup
```
-* The bundle is opaque to the node: it is encrypted client-side under a key derived
- from the account passphrase (`keyderive.js`), and optionally a second copy under the
- account recovery key. The node stores bytes and serves them back to the same
- `user_id`.
+The node stores both fields as it receives them. What the client puts in them is:
+
+```
+bundle_enc = base64( "MBK3" ‖ pepper version (1 byte) ‖ nonce (12) ‖
+ AES-256-GCM(K_node, JSON {skEd, skX}, aad) )
+aad = "meshbay:bundle:v3|" + account id + "|" + node public key (base64)
+K_node = HKDF-SHA256(M, info = "meshbay:bundle:v3|node|" + node public key)
+M = HKDF-SHA256(A ‖ pepper, info = "meshbay:bundle-master:v3|" + account id)
+```
+
+`bundle_enc_recovery` has the same form under the recovery key, with pepper version
+0. `skEd` and `skX` are PKCS#8, base64. The node public key is the one the node
+proved in its signed challenge (§5), so a bundle is sealed for — and opens only on —
+the node that proved it, for the account that stored it.
+
+* The bundle is opaque to the node: it is sealed client-side as above
+ (`keyderive.js` in a browser, `keyring.js` in the desktop application's main
+ process), and optionally a second copy under the account recovery key. The node
+ stores bytes and serves them back to the same `user_id`.
+* **A client reads `MBK3` and nothing else.** A bundle in an earlier format was
+ sealed under the passphrase alone; the client refuses it by name
+ (`bundle_format_retired`) and does not mint a replacement identity, which would
+ leave the node pinning a key nobody holds. `member unpin` drops the bundle, and
+ the next join is a first contact.
* `keypair_bundle_store` is accepted **after** authentication (it is not in the
pre-proof list); the fetch is what happens before.
* A `store` omitting `bundle_enc_recovery` leaves any existing recovery copy in place.
-* Identity keys are **per node**. There is nothing to carry between nodes, and an
- operator who cracks the copy on their own disk gets a key that opens nothing
+* Identity keys are **per node**, and so are bundle keys. There is nothing to carry
+ between nodes, and an identity or a `K_node` taken from one node opens nothing
anywhere else.
-* `keypair_bundle_delete` is **reserved for `device_policy`** (`MESHBAY_DESIGN.md`
- §3.7, open item O3): the node honours it, and no interface sends it yet. Withdrawing
- the bundle is only safe once the account has chosen not to need it from a browser —
- a lone button would strand the next browser that signs in.
+* `keypair_bundle_delete` is sent by the desktop application for an account whose
+ browser access is off (`MESHBAY_DESIGN.md` §3.7): after connecting, it withdraws any
+ bundle a node still holds for the account, and stores none. With browser access on,
+ it stores the identity it holds, sealed as above, and seals it again when the
+ account's `M` has changed since — which is how a passphrase change reaches a node
+ that was offline when it happened. Browser access is decided in the application,
+ never on the wire: nodes do not read it.
### 7.1a Per-account blobs (MNP 3.1)
The same shape as a keypair bundle with a different payload — playlists today
(`docs/playlists.md` §8). The node stores bytes it cannot read for an account it
-already holds a bundle for, so this adds **no new trust boundary**.
+already admits, so this adds **no new trust boundary**. The client seals them under
+`K_pl = HKDF-SHA256(M, info = "meshbay:playlists:v2")` — the one key every node of
+the account shares, since a playlist is read from any of them — and a blob that does
+not open under it is overwritten with the client's own copy, never treated as newer.
```
C N
@@ -2216,6 +2244,11 @@ version can no longer connect" instead. An unreachable hub is deliberately *not*
as too old: a captive portal or a closed laptop must not make starting the application
impossible.
+`client.minimum` also moves for a change the protocol does not see. The keypair bundle
+is opaque to the node, so the `MBK3` format (§7.1) moved no protocol version — but a
+desktop client older than 0.17.0 still writes the format it replaced, so 0.17.0 is the
+minimum.
+
**The version a peer announces is only as good as the number it ships with.** Every
package in the tree carries one version, and a test fails if two disagree — a client
announcing a number from a different scheme sorts wherever that scheme puts it, and