Initial public release

This commit is contained in:
2026-07-17 15:29:53 -04:00
commit 2d71ce77a1
81 changed files with 32056 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
"""Encrypted-frame envelope shared by the variable-sync and live-keystroke
BLE channels.
Wire format (identical for both channels):
byte tag_len 1 byte
bytes tag ASCII "M5Stack|AA:BB:CC:DD:EE:FF"
bytes nonce 12 bytes
bytes ciphertext+gcm_tag N + 16 bytes (AES-256-GCM, AAD = tag bytes)
Each channel layers its own plaintext schema on top: the var-sync channel
uses JSON, the live-keystroke channel uses a compact binary protocol
defined in ble_live.py. Frames whose tag prefix isn't "M5Stack|" or whose
AEAD authentication fails are dropped silently by the parser.
"""
import os
import re
DEVICE_TAG_PREFIX = "M5Stack|"
MAC_RE = re.compile(r"^[0-9A-F]{2}(:[0-9A-F]{2}){5}$")
def build_frame(key: bytes, tag: str, plaintext: bytes) -> bytes:
"""Build an encrypted, MAC-tagged frame.
`tag` is the device-tag string ("M5Stack|AA:BB:..."); it is bound to
the ciphertext via GCM AAD and also written in plaintext at the head
of the frame so the receiver can read it before decrypting.
"""
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
tag_bytes = tag.encode("ascii")
if len(tag_bytes) > 255:
raise ValueError("device tag too long")
nonce = os.urandom(12)
ct = AESGCM(key).encrypt(nonce, plaintext, tag_bytes)
return bytes([len(tag_bytes)]) + tag_bytes + nonce + ct
def extract_tag(frame: bytes):
"""Pull the device tag (e.g. "M5Stack|AA:BB:CC:DD:EE:FF") out of
the frame header WITHOUT decrypting. Returns the tag string or
None if the frame is malformed.
Used by the BLE layer to select the right per-MAC key before
attempting AES-GCM authentication.
"""
if len(frame) < 1 + 12 + 16:
return None
tag_len = frame[0]
if tag_len == 0 or len(frame) < 1 + tag_len + 12 + 16:
return None
tag_bytes = bytes(frame[1:1 + tag_len])
try:
tag_str = tag_bytes.decode("ascii")
except UnicodeDecodeError:
return None
if not tag_str.startswith(DEVICE_TAG_PREFIX):
return None
mac = tag_str[len(DEVICE_TAG_PREFIX):]
if not MAC_RE.match(mac):
return None
return tag_str
def parse_frame(key: bytes, frame: bytes):
"""Return (tag_str, plaintext_bytes) on success, or None on any failure.
Returns None silently for malformed / wrong-prefix / authentication-
failed frames so callers can drop them without leaking timing info.
"""
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.exceptions import InvalidTag
tag_str = extract_tag(frame)
if tag_str is None:
return None
tag_len = frame[0]
tag_bytes = bytes(frame[1:1 + tag_len])
nonce = bytes(frame[1 + tag_len:1 + tag_len + 12])
ct = bytes(frame[1 + tag_len + 12:])
try:
pt = AESGCM(key).decrypt(nonce, ct, tag_bytes)
except InvalidTag:
return None
except Exception:
return None
return tag_str, pt
def parse_frame_auto(frame: bytes):
"""Decode a frame using the right per-MAC key automatically.
Reads the tag from the frame header, asks ble_keystore for the
matching key (falling back to the legacy single-key file), then
AES-GCM-decrypts. Returns (tag_str, plaintext_bytes) on success,
or (tag_str, None) if a tag was readable but no matching key was
available (so callers can surface a clear "unknown device" error
instead of a silent auth fail), or None for fully malformed input.
"""
import ble_keystore
tag_str = extract_tag(frame)
if tag_str is None:
return None
mac = tag_str[len(DEVICE_TAG_PREFIX):]
key = ble_keystore.load_key_for_mac_or_default(mac)
if key is None:
return (tag_str, None)
parsed = parse_frame(key, frame)
if parsed is None:
# Key existed but didn't authenticate — surface as a separate
# signal (None for plaintext) so the live client can emit
# KEY_MISMATCH for this specific device.
return (tag_str, None)
return parsed