aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-common')
-rw-r--r--packages/meshbay-common/src/meshbay_common/keyderive.py130
-rw-r--r--packages/meshbay-common/tests/test_keyderive.py57
2 files changed, 0 insertions, 187 deletions
diff --git a/packages/meshbay-common/src/meshbay_common/keyderive.py b/packages/meshbay-common/src/meshbay_common/keyderive.py
deleted file mode 100644
index 4b90af3..0000000
--- a/packages/meshbay-common/src/meshbay_common/keyderive.py
+++ /dev/null
@@ -1,130 +0,0 @@
-"""
-MeshBay — Key derivation from username + password.
-
-Allows Ed25519 + X25519 keypairs to be derived deterministically
-from credentials. Same inputs → same keys on any device.
-
-Algorithm: Argon2id (Python CLI / native clients)
- salt = SHA-256("meshbay:v1:" + username)
- seed = Argon2id(password, salt, length=64, ...)
- sk_ed = Ed25519PrivateKey.from_private_bytes(seed[:32])
- sk_x25519 = X25519PrivateKey.from_private_bytes(seed[32:])
-
-Browser alternative (keyderive.js): uses PBKDF2-SHA512 because
-WebCrypto does not support Argon2. The two algorithms produce
-DIFFERENT keys from the same password — a user registered via Python
-CLI and via web browser will have different keypairs.
-
-Resolution: the web client generates RANDOM keypairs on first login
-(WebCrypto, stored encrypted in hub), and uses derive_keys_from_password
-only to encrypt/decrypt the stored keypair bundle. This avoids the
-algorithm mismatch problem entirely.
-
-See keyderive.js for the browser-side implementation.
-"""
-
-import hashlib
-
-from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
-from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
-from cryptography.hazmat.primitives.kdf.argon2 import Argon2id
-
-# Argon2id parameters — same as keystore (see crypto.py)
-_ITERATIONS = 3
-_MEMORY_COST = 65536 # 64 MB — increase to 262144 for production
-_LANES = 4
-_SEED_LENGTH = 64 # 32 bytes Ed25519 + 32 bytes X25519
-
-
-def _derive_salt(username: str) -> bytes:
- """Deterministic salt: SHA-256 of 'meshbay:v1:<username>'."""
- return hashlib.sha256(f"meshbay:v1:{username}".encode()).digest()
-
-
-def derive_keys_from_password(
- username: str,
- password: str,
-) -> tuple[Ed25519PrivateKey, X25519PrivateKey]:
- """
- Derive Ed25519 + X25519 keypairs deterministically from username + password.
-
- Properties:
- - Same credentials always produce the same keypairs
- - Different usernames produce different keys (even with same password)
- - Password cannot be recovered from the public keys
- - Changing the password invalidates all GEK bundles stored on the hub
-
- Use for:
- - CLI / native node registration (Argon2id available)
- - Recovery of lost keypairs from credentials
-
- Do NOT use for:
- - Web browser registration (use random keypairs + encrypted bundle instead)
- """
- salt = _derive_salt(username)
- kdf = Argon2id(
- salt=salt, length=_SEED_LENGTH,
- iterations=_ITERATIONS, lanes=_LANES, memory_cost=_MEMORY_COST,
- )
- seed = kdf.derive(password.encode())
- return (
- Ed25519PrivateKey.from_private_bytes(seed[:32]),
- X25519PrivateKey.from_private_bytes(seed[32:]),
- )
-
-
-def encrypt_keypair_bundle(
- sk_ed: Ed25519PrivateKey,
- sk_x: X25519PrivateKey,
- password: str,
- username: str,
-) -> bytes:
- """
- Encrypt a keypair bundle with a password-derived key (for hub storage).
- Used by web clients: random keypairs encrypted with password, stored on hub.
- Returns: AES-256-GCM ciphertext (nonce prepended).
- """
- import os
-
- import msgpack
- from cryptography.hazmat.primitives.ciphers.aead import AESGCM
-
- from meshbay_common.crypto import sk_to_raw
-
- # Derive an AES key from the password (different info string from key derivation)
- salt = hashlib.sha256(f"meshbay:bundle:v1:{username}".encode()).digest()
- kdf = Argon2id(salt=salt, length=32, iterations=_ITERATIONS,
- lanes=_LANES, memory_cost=_MEMORY_COST)
- aes_key = kdf.derive(password.encode())
-
- payload = msgpack.packb({
- "sk_ed": sk_to_raw(sk_ed),
- "sk_x": sk_to_raw(sk_x),
- }, use_bin_type=True)
-
- nonce = os.urandom(12)
- ct = AESGCM(aes_key).encrypt(nonce, payload, None)
- return nonce + ct
-
-
-def decrypt_keypair_bundle(
- bundle: bytes,
- password: str,
- username: str,
-) -> tuple[Ed25519PrivateKey, X25519PrivateKey]:
- """Decrypt a keypair bundle. Raises on wrong password."""
- import msgpack
- from cryptography.hazmat.primitives.ciphers.aead import AESGCM
-
- salt = hashlib.sha256(f"meshbay:bundle:v1:{username}".encode()).digest()
- kdf = Argon2id(salt=salt, length=32, iterations=_ITERATIONS,
- lanes=_LANES, memory_cost=_MEMORY_COST)
- aes_key = kdf.derive(password.encode())
-
- nonce, ct = bundle[:12], bundle[12:]
- payload = AESGCM(aes_key).decrypt(nonce, ct, None)
- data = msgpack.unpackb(payload, raw=False)
- return (
- Ed25519PrivateKey.from_private_bytes(data["sk_ed"]),
- X25519PrivateKey.from_private_bytes(data["sk_x"]),
- )
diff --git a/packages/meshbay-common/tests/test_keyderive.py b/packages/meshbay-common/tests/test_keyderive.py
deleted file mode 100644
index 40b3c71..0000000
--- a/packages/meshbay-common/tests/test_keyderive.py
+++ /dev/null
@@ -1,57 +0,0 @@
-"""Tests for password-based key derivation."""
-
-import pytest
-from meshbay_common.crypto import pk_to_b64
-from meshbay_common.keyderive import (
- decrypt_keypair_bundle,
- derive_keys_from_password,
- encrypt_keypair_bundle,
-)
-
-
-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