espilon-source/tools/C3PO/tui/widgets/device_tabs.py
Eun0us 8b6c1cd53d ε - ChaCha20-Poly1305 AEAD + HKDF crypto upgrade + C3PO rewrite + docs
Crypto:
- Replace broken ChaCha20 (static nonce) with ChaCha20-Poly1305 AEAD
- HKDF-SHA256 key derivation from per-device factory NVS master keys
- Random 12-byte nonce per message (ESP32 hardware RNG)
- crypto_init/encrypt/decrypt API with mbedtls legacy (ESP-IDF v5.3.2)
- Custom partition table with factory NVS (fctry at 0x10000)

Firmware:
- crypto.c full rewrite, messages.c device_id prefix + AEAD encrypt
- crypto_init() at boot with esp_restart() on failure
- Fix command_t initializations across all modules (sub/help fields)
- Clean CMakeLists dependencies for ESP-IDF v5.3.2

C3PO (C2):
- Rename tools/c2 + tools/c3po -> tools/C3PO
- Per-device CryptoContext with HKDF key derivation
- KeyStore (keys.json) for master key management
- Transport parses device_id:base64(...) wire format

Tools:
- New tools/provisioning/provision.py for factory NVS key generation
- Updated flasher with mbedtls config for v5.3.2

Docs:
- Update all READMEs for new crypto, C3PO paths, provisioning
- Update roadmap, architecture diagrams, security sections
- Update CONTRIBUTING.md project structure
2026-02-10 21:28:45 +01:00

160 lines
4.9 KiB
Python

"""
Dynamic device tabs widget.
"""
from textual.widgets import Static, Button
from textual.containers import Horizontal
from textual.message import Message
from textual.reactive import reactive
class DeviceTabs(Horizontal):
"""Tab bar for device switching with dynamic updates."""
DEFAULT_CSS = """
DeviceTabs {
height: 1;
width: 100%;
background: $surface;
padding: 0;
}
DeviceTabs .tab-label {
padding: 0 1;
height: 1;
min-width: 8;
}
DeviceTabs .tab-label.active {
background: $primary;
color: $text;
text-style: bold;
}
DeviceTabs .tab-label:hover {
background: $primary-darken-1;
}
DeviceTabs .header-label {
padding: 0 1;
height: 1;
color: $text-muted;
}
DeviceTabs .separator {
padding: 0;
height: 1;
color: $text-muted;
}
DeviceTabs .device-count {
dock: right;
padding: 0 1;
height: 1;
color: $text-muted;
}
"""
active_tab: reactive[str] = reactive("global")
devices_hidden: reactive[bool] = reactive(False)
class TabSelected(Message):
"""Posted when a tab is selected."""
def __init__(self, tab_id: str, device_id: str | None = None):
self.tab_id = tab_id
self.device_id = device_id
super().__init__()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._devices: list[str] = []
def compose(self):
yield Static("C3PO", classes="header-label", id="c3po-label")
yield Static(" \u2500 ", classes="separator")
yield Static("[G]lobal", classes="tab-label active", id="tab-global")
yield Static(" [H]ide", classes="tab-label", id="tab-hide")
yield Static("", classes="device-count", id="device-count")
def add_device(self, device_id: str):
"""Add a device tab."""
if device_id not in self._devices:
self._devices.append(device_id)
self._rebuild_tabs()
def remove_device(self, device_id: str):
"""Remove a device tab."""
if device_id in self._devices:
self._devices.remove(device_id)
if self.active_tab == device_id:
self.active_tab = "global"
self._rebuild_tabs()
def _rebuild_tabs(self):
"""Rebuild all tabs."""
for widget in list(self.children):
if hasattr(widget, 'id') and widget.id and widget.id.startswith("tab-device-"):
widget.remove()
hide_tab = self.query_one("#tab-hide", Static)
for i, device_id in enumerate(self._devices):
if i < 9:
label = f"[{i+1}]{device_id}"
tab = Static(
label,
classes="tab-label" + (" active" if self.active_tab == device_id else ""),
id=f"tab-device-{device_id}"
)
self.mount(tab, before=hide_tab)
count_label = self.query_one("#device-count", Static)
count_label.update(f"{len(self._devices)} device{'s' if len(self._devices) != 1 else ''}")
def select_tab(self, tab_id: str):
"""Select a tab by ID."""
if tab_id == "global":
self.active_tab = "global"
self.post_message(self.TabSelected("global"))
elif tab_id in self._devices:
self.active_tab = tab_id
self.post_message(self.TabSelected(tab_id, tab_id))
self._update_active_styles()
def select_by_index(self, index: int):
"""Select device tab by numeric index (1-9)."""
if 0 < index <= len(self._devices):
device_id = self._devices[index - 1]
self.select_tab(device_id)
def toggle_hide(self):
"""Toggle device panes visibility."""
self.devices_hidden = not self.devices_hidden
hide_tab = self.query_one("#tab-hide", Static)
hide_tab.update("[H]ide" if not self.devices_hidden else "[H]show")
def _update_active_styles(self):
"""Update tab styles to show active state."""
for tab in self.query(".tab-label"):
tab.remove_class("active")
if self.active_tab == "global":
self.query_one("#tab-global", Static).add_class("active")
else:
try:
self.query_one(f"#tab-device-{self.active_tab}", Static).add_class("active")
except Exception:
pass
def on_click(self, event) -> None:
"""Handle tab clicks."""
target = event.target
if hasattr(target, 'id') and target.id:
if target.id == "tab-global":
self.select_tab("global")
elif target.id == "tab-hide":
self.toggle_hide()
elif target.id.startswith("tab-device-"):
device_id = target.id.replace("tab-device-", "")
self.select_tab(device_id)