summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common/src/meshbay_common
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-11 14:55:52 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-11 14:55:52 +0200
commitcc90dc943fcd0e7bbf52674fb3f95ff097026f4a (patch)
tree6315a5e966c027c50b92d59e60c4b506a418143e /packages/meshbay-common/src/meshbay_common
parentedde9e441fb6b84e9d56215d6e2a8d9338b8f962 (diff)
downloadmeshbay-cc90dc943fcd0e7bbf52674fb3f95ff097026f4a.tar.gz
feat: Phase 10b — Self-service UI (group create/join, upload, IndexedDB, search)
Six self-service features for the web SPA: - Group creation UI with GEK auto-generation (AES-256-GCM ECIES) - Member management + invite by username (GEK wrapping for invitee) - Open group self-join flow (POST /v1/groups/{id}/join) - File upload client→node (FILE_UPLOAD MNP type, .uploads/ staging) - IndexedDB caching of group file indexes (instant display on revisit) - Cross-group file search (SearchPage, pure client-side on cached indexes) 11 new tests (166 total): 8 group self-service + 3 AES GEK wrap/unwrap. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-common/src/meshbay_common')
-rw-r--r--packages/meshbay-common/src/meshbay_common/crypto.py42
-rw-r--r--packages/meshbay-common/src/meshbay_common/protocol.py2
2 files changed, 44 insertions, 0 deletions
diff --git a/packages/meshbay-common/src/meshbay_common/crypto.py b/packages/meshbay-common/src/meshbay_common/crypto.py
index 6066f5f..682e1c0 100644
--- a/packages/meshbay-common/src/meshbay_common/crypto.py
+++ b/packages/meshbay-common/src/meshbay_common/crypto.py
@@ -124,6 +124,48 @@ def unwrap_gek(bundle: dict, sk_recipient: bytes, pk_recipient: bytes) -> bytes:
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:
+ """ECIES wrap using AES-256-GCM — compatible with browser WebCrypto."""
+ 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_AES,
+ ).derive(shared)
+
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
+ nonce = os.urandom(12)
+ wrapped = AESGCM(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_aes(bundle: dict, sk_recipient: bytes, pk_recipient: bytes) -> bytes:
+ """Unwrap a GEK bundle created by browser (AES-256-GCM ECIES)."""
+ 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_AES,
+ ).derive(shared)
+
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
+ return AESGCM(wrap_key).decrypt(nonce, wrapped, pk_recipient)
+
+
# ── Keystore (local key storage) ──────────────────────────────────────────────
# Argon2id parameters — calibrate to ~500ms on target hardware before production.
diff --git a/packages/meshbay-common/src/meshbay_common/protocol.py b/packages/meshbay-common/src/meshbay_common/protocol.py
index fbf871a..8da3ea0 100644
--- a/packages/meshbay-common/src/meshbay_common/protocol.py
+++ b/packages/meshbay-common/src/meshbay_common/protocol.py
@@ -31,6 +31,8 @@ class MNP:
CHAT_HISTORY_RESPONSE = "chat_hist_resp" # history response with messages
GEK_REQUEST = "gek_req" # browser requests group GEK
GEK_RESPONSE = "gek_resp" # node delivers GEK over secure channel
+ FILE_UPLOAD = "file_upload" # client pushes file chunk to node
+ FILE_UPLOAD_ACK = "file_upload_ack" # node acknowledges chunk receipt
EPHEMERAL_STREAM = "ephemeral_stream" # reserved — mobile live push