aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CLAUDE.md39
-rw-r--r--devel-phases.md6
-rw-r--r--docs/USERGUIDE.md39
-rw-r--r--docs/meshbay-draft-v3.md33
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/2041a4060b3c_add_keypair_bundle_federated_groups_.py83
5 files changed, 183 insertions, 17 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 3f05a06..306cad3 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -14,9 +14,18 @@ meshbay/
│ ├── meshbay-hub/ # Hub server (FastAPI + PostgreSQL) — meshbay-hub RPM
│ └── meshbay-node/ # Node daemon + local UI — meshbay-node RPM
├── poc/ # POC spike scripts (reference, not production)
-└── docs/ # Architecture drafts and POC plans
+├── docs/ # Architecture drafts and POC plans
+├── packaging/ # RPM spec files, DEB control files, systemd units
+└── QE/ # NOT versioned (.gitignore) — test artefacts, credentials, demos
+ ├── demo-v1/ # Scripts démo opérationnels (setup_demo.py, run_node.py, download.py)
+ ├── spikes/ # Expérimentations futures (remplace ~/draft/)
+ └── server-state/ # Inventaire de ce qui tourne sur meshbay.org
```
+**Règle QE/** : tout test sur meshbay.org doit ouvrir le port UFW, tester, et
+fermer le port + tuer les processus dans le MÊME bloc de commandes.
+Jamais de processus orphelins ni de ports ouverts après un test.
+
## Python environment
- **Minimum Python:** 3.12
@@ -75,20 +84,34 @@ Scope: `hub`, `node`, `common`, or omitted for cross-cutting
## Security rules
- **Never commit private keys** (hub_private.pem, *.key, unlock.key, keystore.enc)
-- **Never commit node_state.json or bob_state.json** (contain private key material)
+- **Never commit QE/** — credentials, test keys, demo data go there
- **Never log GEK, private keys, or plaintext passwords** — even at DEBUG level
-- **meshbay.org is internet-facing** — kill all test processes after spikes, close test UFW ports
+- **meshbay.org is internet-facing** — open port → test → close port + kill processes in same block
## Known calibration TODOs
- Argon2id `memory_cost`: currently 65536 (64 MB, 78ms) — increase to 262144 (256 MB) before prod
to target ~500ms on typical home server hardware. Implement a `calibrate` CLI command.
-## meshbay.org server
+## Key modules — où trouver quoi
+
+| Besoin | Module | Fichier |
+|---|---|---|
+| Chiffrement chunks (prod) | `meshbay_common.crypto` | `crypto.py` |
+| Dérivation clés depuis password | `meshbay_common.keyderive` | `keyderive.py` |
+| Bundle clés (web) | `meshbay_common.keyderive` | `keyderive.py` + `static/keyderive.js` |
+| GEK wrap/unwrap (ECIES) | `meshbay_common.crypto` | `crypto.py` |
+| Double Ratchet (chat) | `meshbay_common.ratchet` | `ratchet.py` |
+| AES-GCM (navigateur) | `meshbay_common.webcrypto` | `webcrypto.py` + `static/crypto.js` |
+| Keystore node | `meshbay_node.keystore` | `keystore.py` |
+| Scripts de démo opérationnels | — | `QE/demo-v1/*.py` (non versionné) |
+
+## meshbay.org server (état cible)
- OS: Ubuntu 26.04 LTS, Python 3.14.4
- SSH: `ssh cbesson@meshbay.org`
-- Web proxy: Caddy (to be installed) behind ports 80/443
-- UFW rules: only 22/tcp, 80/tcp, 443/tcp
-- **Always close test ports after spikes** (`sudo ufw delete allow <port>/<proto>`)
-- Hub POC files: `~/meshbay-poc/` (reference only, not production)
+- Caddy : reverse proxy HTTPS sur 80/443
+- UFW rules: **22/tcp, 80/tcp, 443/tcp uniquement**
+- Services légitimes : `meshbay-hub.service`, Caddy, PostgreSQL (local)
+- Inventaire détaillé : `QE/server-state/meshbay.org.md`
+- Deploy hub : voir `QE/server-state/meshbay.org.md`
diff --git a/devel-phases.md b/devel-phases.md
index 2b50330..74141e7 100644
--- a/devel-phases.md
+++ b/devel-phases.md
@@ -312,8 +312,12 @@ persistent MHP federation, content replication, and browser private group decryp
| 6.3 | MHP federation persistence | `FederatedGroup` + `SwarmSource` DB tables | ✅ |
| 6.4 | Content replication | `node/replication.py` + hub `/v1/swarm/*` | ✅ |
| 6.5 | Browser private group | `webcrypto.py` + `static/crypto.js` | ✅ 4/4 tests |
+| 6.6 | Dérivation clés depuis password | `meshbay_common/keyderive.py` + `static/keyderive.js` | ✅ 7/7 tests |
+| 6.7 | Bundle clés chiffré (web) | Hub: `keypair_bundle` field + migration Alembic | ✅ |
+| 6.8 | Scripts démo opérationnels | `QE/demo-v1/` (non versionné) | ✅ testés |
+| 6.9 | QUICKSTART réécrit | `docs/QUICKSTART.md` | ✅ |
-**Total: 74/74 tests.**
+**Total: 81/81 tests.**
---
diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md
index b49eb73..af03a60 100644
--- a/docs/USERGUIDE.md
+++ b/docs/USERGUIDE.md
@@ -62,9 +62,26 @@ MeshBay has three components. Understanding which role each plays avoids a lot o
### Register
-Registration requires submitting your Ed25519 (signing) and X25519 (key agreement) public keys at account creation time. These are used by other members' nodes to wrap GEK bundles for you, and by nodes to verify your JWT offline.
+Registration submits your Ed25519 (signing) and X25519 (key agreement) public keys. These let other members wrap GEK bundles for you and let nodes verify your JWT offline.
-**Critical:** generate and persist your keypairs before registering. If you regenerate them later, all GEK bundles stored for you on the hub become undecryptable. See the Quickstart for the key generation script.
+**Deux modes de génération de clés :**
+
+**Mode CLI / native node** (`setup_demo.py`, `meshbay-node`) :
+Les clés sont *dérivées* de votre username + password via Argon2id — pas besoin de
+fichier de clés séparé. Même identifiants → mêmes clés sur n'importe quelle machine.
+Implémenté dans `meshbay_common.keyderive.derive_keys_from_password()`.
+
+```python
+from meshbay_common.keyderive import derive_keys_from_password
+sk_ed, sk_x = derive_keys_from_password("alice", "MonMotDePasse!")
+```
+
+**Mode navigateur** (interface web) :
+Le navigateur génère des clés aléatoires via WebCrypto, les chiffre avec une clé
+dérivée du mot de passe (PBKDF2-SHA512), et envoie le bundle chiffré au hub.
+À la prochaine connexion, le hub retourne le bundle et le navigateur le déchiffre
+localement. Le hub stocke le bundle mais ne peut pas le lire.
+Implémenté dans `static/keyderive.js`.
```
POST /v1/users/register
@@ -72,13 +89,15 @@ POST /v1/users/register
"username": "string",
"password": "string (min 8 chars)",
"pk_user_ed25519": "base64 raw 32-byte Ed25519 public key",
- "pk_user_x25519": "base64 raw 32-byte X25519 public key"
+ "pk_user_x25519": "base64 raw 32-byte X25519 public key",
+ "keypair_bundle": "base64 AES-256-GCM encrypted bundle (web clients only, optional)"
}
→ 201 {"user_id": "uuid"}
→ 409 if username is taken
```
-Passwords are hashed with Argon2id (iterations=4, memory=256 MB, target ~500ms on a home server). This is intentionally slow to limit offline dictionary attacks.
+Passwords are hashed with Argon2id (iterations=3, memory=64 MB in dev;
+target 256 MB / ~500ms in production). Intentionally slow to resist offline attacks.
### Login
@@ -86,13 +105,17 @@ Passwords are hashed with Argon2id (iterations=4, memory=256 MB, target ~500ms o
POST /v1/users/login
{"username": "yourname", "password": "yourpassword"}
→ {
- "access_token": "JWT (Ed25519, 1 hour validity)",
- "refresh_token": "opaque 256-bit token (30 days)",
- "token_type": "bearer",
- "expires_in": 3600
+ "access_token": "JWT (Ed25519, 1 hour validity)",
+ "refresh_token": "opaque 256-bit token (30 days)",
+ "token_type": "bearer",
+ "expires_in": 3600,
+ "keypair_bundle": "base64 AES-GCM blob (présent uniquement si enregistré via web)"
}
```
+Les clients web utilisent `keypair_bundle` pour récupérer leurs clés privées
+sur un nouvel appareil : déchiffrement local avec le mot de passe via `keyderive.js`.
+
```bash
curl -s -X POST https://meshbay.org/v1/users/login \
-H "Content-Type: application/json" \
diff --git a/docs/meshbay-draft-v3.md b/docs/meshbay-draft-v3.md
index c327415..6983191 100644
--- a/docs/meshbay-draft-v3.md
+++ b/docs/meshbay-draft-v3.md
@@ -406,6 +406,39 @@ All private keys stored exclusively on the node (or client device) in the encryp
Both `PK_ed25519` and `PK_x25519` are registered with the hub at account creation. The hub exposes them via `GET /v1/users/{username}/pubkeys` so that group admins can wrap GEK bundles for members without any direct contact between nodes.
+### 6.1.1 Key Generation Strategies
+
+Three strategies, depending on client type:
+
+**A — CLI / native node (Argon2id derivation)**
+Keys are derived deterministically from `username + password`:
+```
+salt = SHA-256("meshbay:v1:" + username)
+seed = Argon2id(password, salt, length=64)
+sk_ed25519 = Ed25519.from_private_bytes(seed[:32])
+sk_x25519 = X25519.from_private_bytes(seed[32:])
+```
+Same credentials → same keys on any machine. Password recovery = key recovery.
+Implemented in `meshbay_common/keyderive.py::derive_keys_from_password()`.
+
+**B — Web browser (random keypairs + encrypted bundle)**
+Browser generates random keypairs via WebCrypto `generateKey()`, encrypts them
+with a PBKDF2-SHA512 derived key, and uploads the encrypted bundle to the hub
+alongside the public keys. On subsequent logins, the hub returns the bundle
+and the browser decrypts it locally with the password.
+
+The hub stores `keypair_bundle` (AES-256-GCM ciphertext) — opaque, cannot decrypt it.
+Implemented in `static/keyderive.js`. Python side in `keyderive.py::encrypt_keypair_bundle()`.
+
+**C — Native node with keystore file**
+Random keypairs generated once, stored in the Argon2id-encrypted keystore file
+(`~/.config/meshbay/keystore.enc`). Standard operating mode for `meshbay-node`.
+
+**Algorithm mismatch note:** strategies A and B use different KDFs (Argon2id vs PBKDF2).
+A user who registered via CLI (A) and later tries to recover via web (B) with the same
+password will get different keypairs. This is by design: users pick one registration path.
+Cross-path recovery requires the admin to issue new GEK bundles.
+
### 6.2 GEK Management
**Scope:** GEK applies to private groups only. Public groups use TLS transport only (no application-layer encryption).
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
new file mode 100644
index 0000000..779efdc
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/2041a4060b3c_add_keypair_bundle_federated_groups_.py
@@ -0,0 +1,83 @@
+"""add_keypair_bundle_federated_groups_swarm_sources
+
+Revision ID: 2041a4060b3c
+Revises: d28b9caf9f07
+Create Date: 2026-08-09 14:54:20.883227
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision: str = '2041a4060b3c'
+down_revision: Union[str, Sequence[str], None] = 'd28b9caf9f07'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ """Upgrade schema."""
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('content_blocklist',
+ sa.Column('content_hash', sa.String(length=64), nullable=False),
+ sa.Column('reason', sa.String(length=64), nullable=False),
+ sa.Column('added_at', sa.DateTime(timezone=True), nullable=False),
+ sa.Column('added_by', sa.String(length=64), nullable=True),
+ sa.PrimaryKeyConstraint('content_hash')
+ )
+ op.create_table('federated_groups',
+ sa.Column('id', sa.String(length=36), nullable=False),
+ sa.Column('name', sa.String(length=128), nullable=False),
+ sa.Column('source_hub', sa.String(length=128), nullable=False),
+ sa.Column('join_policy', sa.String(length=16), nullable=False),
+ sa.Column('received_at', sa.DateTime(timezone=True), nullable=False),
+ sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_index('ix_federated_groups_name', 'federated_groups', ['name'], unique=False)
+ op.create_index('ix_federated_groups_source', 'federated_groups', ['source_hub'], unique=False)
+ op.create_table('swarm_sources',
+ sa.Column('content_hash', sa.String(length=64), nullable=False),
+ sa.Column('node_id', sa.String(length=36), nullable=False),
+ sa.Column('endpoint', sa.String(length=128), nullable=False),
+ sa.Column('registered_at', sa.DateTime(timezone=True), nullable=False),
+ sa.Column('last_seen', sa.DateTime(timezone=True), nullable=False),
+ sa.PrimaryKeyConstraint('content_hash', 'node_id')
+ )
+ op.create_index('ix_swarm_hash', 'swarm_sources', ['content_hash'], unique=False)
+ op.create_table('content_reports',
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+ sa.Column('reporter_id', sa.String(length=36), nullable=True),
+ sa.Column('content_hash', sa.String(length=64), nullable=False),
+ sa.Column('group_id', sa.String(length=36), nullable=True),
+ sa.Column('reason', sa.String(length=32), nullable=False),
+ sa.Column('detail', sa.String(length=256), nullable=True),
+ sa.Column('ip_address', sa.String(length=45), nullable=False),
+ sa.Column('reported_at', sa.DateTime(timezone=True), nullable=False),
+ sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ),
+ sa.ForeignKeyConstraint(['reporter_id'], ['users.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ 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')
+ op.drop_index('ix_swarm_hash', table_name='swarm_sources')
+ op.drop_table('swarm_sources')
+ op.drop_index('ix_federated_groups_source', table_name='federated_groups')
+ op.drop_index('ix_federated_groups_name', table_name='federated_groups')
+ op.drop_table('federated_groups')
+ op.drop_table('content_blocklist')
+ # ### end Alembic commands ###