diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-11 14:55:52 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-11 14:55:52 +0200 |
| commit | cc90dc943fcd0e7bbf52674fb3f95ff097026f4a (patch) | |
| tree | 6315a5e966c027c50b92d59e60c4b506a418143e /packages/meshbay-common/src/meshbay_common/crypto.py | |
| parent | edde9e441fb6b84e9d56215d6e2a8d9338b8f962 (diff) | |
| download | meshbay-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/crypto.py')
| -rw-r--r-- | packages/meshbay-common/src/meshbay_common/crypto.py | 42 |
1 files changed, 42 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. |