mirror of
https://github.com/pe1hvh/meshcore-gui.git
synced 2026-03-28 17:42:38 +01:00
148 lines
5.1 KiB
Python
148 lines
5.1 KiB
Python
"""
|
|
Device identity file writer for MeshCore Observer integration.
|
|
|
|
After a successful connection, the worker calls
|
|
:func:`write_device_identity` with the device's public and private
|
|
keys. The resulting JSON file is placed outside the git repo at::
|
|
|
|
~/.meshcore-gui/device_identity.json
|
|
|
|
The MeshCore Observer reads this file automatically for MQTT
|
|
authentication — no manual key setup required.
|
|
|
|
File format::
|
|
|
|
{
|
|
"public_key": "64-char hex UPPERCASE (from send_appstart)",
|
|
"private_key": "128-char hex lowercase (full orlp/ed25519 expanded key)",
|
|
"device_name": "PE1HVH T1000e",
|
|
"firmware_version": "1.2.3",
|
|
"source_device": "/dev/ttyUSB1",
|
|
"updated_at": "2026-02-26T15:00:00+00:00"
|
|
}
|
|
|
|
Author: PE1HVH
|
|
Version: 1.0.0
|
|
SPDX-License-Identifier: MIT
|
|
Copyright: (c) 2026 PE1HVH
|
|
"""
|
|
|
|
import json
|
|
import stat
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from meshcore_gui.config import DATA_DIR, debug_print
|
|
|
|
# Fixed output path — observer looks here by default
|
|
IDENTITY_FILE: Path = DATA_DIR / "device_identity.json"
|
|
|
|
|
|
def write_device_identity(
|
|
public_key: str,
|
|
private_key_bytes: bytes,
|
|
device_name: str = "",
|
|
firmware_version: str = "",
|
|
source_device: str = "",
|
|
) -> bool:
|
|
"""Write the device identity file for MeshCore Observer.
|
|
|
|
Args:
|
|
public_key: 64-char hex public key (from send_appstart).
|
|
This is the key shown in the GUI and registered
|
|
at LetsMesh. MUST be used for MQTT username.
|
|
private_key_bytes: 64 raw bytes from export_private_key() in
|
|
orlp/ed25519 expanded format. All 64 bytes
|
|
are needed for createAuthToken().
|
|
device_name: Device display name.
|
|
firmware_version: Firmware version string.
|
|
source_device: Device path (e.g. ``/dev/ttyUSB1``).
|
|
|
|
Returns:
|
|
True if the file was written successfully.
|
|
"""
|
|
try:
|
|
# The 64 bytes from export_private_key() are in orlp/ed25519
|
|
# *expanded* format:
|
|
# bytes 0..31 = clamped scalar (NOT the raw seed)
|
|
# bytes 32..63 = nonce prefix (NOT the public key)
|
|
#
|
|
# The public key is NOT contained in these 64 bytes — it must
|
|
# come from send_appstart() which returns the actual device
|
|
# public key as shown in the GUI and registered at LetsMesh.
|
|
#
|
|
# For MQTT auth via meshcore-decoder's createAuthToken(), the
|
|
# full 64 bytes are needed as privateKeyHex (128 hex chars).
|
|
if len(private_key_bytes) != 64:
|
|
debug_print(
|
|
f"DeviceIdentity: unexpected key length "
|
|
f"{len(private_key_bytes)}, expected 64 bytes"
|
|
)
|
|
return False
|
|
|
|
# Full 64-byte private key in MeshCore/orlp format
|
|
private_key_hex = private_key_bytes.hex()
|
|
|
|
if not public_key or len(public_key) != 64:
|
|
debug_print(
|
|
f"DeviceIdentity: no valid public key from appstart "
|
|
f"(got {public_key!r}), cannot write identity file"
|
|
)
|
|
return False
|
|
|
|
identity = {
|
|
"public_key": public_key.upper(),
|
|
"private_key": private_key_hex.lower(),
|
|
"device_name": device_name,
|
|
"firmware_version": firmware_version,
|
|
"source_device": source_device,
|
|
"updated_at": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
|
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
IDENTITY_FILE.write_text(
|
|
json.dumps(identity, indent=2) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
# Restrictive permissions — file contains the private key
|
|
IDENTITY_FILE.chmod(stat.S_IRUSR | stat.S_IWUSR) # 600
|
|
|
|
debug_print(
|
|
f"DeviceIdentity: written to {IDENTITY_FILE} "
|
|
f"(pub={public_key[:12]}... priv={private_key_hex[:12]}...)"
|
|
)
|
|
print(f"📝 Device identity saved → {IDENTITY_FILE}")
|
|
return True
|
|
|
|
except Exception as exc:
|
|
debug_print(f"DeviceIdentity: write failed: {exc}")
|
|
print(f"⚠️ Could not save device identity: {exc}")
|
|
return False
|
|
|
|
|
|
def read_device_identity() -> Optional[dict]:
|
|
"""Read the device identity file.
|
|
|
|
Returns:
|
|
Dict with ``public_key`` and ``private_key`` (hex strings),
|
|
or None if the file does not exist or is invalid.
|
|
"""
|
|
if not IDENTITY_FILE.exists():
|
|
return None
|
|
|
|
try:
|
|
data = json.loads(IDENTITY_FILE.read_text(encoding="utf-8"))
|
|
pub = data.get("public_key", "")
|
|
priv = data.get("private_key", "")
|
|
if len(pub) == 64 and len(priv) in (64, 128):
|
|
return data
|
|
debug_print(
|
|
f"DeviceIdentity: invalid key lengths in {IDENTITY_FILE} "
|
|
f"(pub={len(pub)}, priv={len(priv)})"
|
|
)
|
|
return None
|
|
except (json.JSONDecodeError, OSError) as exc:
|
|
debug_print(f"DeviceIdentity: read error: {exc}")
|
|
return None
|