From a53df3d76fb29467d8e31dbce89f191f194d6c45 Mon Sep 17 00:00:00 2001 From: Rightup Date: Sun, 28 Jun 2026 22:57:14 +0100 Subject: [PATCH] feat: implement MeshCore-compatible key generation for all identity keys with guards for 0x00 and 0xff prefixs --- repeater/config.py | 7 ++++-- repeater/keygen.py | 41 +++++++++++++++++++++++------------ repeater/web/api_endpoints.py | 8 ++++--- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/repeater/config.py b/repeater/config.py index a108a1e..b3b5f6d 100644 --- a/repeater/config.py +++ b/repeater/config.py @@ -422,8 +422,11 @@ def _load_or_create_identity_key(path: Optional[str] = None) -> bytes: except Exception as e: logger.warning(f"Failed to load identity key: {e}") - # Generate new random key - key = os.urandom(32) + # Generate new key via MeshCore-compatible keygen; store 32-byte private scalar. + from repeater.keygen import generate_meshcore_keypair + + _, private_key = generate_meshcore_keypair() + key = private_key[:32] # Save it try: diff --git a/repeater/keygen.py b/repeater/keygen.py index 4a92b08..06b46e2 100644 --- a/repeater/keygen.py +++ b/repeater/keygen.py @@ -14,6 +14,11 @@ from typing import Optional, Tuple from nacl.bindings import crypto_scalarmult_ed25519_base_noclamp +def _has_disallowed_public_prefix(public_key: bytes) -> bool: + """Return True when public key starts with a forbidden prefix byte.""" + return bool(public_key) and public_key[0] in (0x00, 0xFF) + + def generate_meshcore_keypair() -> Tuple[bytes, bytes]: """Generate a MeshCore-compatible Ed25519 keypair. @@ -21,25 +26,30 @@ def generate_meshcore_keypair() -> Tuple[bytes, bytes]: (public_key, private_key) as raw bytes. public_key is 32 bytes, private_key is 64 bytes. """ - # 1. Random 32-byte seed - seed = secrets.token_bytes(32) + while True: + # 1. Random 32-byte seed + seed = secrets.token_bytes(32) - # 2. SHA-512 hash - digest = hashlib.sha512(seed).digest() + # 2. SHA-512 hash + digest = hashlib.sha512(seed).digest() - # 3. Ed25519 scalar clamping on first 32 bytes - clamped = bytearray(digest[:32]) - clamped[0] &= 248 # Clear bottom 3 bits - clamped[31] &= 63 # Clear top 2 bits - clamped[31] |= 64 # Set bit 6 + # 3. Ed25519 scalar clamping on first 32 bytes + clamped = bytearray(digest[:32]) + clamped[0] &= 248 # Clear bottom 3 bits + clamped[31] &= 63 # Clear top 2 bits + clamped[31] |= 64 # Set bit 6 - # 4. Derive public key - public_key = crypto_scalarmult_ed25519_base_noclamp(bytes(clamped)) + # 4. Derive public key + public_key = crypto_scalarmult_ed25519_base_noclamp(bytes(clamped)) - # 5. Private key = [clamped_scalar][sha512_upper_half] - private_key = bytes(clamped) + digest[32:64] + # Disallow keys with 0x00 / 0xFF leading byte. + if _has_disallowed_public_prefix(public_key): + continue - return public_key, private_key + # 5. Private key = [clamped_scalar][sha512_upper_half] + private_key = bytes(clamped) + digest[32:64] + + return public_key, private_key def generate_vanity_key( @@ -57,6 +67,9 @@ def generate_vanity_key( """ target = prefix.upper() + if target.startswith("00") or target.startswith("FF"): + return None + for attempt in range(1, max_iterations + 1): pub, priv = generate_meshcore_keypair() if pub.hex().upper().startswith(target): diff --git a/repeater/web/api_endpoints.py b/repeater/web/api_endpoints.py index eddb78f..e60161c 100644 --- a/repeater/web/api_endpoints.py +++ b/repeater/web/api_endpoints.py @@ -4256,9 +4256,11 @@ class APIEndpoints: key_was_generated = False if not identity_key: try: - # Generate a new random 32-byte key (same method as config.py) - random_key = os.urandom(32) - identity_key = random_key.hex() + # Use MeshCore-compatible keygen and store 32-byte private scalar hex. + from repeater.keygen import generate_meshcore_keypair + + _, private_key = generate_meshcore_keypair() + identity_key = private_key[:32].hex() key_was_generated = True logger.info(f"Auto-generated identity key for '{name}': {identity_key[:16]}...") except Exception as gen_error: