diff options
Diffstat (limited to 'docs/MESHBAY_NODE_PROTOCOL.md')
| -rw-r--r-- | docs/MESHBAY_NODE_PROTOCOL.md | 34 |
1 files changed, 13 insertions, 21 deletions
diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md index 642e966..07b179c 100644 --- a/docs/MESHBAY_NODE_PROTOCOL.md +++ b/docs/MESHBAY_NODE_PROTOCOL.md @@ -728,9 +728,9 @@ wrap_key = HKDF-SHA256(shared, salt = pk_eph, info = "meshbay:gek_wrap:v wrapped = AES-256-GCM(wrap_key).encrypt(nonce_96, GEK, aad = pk_recipient) ``` -AES-GCM because WebCrypto has no ChaCha20-Poly1305; a `chacha20-poly1305` variant with -`info = "meshbay:gek_wrap:v1"` exists for native clients. The recipient's public key is -the AEAD's associated data, so a bundle cannot be re-addressed. +AES-GCM because WebCrypto has no ChaCha20-Poly1305, and one cipher serves every +client. The recipient's public key is the AEAD's associated data, so a bundle cannot be +re-addressed. `found: false` is the normal answer: per-member bundles are not stored, and the key is produced on demand by the join path (§8). The node keeps one stored bundle of its own @@ -1290,9 +1290,9 @@ ciphertext. That is the same statement the index makes, one step stronger. `salt = <none>` is Python's `salt=None` and WebCrypto's `salt: new Uint8Array(0)`; RFC 5869 extracts with a zero key either way. The subkeys are purpose-separated -rather than borrowed from a file's key space — `GroupIndex.serialize()` reuses -`chunk_key_aes` with a pseudo-file ("the index as chunk 0 of a virtual index file"), -which is a hack this deliberately does not repeat. +rather than borrowed from a file's key space — reusing `chunk_key_aes` with a +pseudo-file ("the index as chunk 0 of a virtual index file") is a hack this +deliberately does not repeat. **What stays in clear, and why each one has to:** @@ -1337,10 +1337,6 @@ purpose and a fresh 96-bit random nonce per message: at one message per 48 KiB c reaches 2⁻³², and `gek_rotate` exists. Deriving the nonce from the payload instead would be worse, not better — two chunks of identical bytes are ordinary in a file. -`GroupIndex.serialize()` is not a candidate for reuse here: it compresses with zstd, -which no browser can decompress (`DecompressionStream` offers gzip and deflate only), -so reusing it would mean shipping a WASM decoder to every client for no gain. - **Failure is fatal, never degraded** (I8). A client that cannot open an index message ends the session naming the message type; it never reports an empty index, because "the group has no files" is a state a real group can be in. @@ -1531,8 +1527,9 @@ nonce = 12 random bytes ct = AES-256-GCM(chunk_key).encrypt(nonce, plaintext) no AAD ``` -* The `:aes` suffix keeps AES keys distinct from the ChaCha20 variant - (`chunk_key`/`encrypt_chunk`, `info` without the suffix) derived from the same GEK. +* The `:aes` suffix is part of every chunk key: it once kept these distinct from a + ChaCha20 variant derived from the same GEK, which no longer exists, and it stays + because removing it would change every key. * `nonce` and `ct` are msgpack **binary**, not base64. Every message that carries content carries it this way, and none carries it outside an AEAD. * The key is a pure function of (GEK, file hash, index), so chunks are cacheable, @@ -2094,7 +2091,7 @@ speak, and every message above is available on it. **QUIC is in development** (§5.2): a partial message set, no client, and not a shipped feature. Nothing about it is a compatibility commitment yet. -Two rules hold across transports, and both are about there being exactly one of each +One rule holds across transports, and it is about there being exactly one of each message: * **One encoder per message type, shared by every transport.** `file_chunk` comes from @@ -2103,11 +2100,6 @@ message: its own. Two encoders for one type is a type free to drift, with a name that no longer says which shape will arrive — and, when one of them is a sealed envelope, a second construction site that goes on sending cleartext. -* **`GroupIndex.serialize()` / `deserialize()` describes no MNP message.** It is a - signed, compressed, encrypted at-rest and interchange format, and reading it as a wire - contract is a mistake worth naming: the sealed envelope of §11.1a is what index - messages travel under, and it is deliberately not this, because zstd decompresses in - no browser. --- @@ -2371,7 +2363,7 @@ LP(x) = uint32be(len(x)) || x every field, no exceptions | `MAX_CHAT_CIPHERTEXT` / chat rate | 64 KiB / 60 per 60 s per account per group | `webrtc/chat.py` | | GEK | 256-bit, node CSPRNG | `crypto.py` | | Chat epoch key | 256-bit, node CSPRNG, one per group per epoch | `chatbox.py` | -| Chunk cipher | AES-256-GCM, 96-bit nonce (ChaCha20-Poly1305 variant for native) | `webcrypto.py`, `crypto.py` | +| Chunk cipher | AES-256-GCM, 96-bit nonce | `webcrypto.py` | | GEK wrap | X25519 + HKDF-SHA256 + AES-256-GCM, AAD = recipient public key | `crypto.py` | | Sealed message envelope | HKDF-SHA256 subkey per purpose, AES-256-GCM, 96-bit random nonce | `groupbox.py`, `static/crypto.js` | | Chat envelope | HKDF-SHA256 subkey per device per epoch, AES-256-GCM, 96-bit random nonce, Ed25519 over the ciphertext | `chatbox.py` | @@ -2391,8 +2383,8 @@ meshbay-common/ protocol.py message types, chunk and upload codecs adminop.py the admin transcript and the operation catalogue join.py join transcript and pairing codes device.py device request / add / hello transcripts - crypto.py GEK, chunk keys, ECIES wrap, BLAKE3 ids - webcrypto.py the AES variants the browser can also compute + crypto.py GEK, ECIES wrap, keystore, BLAKE3 ids + webcrypto.py the content cipher: per-chunk AES-GCM keys tokens.py the two token audiences (node vs hub API) meshbay-node/ transport/webrtc_server.py the reference implementation of MNP, |