aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/meshbay-common/pyproject.toml25
-rw-r--r--packages/meshbay-common/src/meshbay_common/__init__.py5
-rw-r--r--packages/meshbay-common/src/meshbay_common/crypto.py170
-rw-r--r--packages/meshbay-common/src/meshbay_common/protocol.py72
-rw-r--r--packages/meshbay-hub/pyproject.toml32
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/__init__.py3
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/__init__.py0
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/app.py31
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/daemon.py17
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/__init__.py0
-rw-r--r--packages/meshbay-hub/tests/__init__.py0
-rw-r--r--packages/meshbay-node/pyproject.toml34
-rw-r--r--packages/meshbay-node/src/meshbay_node/__init__.py3
-rw-r--r--packages/meshbay-node/src/meshbay_node/daemon.py9
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/__init__.py0
-rw-r--r--packages/meshbay-node/src/meshbay_node/modules/__init__.py0
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/__init__.py0
-rw-r--r--packages/meshbay-node/src/meshbay_node/ui/__init__.py0
-rw-r--r--packages/meshbay-node/tests/__init__.py0
19 files changed, 401 insertions, 0 deletions
diff --git a/packages/meshbay-common/pyproject.toml b/packages/meshbay-common/pyproject.toml
new file mode 100644
index 0000000..ac7ba08
--- /dev/null
+++ b/packages/meshbay-common/pyproject.toml
@@ -0,0 +1,25 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "meshbay-common"
+version = "0.1.0"
+description = "MeshBay shared cryptographic primitives and protocol types"
+requires-python = ">=3.12"
+dependencies = [
+ "cryptography>=43.0",
+ "PyJWT>=2.9",
+ "blake3>=1.0",
+ "msgpack>=1.1",
+ "zstandard>=0.23",
+]
+
+[project.optional-dependencies]
+dev = ["pytest>=8", "pytest-asyncio>=0.24", "ruff>=0.6"]
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/meshbay_common"]
+
+# RPM/DEB: python3-meshbay-common
+# Dependency of both meshbay-hub and meshbay-node
diff --git a/packages/meshbay-common/src/meshbay_common/__init__.py b/packages/meshbay-common/src/meshbay_common/__init__.py
new file mode 100644
index 0000000..94b3d27
--- /dev/null
+++ b/packages/meshbay-common/src/meshbay_common/__init__.py
@@ -0,0 +1,5 @@
+"""MeshBay common — shared crypto primitives and protocol types."""
+
+__version__ = "0.1.0"
+MNP_VERSION = "0.1"
+MHP_VERSION = "0.1"
diff --git a/packages/meshbay-common/src/meshbay_common/crypto.py b/packages/meshbay-common/src/meshbay_common/crypto.py
new file mode 100644
index 0000000..2104469
--- /dev/null
+++ b/packages/meshbay-common/src/meshbay_common/crypto.py
@@ -0,0 +1,170 @@
+"""
+MeshBay cryptographic primitives.
+
+Validated in Spike 1 and Spike 6 of the POC.
+All operations use PyCA cryptography (OpenSSL-backed, hardware-accelerated).
+"""
+
+import os
+import base64
+
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
+from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
+from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
+from cryptography.hazmat.primitives.kdf.hkdf import HKDF
+from cryptography.hazmat.primitives.kdf.argon2 import Argon2id
+from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+from cryptography.hazmat.primitives import hashes, serialization
+import blake3
+
+# ── Key serialisation helpers ─────────────────────────────────────────────────
+
+def sk_to_raw(sk: Ed25519PrivateKey | X25519PrivateKey) -> bytes:
+ return sk.private_bytes(
+ serialization.Encoding.Raw,
+ serialization.PrivateFormat.Raw,
+ serialization.NoEncryption(),
+ )
+
+def pk_to_raw(pk: Ed25519PublicKey | X25519PublicKey) -> bytes:
+ return pk.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
+
+def pk_to_b64(pk: Ed25519PublicKey | X25519PublicKey) -> str:
+ return base64.b64encode(pk_to_raw(pk)).decode()
+
+def sk_to_b64(sk: Ed25519PrivateKey | X25519PrivateKey) -> str:
+ return base64.b64encode(sk_to_raw(sk)).decode()
+
+# ── GEK (Group Encryption Key) ────────────────────────────────────────────────
+
+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):
+ import pathlib
+ data = pathlib.Path(path_or_bytes).read_bytes()
+ elif isinstance(path_or_bytes, bytes):
+ data = path_or_bytes
+ else:
+ data = path_or_bytes.read_bytes()
+ return blake3.blake3(data).digest()
+
+# ── 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)
+
+# ── Keystore (local key storage) ──────────────────────────────────────────────
+
+# Argon2id parameters — calibrate to ~500ms on target hardware before production.
+# POC measured 78ms with these; increase memory_cost to 262144 (256MB) for prod.
+ARGON2_ITERATIONS = 3
+ARGON2_MEMORY_COST = 65536 # 64 MB — increase to 262144 for production
+ARGON2_LANES = 4
+ARGON2_KEY_LENGTH = 32
+
+def derive_keystore_key(password: str, salt: bytes) -> bytes:
+ """Derive AES-256 key from password using Argon2id."""
+ return Argon2id(
+ salt=salt,
+ length=ARGON2_KEY_LENGTH,
+ iterations=ARGON2_ITERATIONS,
+ lanes=ARGON2_LANES,
+ memory_cost=ARGON2_MEMORY_COST,
+ ).derive(password.encode())
+
+def encrypt_keystore(plaintext: bytes, key: bytes) -> tuple[bytes, bytes, bytes]:
+ """Encrypt keystore blob with AES-256-GCM. Returns (iv, ciphertext, tag)."""
+ iv = os.urandom(16)
+ enc = Cipher(algorithms.AES(key), modes.GCM(iv)).encryptor()
+ ct = enc.update(plaintext) + enc.finalize()
+ return iv, ct, enc.tag
+
+def decrypt_keystore(iv: bytes, ciphertext: bytes, tag: bytes, key: bytes) -> bytes:
+ """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."""
+ 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/protocol.py b/packages/meshbay-common/src/meshbay_common/protocol.py
new file mode 100644
index 0000000..c3f2b1a
--- /dev/null
+++ b/packages/meshbay-common/src/meshbay_common/protocol.py
@@ -0,0 +1,72 @@
+"""
+MeshBay protocol constants and message type definitions.
+
+MNP (Mesh Node Protocol) — v0.1
+MHP (Mesh Bay Hub Protocol) — v0.1
+
+All wire messages are length-prefixed msgpack (4-byte big-endian length header).
+Every message carries a "v" field for protocol version.
+"""
+
+from dataclasses import dataclass, field
+from typing import Any
+
+MNP_VERSION = "0.1"
+MHP_VERSION = "0.1"
+
+
+# ── MNP message types ─────────────────────────────────────────────────────────
+
+class MNP:
+ HANDSHAKE = "handshake"
+ HANDSHAKE_ACK = "handshake_ack"
+ INDEX_SYNC = "index_sync" # full Mesh Group Index
+ INDEX_DELTA = "index_delta" # incremental update
+ FILE_REQUEST = "file_req" # request chunk(s)
+ FILE_CHUNK = "file_chunk" # encrypted chunk response
+ STREAM_SEGMENT = "stream_seg" # HLS/DASH segment
+ CHAT_MESSAGE = "chat_msg" # Double Ratchet message
+ CHAT_ATTACHMENT = "chat_attach" # attachment metadata
+ EPHEMERAL_STREAM = "ephemeral_stream" # reserved — mobile live push
+
+
+# ── Index entry ───────────────────────────────────────────────────────────────
+
+@dataclass
+class IndexEntry:
+ id: str # blake3 hash of file (hex)
+ name: str # filename
+ path: str # path relative to shared directory
+ size: int # bytes
+ type: str # video | audio | image | document | archive | other
+ added_at: int # unix timestamp
+ duration: int | None = None # seconds, for media
+ thumb_hash: str | None = None # blake3 of thumbnail
+
+
+@dataclass
+class IndexDelta:
+ base_version: int
+ version: int
+ additions: list[IndexEntry] = field(default_factory=list)
+ deletions: list[str] = field(default_factory=list) # list of ids
+
+
+# ── Chunk request/response ────────────────────────────────────────────────────
+
+@dataclass
+class ChunkRequest:
+ file_id: str # blake3 hash of file (hex)
+ chunk_index: int
+
+@dataclass
+class ChunkResponse:
+ chunk_index: int
+ plaintext_size: int
+ nonce_b64: str
+ ct_b64: str
+ ct_hash_b64: str
+ pt_hash_b64: str
+ sig_b64: str
+ pk_node_b64: str
+ file_hash_b64: str
diff --git a/packages/meshbay-hub/pyproject.toml b/packages/meshbay-hub/pyproject.toml
new file mode 100644
index 0000000..e0a880a
--- /dev/null
+++ b/packages/meshbay-hub/pyproject.toml
@@ -0,0 +1,32 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "meshbay-hub"
+version = "0.1.0"
+description = "MeshBay Hub — identity authority and group registry server"
+requires-python = ">=3.12"
+dependencies = [
+ "meshbay-common>=0.1.0",
+ "fastapi>=0.115",
+ "uvicorn[standard]>=0.30",
+ "sqlalchemy>=2.0",
+ "alembic>=1.13",
+ "asyncpg>=0.30", # PostgreSQL async driver
+ "httpx>=0.28",
+]
+
+[project.optional-dependencies]
+dev = ["pytest>=8", "pytest-asyncio>=0.24", "ruff>=0.6", "httpx>=0.28"]
+
+[project.scripts]
+meshbay-hub = "meshbay_hub.daemon:main"
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/meshbay_hub"]
+
+# RPM/DEB: meshbay-hub
+# Systemd service: meshbay-hub.service
+# Config: /etc/meshbay/hub.conf
+# Data: /var/lib/meshbay/hub/
diff --git a/packages/meshbay-hub/src/meshbay_hub/__init__.py b/packages/meshbay-hub/src/meshbay_hub/__init__.py
new file mode 100644
index 0000000..6372557
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/__init__.py
@@ -0,0 +1,3 @@
+"""MeshBay Hub — identity authority and group registry."""
+
+__version__ = "0.1.0"
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/__init__.py b/packages/meshbay-hub/src/meshbay_hub/api/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/api/__init__.py
diff --git a/packages/meshbay-hub/src/meshbay_hub/app.py b/packages/meshbay-hub/src/meshbay_hub/app.py
new file mode 100644
index 0000000..3b9ee4f
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/app.py
@@ -0,0 +1,31 @@
+"""
+MeshBay Hub — FastAPI application.
+
+POC endpoints are implemented in this file (in-memory storage).
+Production implementation will use db/ with SQLAlchemy + PostgreSQL.
+"""
+
+from fastapi import FastAPI
+from meshbay_hub import __version__
+from meshbay_common import MNP_VERSION, MHP_VERSION
+
+app = FastAPI(
+ title="MeshBay Hub",
+ version=__version__,
+ description="MeshBay identity authority and group registry",
+)
+
+# TODO: import and include routers from api/ submodules
+# from meshbay_hub.api import users, nodes, groups
+# app.include_router(users.router, prefix="/v1")
+# app.include_router(nodes.router, prefix="/v1")
+# app.include_router(groups.router, prefix="/v1")
+
+
+@app.get("/v1/hub/info")
+async def hub_info() -> dict:
+ return {
+ "hub_id": "meshbay.org",
+ "mnp_version": MNP_VERSION,
+ "mhp_version": MHP_VERSION,
+ }
diff --git a/packages/meshbay-hub/src/meshbay_hub/daemon.py b/packages/meshbay-hub/src/meshbay_hub/daemon.py
new file mode 100644
index 0000000..ff8158a
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/daemon.py
@@ -0,0 +1,17 @@
+"""Entry point for the meshbay-hub systemd service."""
+
+import uvicorn
+from meshbay_hub.app import app # noqa: F401
+
+
+def main() -> None:
+ uvicorn.run(
+ "meshbay_hub.app:app",
+ host="127.0.0.1",
+ port=8000,
+ log_level="info",
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/__init__.py b/packages/meshbay-hub/src/meshbay_hub/db/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/db/__init__.py
diff --git a/packages/meshbay-hub/tests/__init__.py b/packages/meshbay-hub/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/packages/meshbay-hub/tests/__init__.py
diff --git a/packages/meshbay-node/pyproject.toml b/packages/meshbay-node/pyproject.toml
new file mode 100644
index 0000000..48231ad
--- /dev/null
+++ b/packages/meshbay-node/pyproject.toml
@@ -0,0 +1,34 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "meshbay-node"
+version = "0.1.0"
+description = "MeshBay Node — local file host, streaming server, and group daemon"
+requires-python = ">=3.12"
+dependencies = [
+ "meshbay-common>=0.1.0",
+ "fastapi>=0.115", # local web UI on localhost:18000
+ "uvicorn[standard]>=0.30",
+ "httpx>=0.28", # hub client
+ "watchdog>=5.0", # directory monitoring
+ "aioice>=0.9", # ICE/STUN for NAT traversal
+ # aioquic>=1.0 added in v2 (QUIC transport)
+]
+
+[project.optional-dependencies]
+dev = ["pytest>=8", "pytest-asyncio>=0.24", "ruff>=0.6"]
+
+[project.scripts]
+meshbay-node = "meshbay_node.daemon:main"
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/meshbay_node"]
+
+# RPM/DEB: meshbay-node
+# Systemd service: meshbay-node.service
+# Config: /etc/meshbay/node.conf (or ~/.config/meshbay/node.conf per-user)
+# Data: ~/.local/share/meshbay/node/
+# Keystore: ~/.config/meshbay/keystore.enc
+# Local UI: http://localhost:18000
diff --git a/packages/meshbay-node/src/meshbay_node/__init__.py b/packages/meshbay-node/src/meshbay_node/__init__.py
new file mode 100644
index 0000000..91f6593
--- /dev/null
+++ b/packages/meshbay-node/src/meshbay_node/__init__.py
@@ -0,0 +1,3 @@
+"""MeshBay Node — local file host, streaming server, and group daemon."""
+
+__version__ = "0.1.0"
diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py
new file mode 100644
index 0000000..83c0ed7
--- /dev/null
+++ b/packages/meshbay-node/src/meshbay_node/daemon.py
@@ -0,0 +1,9 @@
+"""Entry point for the meshbay-node systemd service."""
+
+
+def main() -> None:
+ raise NotImplementedError("Node daemon not yet implemented — see Phase 2")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/packages/meshbay-node/src/meshbay_node/indexer/__init__.py b/packages/meshbay-node/src/meshbay_node/indexer/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/packages/meshbay-node/src/meshbay_node/indexer/__init__.py
diff --git a/packages/meshbay-node/src/meshbay_node/modules/__init__.py b/packages/meshbay-node/src/meshbay_node/modules/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/packages/meshbay-node/src/meshbay_node/modules/__init__.py
diff --git a/packages/meshbay-node/src/meshbay_node/transport/__init__.py b/packages/meshbay-node/src/meshbay_node/transport/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/packages/meshbay-node/src/meshbay_node/transport/__init__.py
diff --git a/packages/meshbay-node/src/meshbay_node/ui/__init__.py b/packages/meshbay-node/src/meshbay_node/ui/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/packages/meshbay-node/src/meshbay_node/ui/__init__.py
diff --git a/packages/meshbay-node/tests/__init__.py b/packages/meshbay-node/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/packages/meshbay-node/tests/__init__.py