diff options
Diffstat (limited to 'packages/meshbay-common/src/meshbay_common/keyderive.py')
| -rw-r--r-- | packages/meshbay-common/src/meshbay_common/keyderive.py | 130 |
1 files changed, 0 insertions, 130 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"]), - ) |