diff options
Diffstat (limited to 'packages/meshbay-common')
| -rw-r--r-- | packages/meshbay-common/src/meshbay_common/keyderive.py | 130 | ||||
| -rw-r--r-- | packages/meshbay-common/tests/test_keyderive.py | 57 |
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 |