Files
meshcore-gui/meshcore_gui/services/device_identity.py
2026-03-09 17:53:29 +01:00

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