diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/db')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e5a2b7d31f88_user_device_auth_keys.py | 47 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/db/models.py | 35 |
2 files changed, 82 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e5a2b7d31f88_user_device_auth_keys.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e5a2b7d31f88_user_device_auth_keys.py new file mode 100644 index 0000000..ddada15 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e5a2b7d31f88_user_device_auth_keys.py @@ -0,0 +1,47 @@ +"""user device auth keys + +Device Ed25519 authentication to the hub, so a client signs in without +re-deriving a key from the passphrase every time. + +Deliberately **not** the key directory that was H3: nothing reads this but the +hub itself, nobody wraps a group key for it, and it is a different key from the +per-node identity keys, which never leave the device-node relationship. See +`docs/desktop-client-v1.md` ยง5. + +`create_all()` is not a migration โ it creates missing tables and never a +missing column, so a schema change without a file here reaches the tests (fresh +DB every run) and never reaches the deployed hub. + +Revision ID: e5a2b7d31f88 +Revises: 3dc91cd4ea52 +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "e5a2b7d31f88" +down_revision: Union[str, Sequence[str], None] = "3dc91cd4ea52" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "user_devices", + sa.Column("id", sa.String(36), primary_key=True), + sa.Column("user_id", sa.String(36), sa.ForeignKey("users.id"), + nullable=False), + sa.Column("pk_auth_ed25519", sa.String(64), nullable=False, unique=True), + sa.Column("label", sa.String(64), nullable=False, server_default=""), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, + server_default=sa.func.now()), + sa.Column("last_seen", sa.DateTime(timezone=True), nullable=True), + ) + op.create_index("ix_user_devices_user", "user_devices", ["user_id"]) + + +def downgrade() -> None: + op.drop_index("ix_user_devices_user", table_name="user_devices") + op.drop_table("user_devices") diff --git a/packages/meshbay-hub/src/meshbay_hub/db/models.py b/packages/meshbay-hub/src/meshbay_hub/db/models.py index a220a64..12507a6 100644 --- a/packages/meshbay-hub/src/meshbay_hub/db/models.py +++ b/packages/meshbay-hub/src/meshbay_hub/db/models.py @@ -178,6 +178,41 @@ class FederatedGroup(Base): ) +class UserDevice(Base): + """ + A device's key for authenticating **to the hub**, and nothing else. + + This is not a reintroduction of the key directory that was H3, and the + distinction is worth being precise about because it looks like one: + + * **Nobody reads this but the hub.** No endpoint publishes it, nothing + wraps a group key for it, and no node ever asks for it. H3 was a + directory *others* read from, where a substituted key was handed the + GEK by an honest member. + * **It is not a node identity key.** Those are generated per node, pinned + there, and never leave that relationship (`docs/per-node-identity-v1.md`). + A device holds one of these *plus* a different key per node, so nothing + here correlates a person across operators. + + What it does cost, stated plainly: the hub now knows how many devices an + account has and when each one last signed in. That is new metadata, and it + is the price of not deriving a key from the passphrase on every sign-in. + """ + + __tablename__ = "user_devices" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid) + user_id: Mapped[str] = mapped_column(ForeignKey("users.id"), nullable=False) + # base64 raw Ed25519, unique so one device key belongs to one account + pk_auth_ed25519: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) + label: Mapped[str] = mapped_column(String(64), default="") + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now) + last_seen: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True) + + __table_args__ = (Index("ix_user_devices_user", "user_id"),) + + class SwarmSource(Base): """ Tracks which nodes can serve a given content hash (public swarm). |