diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-13 03:56:30 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-13 03:56:30 +0200 |
| commit | f0248975908ad670fa8a820f865bf22ea8d0172d (patch) | |
| tree | f4af64d36cacaccb4f6d13436e001aeb57e861e3 /packages/meshbay-node/src/meshbay_node/bundle_store.py | |
| parent | 35130e5528a52161630fd1c93572e1b2b7cd911b (diff) | |
| download | meshbay-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.py | 105 |
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 |