aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/db
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-hub/src/meshbay_hub/db
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-hub/src/meshbay_hub/db')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/__init__.py4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/2041a4060b3c_add_keypair_bundle_federated_groups_.py2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d28b9caf9f07_initial_schema.py12
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/models.py25
4 files changed, 5 insertions, 38 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/__init__.py b/packages/meshbay-hub/src/meshbay_hub/db/__init__.py
index 5ef1d3c..62e5388 100644
--- a/packages/meshbay-hub/src/meshbay_hub/db/__init__.py
+++ b/packages/meshbay-hub/src/meshbay_hub/db/__init__.py
@@ -1,9 +1,9 @@
"""Hub database layer."""
from .engine import init_db, close_db, get_db
-from .models import Base, User, Node, Group, GroupMember, GEKBundle, RefreshToken, IPLog
+from .models import Base, User, Node, Group, GroupMember, RefreshToken, IPLog
__all__ = [
"init_db", "close_db", "get_db",
"Base", "User", "Node", "Group", "GroupMember",
- "GEKBundle", "RefreshToken", "IPLog",
+ "RefreshToken", "IPLog",
]
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/2041a4060b3c_add_keypair_bundle_federated_groups_.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/2041a4060b3c_add_keypair_bundle_federated_groups_.py
index 779efdc..a59a3d1 100644
--- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/2041a4060b3c_add_keypair_bundle_federated_groups_.py
+++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/2041a4060b3c_add_keypair_bundle_federated_groups_.py
@@ -63,14 +63,12 @@ def upgrade() -> None:
)
op.create_index('ix_content_reports_group', 'content_reports', ['group_id'], unique=False)
op.create_index('ix_content_reports_hash', 'content_reports', ['content_hash'], unique=False)
- op.add_column('users', sa.Column('keypair_bundle', sa.Text(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
- op.drop_column('users', 'keypair_bundle')
op.drop_index('ix_content_reports_hash', table_name='content_reports')
op.drop_index('ix_content_reports_group', table_name='content_reports')
op.drop_table('content_reports')
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d28b9caf9f07_initial_schema.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d28b9caf9f07_initial_schema.py
index d4a9aa6..5192eb8 100644
--- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d28b9caf9f07_initial_schema.py
+++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d28b9caf9f07_initial_schema.py
@@ -84,17 +84,6 @@ def upgrade() -> None:
sa.UniqueConstraint('token_hash')
)
op.create_index('ix_refresh_tokens_hash', 'refresh_tokens', ['token_hash'], unique=False)
- op.create_table('gek_bundles',
- sa.Column('group_id', sa.String(length=36), nullable=False),
- sa.Column('user_id', sa.String(length=36), nullable=False),
- sa.Column('pk_eph_b64', sa.String(length=64), nullable=False),
- sa.Column('nonce_b64', sa.String(length=32), nullable=False),
- sa.Column('wrapped_b64', sa.String(length=128), nullable=False),
- sa.Column('stored_at', sa.DateTime(timezone=True), nullable=False),
- sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ),
- sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
- sa.PrimaryKeyConstraint('group_id', 'user_id')
- )
op.create_table('group_members',
sa.Column('group_id', sa.String(length=36), nullable=False),
sa.Column('user_id', sa.String(length=36), nullable=False),
@@ -110,7 +99,6 @@ def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('group_members')
- op.drop_table('gek_bundles')
op.drop_index('ix_refresh_tokens_hash', table_name='refresh_tokens')
op.drop_table('refresh_tokens')
op.drop_index('ix_nodes_user_id', table_name='nodes')
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/models.py b/packages/meshbay-hub/src/meshbay_hub/db/models.py
index c420661..cdebd3c 100644
--- a/packages/meshbay-hub/src/meshbay_hub/db/models.py
+++ b/packages/meshbay-hub/src/meshbay_hub/db/models.py
@@ -6,7 +6,6 @@ Tables:
nodes — node announcements
groups — group registry
group_members — group membership
- gek_bundles — encrypted GEK per (group, user)
refresh_tokens — hashed refresh tokens
ip_logs — connection log for legal compliance (1-year retention)
"""
@@ -45,15 +44,14 @@ class User(Base):
pw_version: Mapped[int] = mapped_column(Integer, default=1)
pk_ed25519: Mapped[str] = mapped_column(String(64), nullable=False) # base64 raw 32B
pk_x25519: Mapped[str] = mapped_column(String(64), nullable=False) # base64 raw 32B
+ pk_node_ed25519: Mapped[str | None] = mapped_column(String(64), nullable=True) # node daemon key
hub_id: Mapped[str] = mapped_column(String(128), nullable=False)
- keypair_bundle: Mapped[str | None] = mapped_column(Text) # AES-GCM encrypted, web clients only
role: Mapped[str] = mapped_column(String(16), default="user") # user|moderator|admin
status: Mapped[str] = mapped_column(String(16), default="active") # active|suspended|revoked
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
nodes: Mapped[list["Node"]] = relationship(back_populates="user")
group_memberships: Mapped[list["GroupMember"]] = relationship(back_populates="user")
- gek_bundles: Mapped[list["GEKBundle"]] = relationship(back_populates="user")
refresh_tokens: Mapped[list["RefreshToken"]] = relationship(back_populates="user")
ip_logs: Mapped[list["IPLog"]] = relationship(back_populates="user")
@@ -89,11 +87,11 @@ class Group(Base):
admin_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False)
visibility: Mapped[str] = mapped_column(String(16), default="private") # public|private
join_policy: Mapped[str] = mapped_column(String(16), default="invite") # open|request|invite
- status: Mapped[str] = mapped_column(String(16), default="active") # active|revoked
+ description: Mapped[str | None] = mapped_column(String(512))
+ status: Mapped[str] = mapped_column(String(16), default="active") # active|suspended|revoked
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
members: Mapped[list["GroupMember"]] = relationship(back_populates="group")
- gek_bundles: Mapped[list["GEKBundle"]] = relationship(back_populates="group")
__table_args__ = (Index("ix_groups_name", "name"),)
@@ -109,23 +107,6 @@ class GroupMember(Base):
user: Mapped["User"] = relationship(back_populates="group_memberships")
-# ── GEK bundles ───────────────────────────────────────────────────────────────
-
-class GEKBundle(Base):
- """Encrypted GEK bundle — opaque to the hub (hub cannot decrypt it)."""
- __tablename__ = "gek_bundles"
-
- group_id: Mapped[str] = mapped_column(ForeignKey("groups.id"), primary_key=True)
- user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), primary_key=True)
- pk_eph_b64: Mapped[str] = mapped_column(String(64), nullable=False)
- nonce_b64: Mapped[str] = mapped_column(String(32), nullable=False)
- wrapped_b64: Mapped[str] = mapped_column(String(128), nullable=False)
- stored_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
-
- group: Mapped["Group"] = relationship(back_populates="gek_bundles")
- user: Mapped["User"] = relationship(back_populates="gek_bundles")
-
-
# ── Refresh tokens ────────────────────────────────────────────────────────────
class RefreshToken(Base):