diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-30 17:26:59 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-30 17:26:59 +0200 |
| commit | d692db441680eef8969573047cf5da00cfb61362 (patch) | |
| tree | 4a67e4bd2a6421a154c706d2aeb8cc7bfd548187 /docs/MESHBAY_NODE_PROTOCOL.md | |
| parent | b1ebcdeb9082457972c41a47e77494902335d262 (diff) | |
| download | meshbay-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.md | 65 |
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 |