- C1: stop feeding the unconsumed barge-in TTSQueue in the reply path (deadlocked after 64 chunks on a real long reply) - C2: encode reply Opus at the TTS/codec sample rate (24 kHz), not hardcoded 16 kHz; encoder now rate-generic - M3: bound the mic PCM buffer to the 60 s max utterance - M4: tts_sentence_start per speakable sentence, not per LLM token - Minor: send tts.stop on a failed turn; constant-time token compare - Sec: untrack + gitignore config.yaml; add config.example.yaml with placeholder secrets - Tests: 23 pass (2 new regressions: long-reply deadlock, rate-generic encoder)
38 lines
1.2 KiB
Python
38 lines
1.2 KiB
Python
"""Device authentication (REQ-009).
|
|
|
|
Handshake carries Device-Id + Authorization. A device is allowed only if its
|
|
device_id is on the allowlist AND its token hashes match. No device = no
|
|
session. This is what keeps the bridge the *only* internet-exposed surface
|
|
(MUST-NOT-002): everything behind it is LAN/localhost.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
|
|
|
|
class AuthError(Exception):
|
|
pass
|
|
|
|
|
|
class DeviceAuth:
|
|
def __init__(self, devices: dict[str, str], *, enabled: bool = True) -> None:
|
|
# device_id -> token_hash
|
|
self._devices = devices
|
|
self.enabled = enabled
|
|
|
|
@staticmethod
|
|
def hash_token(token: str) -> str:
|
|
return hashlib.sha256(token.encode("utf-8")).hexdigest()
|
|
|
|
def authenticate(self, device_id: str, token: str) -> bool:
|
|
if not self.enabled:
|
|
return True
|
|
expected = self._devices.get(device_id)
|
|
if expected is None:
|
|
raise AuthError(f"device not allowed: {device_id}")
|
|
got = self.hash_token(token)
|
|
if not hmac.compare_digest(got.encode(), expected.encode()):
|
|
raise AuthError(f"bad token for device: {device_id}")
|
|
return True
|