aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/bundle_store.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-13 03:56:30 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-13 03:56:30 +0200
commitf0248975908ad670fa8a820f865bf22ea8d0172d (patch)
treef4af64d36cacaccb4f6d13436e001aeb57e861e3 /packages/meshbay-node/src/meshbay_node/bundle_store.py
parent35130e5528a52161630fd1c93572e1b2b7cd911b (diff)
downloadmeshbay-f0248975908ad670fa8a820f865bf22ea8d0172d.tar.gz
feat: Phase 12 — P2P crypto material, password split, node Ed25519 auth
Baseline commit capturing in-progress Phase 12 work that was already present in the working tree (uncommitted) before the Phase 11.5 security remediation begins. Committed as-is, without review or modification, so that remediation changes arrive as a separable diff. Contents: BundleStore (P2P GEK + keypair bundles), password split (auth_key / bundle_key), node Ed25519 auth (POST /v1/nodes/auth, node-scoped JWT), GEK-HMAC handshake proof with DTLS channel binding, Ed25519 admin challenge-response, node local admin UI rewrite, browser key persistence. Not authored in this session — captured to establish a baseline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/bundle_store.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/bundle_store.py105
1 files changed, 105 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/bundle_store.py b/packages/meshbay-node/src/meshbay_node/bundle_store.py
new file mode 100644
index 0000000..e7c6981
--- /dev/null
+++ b/packages/meshbay-node/src/meshbay_node/bundle_store.py
@@ -0,0 +1,105 @@
+"""
+Bundle store — SQLite-backed storage for GEK bundles and keypair bundles.
+
+GEK bundles: ECIES-wrapped GEK targeted at a specific user's X25519 key.
+Keypair bundles: AES-GCM encrypted (Ed25519 + X25519) private keys, encrypted
+with the user's password-derived bundle_key. Opaque to the node.
+
+Both are stored and served over the P2P DataChannel during MNP handshake.
+"""
+
+import logging
+from pathlib import Path
+
+import aiosqlite
+
+log = logging.getLogger(__name__)
+
+_SCHEMA_GEK = """\
+CREATE TABLE IF NOT EXISTS gek_bundles (
+ group_id TEXT NOT NULL,
+ user_id TEXT NOT NULL,
+ pk_eph_b64 TEXT NOT NULL,
+ nonce_b64 TEXT NOT NULL,
+ wrapped_b64 TEXT NOT NULL,
+ stored_at TEXT NOT NULL DEFAULT (datetime('now')),
+ PRIMARY KEY (group_id, user_id)
+);
+"""
+
+_SCHEMA_KEYPAIR = """\
+CREATE TABLE IF NOT EXISTS keypair_bundles (
+ user_id TEXT PRIMARY KEY,
+ bundle_enc TEXT NOT NULL,
+ stored_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+"""
+
+
+class BundleStore:
+ def __init__(self, db_path: Path):
+ self._db_path = db_path
+ self._db: aiosqlite.Connection | None = None
+
+ async def open(self) -> None:
+ self._db_path.parent.mkdir(parents=True, exist_ok=True)
+ self._db = await aiosqlite.connect(str(self._db_path))
+ await self._db.execute(_SCHEMA_GEK)
+ await self._db.execute(_SCHEMA_KEYPAIR)
+ await self._db.commit()
+
+ async def store(
+ self,
+ group_id: str,
+ user_id: str,
+ pk_eph_b64: str,
+ nonce_b64: str,
+ wrapped_b64: str,
+ ) -> None:
+ assert self._db
+ await self._db.execute(
+ "INSERT OR REPLACE INTO gek_bundles "
+ "(group_id, user_id, pk_eph_b64, nonce_b64, wrapped_b64, stored_at) "
+ "VALUES (?, ?, ?, ?, ?, datetime('now'))",
+ (group_id, user_id, pk_eph_b64, nonce_b64, wrapped_b64),
+ )
+ await self._db.commit()
+
+ async def fetch(self, group_id: str, user_id: str) -> dict | None:
+ assert self._db
+ async with self._db.execute(
+ "SELECT pk_eph_b64, nonce_b64, wrapped_b64 FROM gek_bundles "
+ "WHERE group_id = ? AND user_id = ?",
+ (group_id, user_id),
+ ) as cursor:
+ row = await cursor.fetchone()
+ if not row:
+ return None
+ return {
+ "pk_eph_b64": row[0],
+ "nonce_b64": row[1],
+ "wrapped_b64": row[2],
+ }
+
+ async def store_keypair(self, user_id: str, bundle_enc: str) -> None:
+ assert self._db
+ await self._db.execute(
+ "INSERT OR REPLACE INTO keypair_bundles "
+ "(user_id, bundle_enc, stored_at) VALUES (?, ?, datetime('now'))",
+ (user_id, bundle_enc),
+ )
+ await self._db.commit()
+
+ async def fetch_keypair(self, user_id: str) -> str | None:
+ assert self._db
+ async with self._db.execute(
+ "SELECT bundle_enc FROM keypair_bundles WHERE user_id = ?",
+ (user_id,),
+ ) as cursor:
+ row = await cursor.fetchone()
+ return row[0] if row else None
+
+ async def close(self) -> None:
+ if self._db:
+ await self._db.close()
+ self._db = None