diff --git a/convert_firmware_key.sh b/convert_firmware_key.sh index bd918e6..7d3b78c 100755 --- a/convert_firmware_key.sh +++ b/convert_firmware_key.sh @@ -1,7 +1,7 @@ #!/bin/bash # Convert MeshCore firmware 64-byte private key to pyMC_Repeater format # -# Usage: sudo ./convert_firmware_key.sh <64-byte-hex-key> [config-path] +# Usage: sudo ./convert_firmware_key.sh <64-byte-hex-key> [--output-format=] [config-path] # Example: sudo ./convert_firmware_key.sh 987BDA619630197351F2B3040FD19B2EE0DEE357DD69BBEEE295786FA78A4D5F298B0BF1B7DE73CBC23257CDB2C562F5033DF58C232916432948B0F6BA4448F2 set -e @@ -9,10 +9,10 @@ set -e if [ $# -eq 0 ]; then echo "Error: No key provided" echo "" - echo "Usage: sudo $0 <64-byte-hex-key> [config-path]" + echo "Usage: sudo $0 <64-byte-hex-key> [--output-format=] [config-path]" echo "" echo "This script imports a 64-byte MeshCore firmware private key into" - echo "pyMC_Repeater config.yaml for full identity compatibility." + echo "pyMC_Repeater for full identity compatibility." echo "" echo "The 64-byte key format: [32-byte scalar][32-byte nonce]" echo " - Enables same node address as firmware device" @@ -20,10 +20,17 @@ if [ $# -eq 0 ]; then echo " - Fully compatible with pyMC_core LocalIdentity" echo "" echo "Arguments:" + echo " --output-format: Optional output format (yaml|identity, default: yaml)" + echo " yaml - Store in config.yaml (embedded binary)" + echo " identity - Save to identity.key file (base64 encoded)" echo " config-path: Optional path to config.yaml (default: /etc/pymc_repeater/config.yaml)" echo "" - echo "Example:" + echo "Examples:" + echo " # Save to config.yaml (default)" echo " sudo $0 987BDA619630197351F2B3040FD19B2EE0DEE357DD69BBEEE295786FA78A4D5F298B0BF1B7DE73CBC23257CDB2C562F5033DF58C232916432948B0F6BA4448F2" + echo "" + echo " # Save to identity.key file" + echo " sudo $0 987BDA619630197351F2B3040FD19B2EE0DEE357DD69BBEEE295786FA78A4D5F298B0BF1B7DE73CBC23257CDB2C562F5033DF58C232916432948B0F6BA4448F2 --output-format=identity" exit 1 fi @@ -35,6 +42,33 @@ if [ "$EUID" -ne 0 ]; then fi FULL_KEY="$1" +OUTPUT_FORMAT="yaml" # Default format +CONFIG_PATH="" + +# Parse arguments +shift # Remove the key argument +while [ $# -gt 0 ]; do + case "$1" in + --output-format=*) + OUTPUT_FORMAT="${1#*=}" + ;; + *) + CONFIG_PATH="$1" + ;; + esac + shift +done + +# Validate output format +if [ "$OUTPUT_FORMAT" != "yaml" ] && [ "$OUTPUT_FORMAT" != "identity" ]; then + echo "Error: Invalid output format '$OUTPUT_FORMAT'. Must be 'yaml' or 'identity'" + exit 1 +fi + +# Set default config path if not provided +if [ -z "$CONFIG_PATH" ]; then + CONFIG_PATH="/etc/pymc_repeater/config.yaml" +fi # Validate hex string if ! [[ "$FULL_KEY" =~ ^[0-9a-fA-F]+$ ]]; then @@ -49,17 +83,28 @@ if [ "$KEY_LEN" -ne 128 ]; then exit 1 fi -# Get config path -CONFIG_PATH="${2:-/etc/pymc_repeater/config.yaml}" - -# Check if config exists -if [ ! -f "$CONFIG_PATH" ]; then - echo "Error: Config file not found: $CONFIG_PATH" - exit 1 +# Check if config/identity file location exists (only for yaml format or if saving identity.key) +if [ "$OUTPUT_FORMAT" = "yaml" ]; then + # Check if config exists + if [ ! -f "$CONFIG_PATH" ]; then + echo "Error: Config file not found: $CONFIG_PATH" + exit 1 + fi +else + # For identity format, use system-wide location matching config.yaml + IDENTITY_DIR="/etc/pymc_repeater" + IDENTITY_PATH="$IDENTITY_DIR/identity.key" fi echo "=== MeshCore Firmware Key Import ===" echo "" +echo "Output format: $OUTPUT_FORMAT" +if [ "$OUTPUT_FORMAT" = "yaml" ]; then + echo "Target file: $CONFIG_PATH" +else + echo "Target file: $IDENTITY_PATH" +fi +echo "" echo "Input (64-byte firmware key):" echo " $FULL_KEY" echo "" @@ -70,11 +115,13 @@ import sys import yaml import base64 import hashlib +import os from pathlib import Path # Import the key key_hex = "$FULL_KEY" key_bytes = bytes.fromhex(key_hex) +output_format = "$OUTPUT_FORMAT" # Verify with pyMC if available try: @@ -94,47 +141,116 @@ except ImportError: print("Warning: PyNaCl not available, skipping verification") print() -# Load config -config_path = Path("$CONFIG_PATH") -try: - with open(config_path, 'r') as f: - config = yaml.safe_load(f) or {} -except Exception as e: - print(f"Error loading config: {e}") - sys.exit(1) +if output_format == "yaml": + # Save to config.yaml + config_path = Path("$CONFIG_PATH") + try: + with open(config_path, 'r') as f: + config = yaml.safe_load(f) or {} + except Exception as e: + print(f"Error loading config: {e}") + sys.exit(1) -# Check for existing key -if 'mesh' in config and 'identity_key' in config['mesh']: - existing = config['mesh']['identity_key'] - if isinstance(existing, bytes): - print(f"WARNING: Existing identity_key found ({len(existing)} bytes)") + # Check for existing key + if 'mesh' in config and 'identity_key' in config['mesh']: + existing = config['mesh']['identity_key'] + if isinstance(existing, bytes): + print(f"WARNING: Existing identity_key found ({len(existing)} bytes)") + else: + print(f"WARNING: Existing identity_key found") + print() + + # Ensure mesh section exists + if 'mesh' not in config: + config['mesh'] = {} + + # Store the full 64-byte key + config['mesh']['identity_key'] = key_bytes + + # Save config atomically + backup_path = f"{config_path}.backup.{Path(config_path).stat().st_mtime_ns}" + import shutil + shutil.copy2(config_path, backup_path) + print(f"Created backup: {backup_path}") + + try: + with open(config_path, 'w') as f: + yaml.safe_dump(config, f, default_flow_style=False, allow_unicode=True) + print(f"✓ Successfully updated {config_path}") + print() + except Exception as e: + print(f"Error writing config: {e}") + shutil.copy2(backup_path, config_path) + print(f"Restored from backup") + sys.exit(1) + +else: + # Save to identity.key file + identity_path = Path("$IDENTITY_PATH") + + # Create directory if it doesn't exist + identity_path.parent.mkdir(parents=True, exist_ok=True) + + # Check for existing identity.key + if identity_path.exists(): + print(f"WARNING: Existing identity.key found at {identity_path}") + backup_path = identity_path.with_suffix('.key.backup') + import shutil + shutil.copy2(identity_path, backup_path) + print(f"Created backup: {backup_path}") + print() + + # Save as base64-encoded + try: + with open(identity_path, 'wb') as f: + f.write(base64.b64encode(key_bytes)) + os.chmod(identity_path, 0o600) # Restrict permissions + print(f"✓ Successfully saved to {identity_path}") + print(f"✓ File permissions set to 0600 (owner read/write only)") + print() + except Exception as e: + print(f"Error writing identity.key: {e}") + sys.exit(1) + + # Update config.yaml to remove embedded identity_key so it uses the file + config_path = Path("$CONFIG_PATH") + if config_path.exists(): + try: + with open(config_path, 'r') as f: + config = yaml.safe_load(f) or {} + + # Check if identity_key exists in config + if 'mesh' in config and 'identity_key' in config['mesh']: + print(f"Updating {config_path} to use identity.key file...") + + # Create backup + backup_path = f"{config_path}.backup.{Path(config_path).stat().st_mtime_ns}" + import shutil + shutil.copy2(config_path, backup_path) + print(f"Created backup: {backup_path}") + + # Remove identity_key from config + del config['mesh']['identity_key'] + + # Save updated config + with open(config_path, 'w') as f: + yaml.safe_dump(config, f, default_flow_style=False, allow_unicode=True) + + print(f"✓ Removed embedded identity_key from {config_path}") + print(f"✓ Config will now use {identity_path}") + print() + else: + print(f"✓ Config file already configured to use identity.key file") + print() + + except Exception as e: + print(f"Warning: Could not update config.yaml: {e}") + print(f"You may need to manually remove 'identity_key' from {config_path}") + print() else: - print(f"WARNING: Existing identity_key found") - print() - -# Ensure mesh section exists -if 'mesh' not in config: - config['mesh'] = {} - -# Store the full 64-byte key -config['mesh']['identity_key'] = key_bytes - -# Save config atomically -backup_path = f"{config_path}.backup.{Path(config_path).stat().st_mtime_ns}" -import shutil -shutil.copy2(config_path, backup_path) -print(f"Created backup: {backup_path}") - -try: - with open(config_path, 'w') as f: - yaml.safe_dump(config, f, default_flow_style=False, allow_unicode=True) - print(f"✓ Successfully updated {config_path}") - print() -except Exception as e: - print(f"Error writing config: {e}") - shutil.copy2(backup_path, config_path) - print(f"Restored from backup") - sys.exit(1) + print(f"Note: Config file not found at {config_path}") + print(f" Identity will be loaded from {identity_path}") + print() EOF @@ -143,20 +259,41 @@ if [ $? -ne 0 ]; then exit 1 fi -# Offer to restart service -if systemctl is-active --quiet pymc-repeater 2>/dev/null; then - read -p "Restart pymc-repeater service now? (yes/no): " RESTART - if [ "$RESTART" = "yes" ]; then - systemctl restart pymc-repeater - echo "✓ Service restarted" - echo "" - echo "Check logs for new identity:" - echo " sudo journalctl -u pymc-repeater -f | grep -i 'identity\|hash'" +# Offer to restart service (only relevant for yaml format) +if [ "$OUTPUT_FORMAT" = "yaml" ]; then + if systemctl is-active --quiet pymc-repeater 2>/dev/null; then + read -p "Restart pymc-repeater service now? (yes/no): " RESTART + if [ "$RESTART" = "yes" ]; then + systemctl restart pymc-repeater + echo "✓ Service restarted" + echo "" + echo "Check logs for new identity:" + echo " sudo journalctl -u pymc-repeater -f | grep -i 'identity\|hash'" + else + echo "Remember to restart the service:" + echo " sudo systemctl restart pymc-repeater" + fi else - echo "Remember to restart the service:" - echo " sudo systemctl restart pymc-repeater" + echo "Note: pymc-repeater service is not running" + echo "Start it with: sudo systemctl start pymc-repeater" fi else - echo "Note: pymc-repeater service is not running" - echo "Start it with: sudo systemctl start pymc-repeater" + echo "Identity key saved to file." + echo "" + if systemctl is-active --quiet pymc-repeater 2>/dev/null; then + read -p "Restart pymc-repeater service now? (yes/no): " RESTART + if [ "$RESTART" = "yes" ]; then + systemctl restart pymc-repeater + echo "✓ Service restarted" + echo "" + echo "Check logs for new identity:" + echo " sudo journalctl -u pymc-repeater -f | grep -i 'identity\|hash'" + else + echo "Remember to restart the service:" + echo " sudo systemctl restart pymc-repeater" + fi + else + echo "Note: pymc-repeater service is not running" + echo "Start it with: sudo systemctl start pymc-repeater" + fi fi diff --git a/repeater/config.py b/repeater/config.py index e95deb0..f754614 100644 --- a/repeater/config.py +++ b/repeater/config.py @@ -156,13 +156,18 @@ def update_global_flood_policy(allow: bool, config_path: Optional[str] = None) - def _load_or_create_identity_key(path: Optional[str] = None) -> bytes: if path is None: - # Follow XDG spec - xdg_config_home = os.environ.get("XDG_CONFIG_HOME") - if xdg_config_home: - config_dir = Path(xdg_config_home) / "pymc_repeater" + # Check system-wide location first (matches config.yaml location) + system_key_path = Path("/etc/pymc_repeater/identity.key") + if system_key_path.exists(): + key_path = system_key_path else: - config_dir = Path.home() / ".config" / "pymc_repeater" - key_path = config_dir / "identity.key" + # Follow XDG spec + xdg_config_home = os.environ.get("XDG_CONFIG_HOME") + if xdg_config_home: + config_dir = Path(xdg_config_home) / "pymc_repeater" + else: + config_dir = Path.home() / ".config" / "pymc_repeater" + key_path = config_dir / "identity.key" else: key_path = Path(path) @@ -173,8 +178,8 @@ def _load_or_create_identity_key(path: Optional[str] = None) -> bytes: with open(key_path, "rb") as f: encoded = f.read() key = base64.b64decode(encoded) - if len(key) != 32: - raise ValueError(f"Invalid key length: {len(key)}, expected 32") + if len(key) not in (32, 64): + raise ValueError(f"Invalid key length: {len(key)}, expected 32 or 64") logger.info(f"Loaded existing identity key from {key_path}") return key except Exception as e: