fix(events): ArmStateFSM uses PBKDF2 via alerts.pin (issue #2)

Was: unsalted SHA-256 read from [system] arm_pin_hash.
Now: PBKDF2-SHA256 600k iterations read from [security] pin_hash,
matching the web arm/disarm path and the alerts/pin module.

Also drops the redundant pin re-hash on the arm_state_log audit row
(a fresh PBKDF2 salt made the column valueless for traceability).

Part of issue #2 PIN hashing unification.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-05 11:26:07 -04:00
parent c64f863741
commit efa3ce4b1b
2 changed files with 9 additions and 12 deletions

View File

@@ -1,6 +1,5 @@
"""Tests for the Phase 6 events subsystem: rules, arm state FSM, history.""" """Tests for the Phase 6 events subsystem: rules, arm state FSM, history."""
import hashlib
import time import time
import pytest import pytest
@@ -19,7 +18,7 @@ from vigilar.storage.queries import insert_event
def _make_config(rules=None, pin_hash=""): def _make_config(rules=None, pin_hash=""):
return VigilarConfig( return VigilarConfig(
system={"arm_pin_hash": pin_hash}, security={"pin_hash": pin_hash},
cameras=[], cameras=[],
sensors=[], sensors=[],
rules=rules or [], rules=rules or [],
@@ -27,7 +26,8 @@ def _make_config(rules=None, pin_hash=""):
def _pin_hash(pin: str) -> str: def _pin_hash(pin: str) -> str:
return hashlib.sha256(pin.encode()).hexdigest() from vigilar.alerts.pin import hash_pin
return hash_pin(pin)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@@ -1,12 +1,11 @@
"""Arm state finite state machine.""" """Arm state finite state machine."""
import hashlib
import hmac
import logging import logging
import time import time
from sqlalchemy.engine import Engine from sqlalchemy.engine import Engine
from vigilar.alerts.pin import verify_pin as _verify_pin_hash
from vigilar.config import VigilarConfig from vigilar.config import VigilarConfig
from vigilar.constants import ArmState, EventType, Severity, Topics from vigilar.constants import ArmState, EventType, Severity, Topics
from vigilar.storage.queries import get_current_arm_state, insert_arm_state, insert_event from vigilar.storage.queries import get_current_arm_state, insert_arm_state, insert_event
@@ -19,7 +18,7 @@ class ArmStateFSM:
def __init__(self, engine: Engine, config: VigilarConfig): def __init__(self, engine: Engine, config: VigilarConfig):
self._engine = engine self._engine = engine
self._pin_hash = config.system.arm_pin_hash self._pin_hash = config.security.pin_hash
self._state = ArmState.DISARMED self._state = ArmState.DISARMED
self._bus = None self._bus = None
self._load_initial_state() self._load_initial_state()
@@ -43,12 +42,11 @@ class ArmStateFSM:
return self._state return self._state
def verify_pin(self, pin: str) -> bool: def verify_pin(self, pin: str) -> bool:
"""Verify a PIN against the stored hash using HMAC comparison.""" """Verify a PIN against the stored PBKDF2 hash."""
if not self._pin_hash: if not self._pin_hash:
# No PIN configured — allow all transitions # No PIN configured — allow all transitions
return True return True
candidate = hashlib.sha256(pin.encode()).hexdigest() return _verify_pin_hash(pin, self._pin_hash)
return hmac.compare_digest(candidate, self._pin_hash)
def transition( def transition(
self, self,
@@ -68,9 +66,8 @@ class ArmStateFSM:
old_state = self._state old_state = self._state
self._state = new_state self._state = new_state
# Log to database # Log to database (pin_hash column is no longer populated — see #2)
pin_hash = hashlib.sha256(pin.encode()).hexdigest() if pin else None insert_arm_state(self._engine, new_state.value, triggered_by, None)
insert_arm_state(self._engine, new_state.value, triggered_by, pin_hash)
# Log event # Log event
insert_event( insert_event(