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 cdd8cf4693
commit 7fda351c02
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."""
import hashlib
import time
import pytest
@@ -19,7 +18,7 @@ from vigilar.storage.queries import insert_event
def _make_config(rules=None, pin_hash=""):
return VigilarConfig(
system={"arm_pin_hash": pin_hash},
security={"pin_hash": pin_hash},
cameras=[],
sensors=[],
rules=rules or [],
@@ -27,7 +26,8 @@ def _make_config(rules=None, pin_hash=""):
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."""
import hashlib
import hmac
import logging
import time
from sqlalchemy.engine import Engine
from vigilar.alerts.pin import verify_pin as _verify_pin_hash
from vigilar.config import VigilarConfig
from vigilar.constants import ArmState, EventType, Severity, Topics
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):
self._engine = engine
self._pin_hash = config.system.arm_pin_hash
self._pin_hash = config.security.pin_hash
self._state = ArmState.DISARMED
self._bus = None
self._load_initial_state()
@@ -43,12 +42,11 @@ class ArmStateFSM:
return self._state
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:
# No PIN configured — allow all transitions
return True
candidate = hashlib.sha256(pin.encode()).hexdigest()
return hmac.compare_digest(candidate, self._pin_hash)
return _verify_pin_hash(pin, self._pin_hash)
def transition(
self,
@@ -68,9 +66,8 @@ class ArmStateFSM:
old_state = self._state
self._state = new_state
# Log to database
pin_hash = hashlib.sha256(pin.encode()).hexdigest() if pin else None
insert_arm_state(self._engine, new_state.value, triggered_by, pin_hash)
# Log to database (pin_hash column is no longer populated — see #2)
insert_arm_state(self._engine, new_state.value, triggered_by, None)
# Log event
insert_event(