diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/MESHBAY_DESIGN.md | 17 | ||||
| -rw-r--r-- | docs/MESHBAY_NODE_PROTOCOL.md | 34 |
2 files changed, 21 insertions, 30 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index 74c1050..23c587e 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -638,17 +638,17 @@ Every private key lives in an encrypted keystore on the machine that owns it. Th hub never sees one. **Domain separation is consistent and mandatory.** Every derivation uses a -distinct `info` string, and the AES variant adds an `:aes` suffix so two ciphers -can never derive the same key from one group key. This is a small detail that -prevents cross-protocol key reuse, and it is checked rather than assumed. +distinct `info` string, so no two purposes can derive the same key from one group +key. The chunk and wrap strings end in `:aes`, left from a second cipher that no +longer exists; it stays because it is part of every key already derived. ### 4.2 Group key wrapping (ECIES) ``` wrap: sk_eph, pk_eph = X25519.generate() # fresh per bundle shared = X25519(sk_eph, pk_recipient) - wrap_key = HKDF(shared, salt=pk_eph, info="meshbay:gek_wrap:v1", len=32) - wrapped = AEAD(wrap_key).encrypt(nonce, gek, aad=pk_recipient) + wrap_key = HKDF(shared, salt=pk_eph, info="meshbay:gek_wrap:v1:aes", len=32) + wrapped = AES-256-GCM(wrap_key).encrypt(nonce, gek, aad=pk_recipient) bundle = pk_eph ‖ nonce ‖ wrapped unwrap: shared = X25519(sk_recipient, pk_eph) # same derivation @@ -678,20 +678,19 @@ time. This avoids double storage and makes key rotation feasible without re-encrypting terabytes. ``` -disk (plaintext) → compress → per-chunk AEAD under a group-derived key → transport → client +disk (plaintext) → per-chunk AES-256-GCM under a group-derived key → transport → client ``` - Chunk size 1 MB: amortises AEAD overhead and enables seeking, because each chunk is independently decryptable. -- `chunk_key = HKDF(GEK, salt=None, info="file:" ‖ blake3(file) ‖ ":chunk:" ‖ index)`. +- `chunk_key = HKDF(GEK, salt=None, info="file:" ‖ blake3(file) ‖ ":chunk:" ‖ index ‖ ":aes")`. The salt is omitted deliberately: the group key is CSPRNG output and already uniform, so the file and chunk context belongs in `info`, which is the correct HKDF usage (**M5**, first review). - **Chunk authentication is the AEAD tag**, not a per-chunk signature. The tag authenticates the ciphertext under a key only members hold, which is what the signature was for. -- Compression precedes encryption, because compression is ineffective on - ciphertext. +- **Chunks are not compressed**: a chunk is encrypted and sent as it was read. - Upload chunk size is 48 KB, which is what fits the SCTP limit after msgpack overhead. 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, |