From aed220d9f0bab42efd57b56851319e840ab8ae26 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sun, 9 Aug 2026 14:50:22 +0200 Subject: feat: password-based key derivation + operational QUICKSTART MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit keyderive.py: derive Ed25519+X25519 from username+password via Argon2id. Same credentials → same keys on any device. Encrypt/decrypt keypair bundle (AES-256-GCM) for hub storage (web clients). 7/7 tests. Full suite: 81/81. keyderive.js: browser counterpart using PBKDF2-SHA512 + random keypairs encrypted for hub storage. Avoids algorithm mismatch with Python. hub/models.py + users.py: keypair_bundle field added to User, stored on registration, returned in login response for web client key recovery. QUICKSTART.md: fully rewritten. 3 operational scripts in QE/demo-v1/: setup_demo.py — create accounts, group, distribute GEK run_node.py — start HTTP node (watches shared/ directory) download.py — bob login → GEK fetch → decrypt → save All tested locally end-to-end. No invented URLs. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- packages/meshbay-common/tests/test_keyderive.py | 57 +++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 packages/meshbay-common/tests/test_keyderive.py (limited to 'packages/meshbay-common/tests/test_keyderive.py') diff --git a/packages/meshbay-common/tests/test_keyderive.py b/packages/meshbay-common/tests/test_keyderive.py new file mode 100644 index 0000000..0aa6201 --- /dev/null +++ b/packages/meshbay-common/tests/test_keyderive.py @@ -0,0 +1,57 @@ +"""Tests for password-based key derivation.""" + +import pytest +from meshbay_common.keyderive import ( + derive_keys_from_password, + encrypt_keypair_bundle, + decrypt_keypair_bundle, +) +from meshbay_common.crypto import pk_to_b64 + + +def test_deterministic(): + """Same credentials → same keys.""" + sk_ed1, sk_x1 = derive_keys_from_password("alice", "correct-horse") + sk_ed2, sk_x2 = derive_keys_from_password("alice", "correct-horse") + assert pk_to_b64(sk_ed1.public_key()) == pk_to_b64(sk_ed2.public_key()) + assert pk_to_b64(sk_x1.public_key()) == pk_to_b64(sk_x2.public_key()) + + +def test_different_users_different_keys(): + sk_ed_a, _ = derive_keys_from_password("alice", "samepassword") + sk_ed_b, _ = derive_keys_from_password("bob", "samepassword") + assert pk_to_b64(sk_ed_a.public_key()) != pk_to_b64(sk_ed_b.public_key()) + + +def test_different_passwords_different_keys(): + sk_ed1, _ = derive_keys_from_password("alice", "password1") + sk_ed2, _ = derive_keys_from_password("alice", "password2") + assert pk_to_b64(sk_ed1.public_key()) != pk_to_b64(sk_ed2.public_key()) + + +def test_ed_and_x_keys_independent(): + sk_ed, sk_x = derive_keys_from_password("user", "pass12345") + from meshbay_common.crypto import sk_to_raw + assert sk_to_raw(sk_ed) != sk_to_raw(sk_x) + + +def test_bundle_encrypt_decrypt(): + sk_ed, sk_x = derive_keys_from_password("alice", "strongpass!") + bundle = encrypt_keypair_bundle(sk_ed, sk_x, "password123", "alice") + sk_ed2, sk_x2 = decrypt_keypair_bundle(bundle, "password123", "alice") + assert pk_to_b64(sk_ed.public_key()) == pk_to_b64(sk_ed2.public_key()) + assert pk_to_b64(sk_x.public_key()) == pk_to_b64(sk_x2.public_key()) + + +def test_bundle_wrong_password_rejected(): + sk_ed, sk_x = derive_keys_from_password("alice", "correctpass") + bundle = encrypt_keypair_bundle(sk_ed, sk_x, "correctpass", "alice") + with pytest.raises(Exception): + decrypt_keypair_bundle(bundle, "wrongpass", "alice") + + +def test_bundle_wrong_username_rejected(): + sk_ed, sk_x = derive_keys_from_password("alice", "pass12345") + bundle = encrypt_keypair_bundle(sk_ed, sk_x, "pass12345", "alice") + with pytest.raises(Exception): + decrypt_keypair_bundle(bundle, "pass12345", "bob") # wrong username salt -- cgit v1.2.3