aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common/src/meshbay_common
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-28 16:24:12 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-28 16:24:12 +0200
commita4b36e5fe31cb671a4cbbdaad75746cd68b601fe (patch)
tree48f9f4fb76f91e063c436ace344d5220611de8b5 /packages/meshbay-common/src/meshbay_common
parent5330896c0e8023053d4cd15961b2ae0482686ca6 (diff)
downloadmeshbay-a4b36e5fe31cb671a4cbbdaad75746cd68b601fe.tar.gz
refactor: remove the unused GroupIndex.serialize chain
serialize/deserialize had no production caller, and took with them the per-chunk signature, the ChaCha20 cipher variant and the zstandard dependency. Key derivations are unchanged. Docs corrected, including design §4.3's claim that chunks are compressed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-common/src/meshbay_common')
-rw-r--r--packages/meshbay-common/src/meshbay_common/crypto.py92
-rw-r--r--packages/meshbay-common/src/meshbay_common/groupbox.py8
-rw-r--r--packages/meshbay-common/src/meshbay_common/protocol.py8
-rw-r--r--packages/meshbay-common/src/meshbay_common/webcrypto.py27
4 files changed, 17 insertions, 118 deletions
diff --git a/packages/meshbay-common/src/meshbay_common/crypto.py b/packages/meshbay-common/src/meshbay_common/crypto.py
index e537500..8fb80f8 100644
--- a/packages/meshbay-common/src/meshbay_common/crypto.py
+++ b/packages/meshbay-common/src/meshbay_common/crypto.py
@@ -41,25 +41,6 @@ def generate_gek() -> bytes:
"""Generate a fresh 256-bit Group Encryption Key."""
return ChaCha20Poly1305.generate_key()
-def chunk_key(gek: bytes, file_hash: bytes, chunk_index: int) -> bytes:
- """Derive a per-chunk encryption key from the GEK (deterministic)."""
- return HKDF(
- algorithm=hashes.SHA256(),
- length=32,
- salt=None,
- info=b"file:" + file_hash + b":chunk:" + chunk_index.to_bytes(4, "big"),
- ).derive(gek)
-
-def encrypt_chunk(key: bytes, plaintext: bytes) -> tuple[bytes, bytes]:
- """Encrypt plaintext with ChaCha20-Poly1305. Returns (nonce, ciphertext)."""
- nonce = os.urandom(12)
- ct = ChaCha20Poly1305(key).encrypt(nonce, plaintext, None)
- return nonce, ct
-
-def decrypt_chunk(key: bytes, nonce: bytes, ciphertext: bytes) -> bytes:
- """Decrypt ciphertext. Raises InvalidTag on authentication failure."""
- return ChaCha20Poly1305(key).decrypt(nonce, ciphertext, None)
-
def file_hash(path_or_bytes) -> bytes:
"""Compute blake3 hash of a file (bytes or path-like)."""
if isinstance(path_or_bytes, (str, bytes)) and not isinstance(path_or_bytes, bytes):
@@ -73,58 +54,6 @@ def file_hash(path_or_bytes) -> bytes:
# ── GEK wrapping (ECIES-like) ─────────────────────────────────────────────────
-GEK_WRAP_INFO = b"meshbay:gek_wrap:v1"
-
-def wrap_gek(gek: bytes, pk_recipient: bytes) -> dict:
- """
- Wrap a GEK for a recipient using ephemeral X25519 + HKDF + ChaCha20-Poly1305.
-
- Protocol:
- 1. Generate ephemeral (sk_eph, pk_eph)
- 2. shared = X25519(sk_eph, pk_recipient)
- 3. wrap_key = HKDF(shared, salt=pk_eph, info=GEK_WRAP_INFO)
- 4. wrapped = ChaCha20-Poly1305(wrap_key).encrypt(nonce, gek, aad=pk_recipient)
-
- The hub stores {pk_eph, nonce, wrapped} — opaque, cannot decrypt.
- """
- sk_eph = X25519PrivateKey.generate()
- pk_eph_raw = pk_to_raw(sk_eph.public_key())
-
- shared = sk_eph.exchange(X25519PublicKey.from_public_bytes(pk_recipient))
- wrap_key = HKDF(
- algorithm=hashes.SHA256(), length=32,
- salt=pk_eph_raw, info=GEK_WRAP_INFO,
- ).derive(shared)
-
- nonce = os.urandom(12)
- wrapped = ChaCha20Poly1305(wrap_key).encrypt(nonce, gek, pk_recipient)
-
- return {
- "pk_eph_b64": base64.b64encode(pk_eph_raw).decode(),
- "nonce_b64": base64.b64encode(nonce).decode(),
- "wrapped_b64": base64.b64encode(wrapped).decode(),
- }
-
-def unwrap_gek(bundle: dict, sk_recipient: bytes, pk_recipient: bytes) -> bytes:
- """
- Unwrap a GEK bundle using the recipient's X25519 private key.
- Raises InvalidTag if the key is wrong or the bundle was tampered.
- """
- pk_eph_raw = base64.b64decode(bundle["pk_eph_b64"])
- nonce = base64.b64decode(bundle["nonce_b64"])
- wrapped = base64.b64decode(bundle["wrapped_b64"])
-
- shared = X25519PrivateKey.from_private_bytes(sk_recipient).exchange(
- X25519PublicKey.from_public_bytes(pk_eph_raw)
- )
- wrap_key = HKDF(
- algorithm=hashes.SHA256(), length=32,
- salt=pk_eph_raw, info=GEK_WRAP_INFO,
- ).derive(shared)
-
- return ChaCha20Poly1305(wrap_key).decrypt(nonce, wrapped, pk_recipient)
-
-
GEK_WRAP_INFO_AES = b"meshbay:gek_wrap:v1:aes"
def wrap_gek_aes(gek: bytes, pk_recipient: bytes) -> dict:
@@ -220,24 +149,3 @@ def decrypt_keystore(iv: bytes, ciphertext: bytes, tag: bytes, key: bytes) -> by
"""Decrypt keystore blob. Raises on authentication failure."""
dec = Cipher(algorithms.AES(key), modes.GCM(iv, tag)).decryptor()
return dec.update(ciphertext) + dec.finalize()
-
-# ── Chunk signing ─────────────────────────────────────────────────────────────
-
-def sign_chunk(sk_node: Ed25519PrivateKey, chunk_index: int,
- nonce: bytes, ct_hash: bytes) -> bytes:
- """
- Sign chunk metadata. Payload: chunk_index || nonce || ct_hash.
-
- Despite the name, this no longer signs file chunks — Phase 9.15 dropped per-chunk
- signatures on the WebRTC path and 2026-09-03 dropped the QUIC copy that had been
- left behind. Its one caller is `GroupIndex.serialize()`, which signs a whole index
- envelope under the pseudo-index `INDEX_CHUNK`.
- """
- payload = chunk_index.to_bytes(4, "big") + nonce + ct_hash
- return sk_node.sign(payload)
-
-def verify_chunk_signature(pk_node: Ed25519PublicKey, chunk_index: int,
- nonce: bytes, ct_hash: bytes, signature: bytes) -> None:
- """Verify chunk signature. Raises InvalidSignature on failure."""
- payload = chunk_index.to_bytes(4, "big") + nonce + ct_hash
- pk_node.verify(signature, payload)
diff --git a/packages/meshbay-common/src/meshbay_common/groupbox.py b/packages/meshbay-common/src/meshbay_common/groupbox.py
index 3b45dba..260e362 100644
--- a/packages/meshbay-common/src/meshbay_common/groupbox.py
+++ b/packages/meshbay-common/src/meshbay_common/groupbox.py
@@ -32,10 +32,10 @@ it is an oversight. The node holds the GEK for its own group, so unlike the inde
this direction seals *towards* the node: it opens the payload before it writes
anything to disk, and refuses a chunk that does not open rather than guessing.
-Purpose separation is deliberate. `GroupIndex.serialize()` reuses
-`chunk_key_aes(gek, file_hash, chunk_index)` with a pseudo-file ("the index as chunk
-0 of a virtual index file"), which borrows a file's key space for something that is
-not a file. Each purpose here derives its own subkey instead.
+Purpose separation is deliberate. The alternative — reusing
+`chunk_key_aes(gek, file_hash, chunk_index)` with a pseudo-file, "the index as chunk
+0 of a virtual index file" — borrows a file's key space for something that is not a
+file. Each purpose here derives its own subkey instead.
"""
from __future__ import annotations
diff --git a/packages/meshbay-common/src/meshbay_common/protocol.py b/packages/meshbay-common/src/meshbay_common/protocol.py
index 6b929cd..5e666e9 100644
--- a/packages/meshbay-common/src/meshbay_common/protocol.py
+++ b/packages/meshbay-common/src/meshbay_common/protocol.py
@@ -318,9 +318,8 @@ class IndexEntry:
def index_entry_wire(e: IndexEntry) -> dict:
"""
- The wire-dict shape used by INDEX_SYNC/INDEX_DELTA hand-built messages
- (as opposed to GroupIndex.serialize()'s asdict() encoding of the whole
- index). Centralized so the three call sites that build these
+ The wire-dict shape used by INDEX_SYNC/INDEX_DELTA hand-built messages.
+ Centralized so the three call sites that build these
(webrtc/files.py's _do_index_sync, daemon._broadcast_index_change's two
branches) can't drift from each other as fields are added.
"""
@@ -369,8 +368,7 @@ class IndexDelta:
# under a key derived from the GEK, which only group members hold, and since C3 the
# node authenticates itself in the handshake and is pinned by the client. A
# signature per chunk re-proved, once per megabyte, what the session established
-# once. (`sign_chunk` still exists in `crypto.py` — it signs the serialized index
-# envelope, which is a different artifact; see `indexer/group_index.py`.)
+# once.
def chunk_ciphertext(
diff --git a/packages/meshbay-common/src/meshbay_common/webcrypto.py b/packages/meshbay-common/src/meshbay_common/webcrypto.py
index 3bd5ae6..7119e2e 100644
--- a/packages/meshbay-common/src/meshbay_common/webcrypto.py
+++ b/packages/meshbay-common/src/meshbay_common/webcrypto.py
@@ -1,28 +1,21 @@
"""
-MeshBay — AES-256-GCM cipher variant for browser-accessible groups.
+MeshBay — the content cipher: AES-256-GCM, per-chunk keys derived from the GEK.
-The ChaCha20-Poly1305 GEK used in MNP (TCP+TLS and QUIC transport)
-is NOT available in the WebCrypto API. For groups whose content must
-be decryptable by a web browser (using SubtleCrypto), an AES-256-GCM
-variant is used instead.
-
-The GEK wrapping (X25519 + HKDF) is identical — only the content
-cipher changes. The hub stores and distributes GEK bundles the same way.
-
-Cipher selection is declared per-group in the hub registry:
- "cipher": "chacha20-poly1305" (default, native clients)
- "cipher": "aes-256-gcm" (browser-compatible groups)
+AES-GCM because it is what WebCrypto offers, and one cipher serves every client:
+the browser, the desktop client (the same engine) and the Python side here.
Python side (this module):
- encrypt_chunk_aes / decrypt_chunk_aes
+ chunk_key_aes / encrypt_chunk_aes / decrypt_chunk_aes
JavaScript side (in static/crypto.js):
- Uses SubtleCrypto.importKey + SubtleCrypto.decrypt with AES-GCM.
+ SubtleCrypto.importKey + SubtleCrypto.decrypt with AES-GCM.
-Key derivation for AES variant — same HKDF info string with suffix:
+Key derivation:
info = b"file:" + file_hash + b":chunk:" + chunk_index + b":aes"
-This ensures AES and ChaCha20 keys are always distinct even from the same GEK.
+The `:aes` suffix dates from a ChaCha20-Poly1305 variant derived from the same
+GEK without it, which nothing used and which is gone. It stays: it is part of
+every chunk key in existence, and changing it would change them all.
"""
import os
@@ -33,7 +26,7 @@ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
def chunk_key_aes(gek: bytes, file_hash: bytes, chunk_index: int) -> bytes:
- """Derive a per-chunk AES-256 key. Distinct from ChaCha20 key."""
+ """Derive a per-chunk AES-256 key from the GEK."""
return HKDF(
algorithm=hashes.SHA256(), length=32, salt=None,
info=b"file:" + file_hash + b":chunk:" + chunk_index.to_bytes(4, "big") + b":aes",