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
59 lines
1.8 KiB
Python
59 lines
1.8 KiB
Python
import base64
|
|
|
|
from Crypto.Cipher import ChaCha20_Poly1305
|
|
from Crypto.Protocol.KDF import HKDF
|
|
from Crypto.Hash import SHA256
|
|
|
|
HKDF_INFO = b"espilon-c2-v1"
|
|
|
|
|
|
class CryptoContext:
|
|
"""Per-device AEAD crypto context.
|
|
|
|
Derives a 32-byte encryption key from the device's master key
|
|
using HKDF-SHA256 with device_id as salt.
|
|
"""
|
|
|
|
def __init__(self, master_key: bytes, device_id: str):
|
|
if len(master_key) != 32:
|
|
raise ValueError(f"master_key must be 32 bytes, got {len(master_key)}")
|
|
self.derived_key = HKDF(
|
|
master=master_key,
|
|
key_len=32,
|
|
salt=device_id.encode(),
|
|
hashmod=SHA256,
|
|
context=HKDF_INFO,
|
|
)
|
|
|
|
# =========================
|
|
# ChaCha20-Poly1305 AEAD
|
|
# =========================
|
|
def encrypt(self, data: bytes) -> bytes:
|
|
"""Encrypt and authenticate. Returns nonce[12] || ciphertext || tag[16]."""
|
|
cipher = ChaCha20_Poly1305.new(key=self.derived_key)
|
|
ct, tag = cipher.encrypt_and_digest(data)
|
|
return cipher.nonce + ct + tag
|
|
|
|
def decrypt(self, data: bytes) -> bytes:
|
|
"""Decrypt and verify. Input: nonce[12] || ciphertext || tag[16].
|
|
Raises ValueError on authentication failure.
|
|
"""
|
|
if len(data) < 28:
|
|
raise ValueError(f"Encrypted payload too short ({len(data)} bytes)")
|
|
nonce = data[:12]
|
|
tag = data[-16:]
|
|
ct = data[12:-16]
|
|
cipher = ChaCha20_Poly1305.new(key=self.derived_key, nonce=nonce)
|
|
return cipher.decrypt_and_verify(ct, tag)
|
|
|
|
# =========================
|
|
# Base64
|
|
# =========================
|
|
@staticmethod
|
|
def b64_encode(data: bytes) -> bytes:
|
|
return base64.b64encode(data)
|
|
|
|
@staticmethod
|
|
def b64_decode(data: bytes) -> bytes:
|
|
return base64.b64decode(data)
|