diff options
Diffstat (limited to 'docs/MESHBAY_NODE_PROTOCOL.md')
| -rw-r--r-- | docs/MESHBAY_NODE_PROTOCOL.md | 39 |
1 files changed, 29 insertions, 10 deletions
diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md index 9efef08..e7fce90 100644 --- a/docs/MESHBAY_NODE_PROTOCOL.md +++ b/docs/MESHBAY_NODE_PROTOCOL.md @@ -102,7 +102,8 @@ Identical on every transport: | Bound | Value | Where | |---|---|---| | Max frame **before** the client's group-key proof | 64 KiB | `PRE_HANDSHAKE_MAX_MSG` | -| Max frame **after** the proof | 64 MiB | `MAX_MSG` | +| Max frame **after** the proof | 8 MiB | `MAX_MSG` — eight times the largest message a client sends (a sealed playlist blob, 1 MiB) | +| Decoding a frame | arrays 100 000, maps 10 000, strings 1 MiB, binaries 8 MiB, no extension types | `UNPACK_LIMITS` | | File chunk (plaintext) | 1 MiB | `CHUNK_SIZE` | | Video segment (plaintext, before encryption) | 256 KiB | `STREAM_SEGMENT_SIZE` | | Upload chunk sent by the browser | 48 KiB | fits the aiortc SCTP limit after msgpack overhead | @@ -115,6 +116,10 @@ into it, holding that much memory per connection for as long as it liked; a hund such connections is the node's memory, from peers that have proved nothing. Exceeding the limit is a hard protocol error and the buffer raises rather than truncating — truncating would hand a parser a valid-looking prefix of something it never received. +A frame refused for its size or its decoding ends the session: the buffer still starts +with it, so nothing after it could be read. The decoding bounds exist because a frame of +many tiny elements decodes to many times its size in memory; with them, an 8 MiB frame +costs tens of megabytes at worst. ### 3.3 Versioning field @@ -245,7 +250,7 @@ answer, and a discriminator of the message's own (`file_id`, `url`, `upload_id`, v +-------------------------+ | AUTHENTICATED | `_user_id` / `_group_id` set, - | full message set | frame limit raised to 64 MiB + | full message set | frame limit raised to 8 MiB +-----------+-------------+ | channel closes v @@ -416,7 +421,7 @@ fails if a transport skips a step. | compare_digest(proof); | | roster admits the user | | 9. session authenticated: | - | frame limit -> 64 MiB, | + | frame limit -> 8 MiB, | | peer registry, audit | | | | 10. handshake_ack | @@ -680,11 +685,13 @@ the node that proved it, for the account that stored it. (`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. +* **A client writes `MBK3` and nothing else.** It reads one earlier format once, + to replace it (*transitional*): `MBK2`, `"MBK2" ‖ nonce (12) ‖ AES-GCM` under the + passphrase's Argon2 key, no associated data. The identity in it is stored again + as `MBK3` with `keypair_bundle_store` once the session is up. Anything older is + refused by name (`bundle_format_retired`), and the client 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. @@ -1001,6 +1008,9 @@ D_req = "meshbay:device_req:v1" || LP(node_pk) || LP(user_id) || LP(pk_ed) || D_add = "meshbay:device_add:v1" || LP(node_pk) || LP(user_id) || LP(pk_ed) || LP(pk_x) || LP(nonce_s) || LP(ts) signed by a PINNED device +D_rev = "meshbay:device_revoke:v1" || LP(node_pk) || LP(user_id) || LP(pk_ed) || + LP(nonce_s) || LP(ts) signed by a PINNED device + code_hash = sha256( code "\x1f" pk_ed25519_b64 "\x1f" pk_x25519_b64 ) ``` @@ -1009,6 +1019,11 @@ code_hash = sha256( code "\x1f" pk_ed25519_b64 "\x1f" pk_x25519_b64 ) * `D_add` deliberately **omits the code**: the code is a bearer secret used to find the request, never signed, never echoed. What is signed is the key pair being admitted, so a signature collected for one device cannot admit another. +* `device_add` **must name a pending request** (`code_hash`) filed by the same + `pk_ed25519` and `pk_x25519`; without one, or with one filed by other keys, it is + refused and the request is not spent. A countersignature alone admits nothing. +* `D_rev` has a prefix of its own. Were a retirement signed over `D_add`, a signature + given to retire a key would admit that key wherever it is not pinned yet. * Because both keys go into `code_hash`, a node cannot answer the approver with a substituted key: the approver recomputes the hash from what it typed and what it was given. Nothing here rests on a human comparing digits. @@ -1022,7 +1037,7 @@ code_hash = sha256( code "\x1f" pk_ed25519_b64 "\x1f" pk_x25519_b64 ) N -> C device_list_result {pending, devices: [{pk_ed25519, label, pinned_at, pinned_via, added_by_pk, is_this_one}]} - C -> N device_revoke {pk_ed25519, ts, sig over D_add for the victim's keys} + C -> N device_revoke {pk_ed25519, ts, sig over D_rev for the victim's key} N -> C device_add_ack {revoked: pk_ed25519} ``` @@ -2361,6 +2376,10 @@ device add "meshbay:device_add:v1" LP(node_pk) LP(user_id) LP(pk_ed25519) LP LP(nonce_s) LP(ts) -> Ed25519 by an ALREADY-PINNED device of the same account +device rev "meshbay:device_revoke:v1" LP(node_pk) LP(user_id) LP(pk_ed25519) + LP(nonce_s) LP(ts) + -> Ed25519 by an ALREADY-PINNED device of the same account + device hello "meshbay:device_hello:v1" LP(node_pk) LP(group_id) LP(user_id) LP(pk_ed25519) LP(nonce_s) LP(ts) -> Ed25519 by the device claiming this connection @@ -2410,7 +2429,7 @@ LP(x) = uint32be(len(x)) || x every field, no exceptions | Device attempts | 5 per connection | ” | | `MAX_DEVICES_PER_USER` | 5 | `roster.py` | | `MAX_LINK_INVITES_PER_GROUP` | 20 unredeemed invitation links | `roster.py` | -| `PRE_HANDSHAKE_MAX_MSG` / `MAX_MSG` | 64 KiB / 64 MiB | `webrtc/core.py` / `webrtc/limits.py` | +| `PRE_HANDSHAKE_MAX_MSG` / `MAX_MSG` | 64 KiB / 8 MiB | `webrtc/core.py` / `webrtc/limits.py` | | `CHUNK_SIZE` | 1 MiB | `webrtc/limits.py` | | `DOWNLOAD_BUFFER_HIGH` | 2 MiB | `webrtc/files.py` | | `MAX_UPLOAD_BYTES` | 8 GiB, default only — `max_upload_gb` overrides it per node | `webrtc/upload_handlers.py` | |