from __future__ import annotations import base64 from typing import Tuple from nacl import public, signing, bindings def ed25519_to_x25519(ed_seed: bytes) -> Tuple[public.PrivateKey, public.PublicKey]: """Convert Ed25519 seed (32 bytes) to X25519 key pair using libsodium conversion.""" if len(ed_seed) != 32: raise ValueError("ed25519 seed must be 32 bytes") sk_ed = signing.SigningKey(ed_seed) sk_ed_bytes = sk_ed._seed + sk_ed.verify_key._key # 64-byte expanded sk (seed||pub) sk_x_bytes = bindings.crypto_sign_ed25519_sk_to_curve25519(sk_ed_bytes) pk_x_bytes = bindings.crypto_sign_ed25519_pk_to_curve25519(bytes(sk_ed.verify_key)) sk_x = public.PrivateKey(sk_x_bytes) pk_x = public.PublicKey(pk_x_bytes) return sk_x, pk_x def x25519_pub_b64_from_ed_seed(ed_seed: bytes) -> str: _, pk = ed25519_to_x25519(ed_seed) return base64.b64encode(bytes(pk)).decode()