feat(F4): PIN verification on arm/disarm + reset-pin endpoint
Adds PIN checking to arm/disarm endpoints using verify_pin() against cfg.security.pin_hash, and a new POST /system/api/reset-pin endpoint that verifies the recovery passphrase before updating the PIN hash. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f8d28cf78e
commit
e630c206b2
79
tests/unit/test_system_pin.py
Normal file
79
tests/unit/test_system_pin.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
"""Tests for PIN verification on arm/disarm endpoints."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from vigilar.alerts.pin import hash_pin
|
||||||
|
from vigilar.config import VigilarConfig, SecurityConfig
|
||||||
|
from vigilar.web.app import create_app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app_with_pin():
|
||||||
|
pin_hash = hash_pin("1234")
|
||||||
|
cfg = VigilarConfig(
|
||||||
|
security=SecurityConfig(
|
||||||
|
pin_hash=pin_hash,
|
||||||
|
recovery_passphrase_hash=hash_pin("recover123"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
app = create_app(cfg)
|
||||||
|
app.config["TESTING"] = True
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app_no_pin():
|
||||||
|
cfg = VigilarConfig()
|
||||||
|
app = create_app(cfg)
|
||||||
|
app.config["TESTING"] = True
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def test_arm_without_pin_set(app_no_pin):
|
||||||
|
with app_no_pin.test_client() as c:
|
||||||
|
rv = c.post("/system/api/arm", json={"mode": "ARMED_AWAY"})
|
||||||
|
assert rv.status_code == 200
|
||||||
|
assert rv.get_json()["ok"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_arm_correct_pin(app_with_pin):
|
||||||
|
with app_with_pin.test_client() as c:
|
||||||
|
rv = c.post("/system/api/arm", json={"mode": "ARMED_AWAY", "pin": "1234"})
|
||||||
|
assert rv.status_code == 200
|
||||||
|
assert rv.get_json()["ok"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_arm_wrong_pin(app_with_pin):
|
||||||
|
with app_with_pin.test_client() as c:
|
||||||
|
rv = c.post("/system/api/arm", json={"mode": "ARMED_AWAY", "pin": "0000"})
|
||||||
|
assert rv.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_disarm_correct_pin(app_with_pin):
|
||||||
|
with app_with_pin.test_client() as c:
|
||||||
|
rv = c.post("/system/api/disarm", json={"pin": "1234"})
|
||||||
|
assert rv.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_disarm_wrong_pin(app_with_pin):
|
||||||
|
with app_with_pin.test_client() as c:
|
||||||
|
rv = c.post("/system/api/disarm", json={"pin": "9999"})
|
||||||
|
assert rv.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_reset_pin_correct_passphrase(app_with_pin):
|
||||||
|
with app_with_pin.test_client() as c:
|
||||||
|
rv = c.post("/system/api/reset-pin", json={
|
||||||
|
"recovery_passphrase": "recover123",
|
||||||
|
"new_pin": "5678",
|
||||||
|
})
|
||||||
|
assert rv.status_code == 200
|
||||||
|
assert rv.get_json()["ok"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_reset_pin_wrong_passphrase(app_with_pin):
|
||||||
|
with app_with_pin.test_client() as c:
|
||||||
|
rv = c.post("/system/api/reset-pin", json={
|
||||||
|
"recovery_passphrase": "wrong",
|
||||||
|
"new_pin": "5678",
|
||||||
|
})
|
||||||
|
assert rv.status_code == 401
|
||||||
@ -4,6 +4,7 @@ import os
|
|||||||
|
|
||||||
from flask import Blueprint, current_app, jsonify, render_template, request
|
from flask import Blueprint, current_app, jsonify, render_template, request
|
||||||
|
|
||||||
|
from vigilar.alerts.pin import hash_pin, verify_pin
|
||||||
from vigilar.config import VigilarConfig
|
from vigilar.config import VigilarConfig
|
||||||
from vigilar.config_writer import (
|
from vigilar.config_writer import (
|
||||||
save_config,
|
save_config,
|
||||||
@ -58,7 +59,10 @@ def arm_system():
|
|||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
mode = data.get("mode", "ARMED_AWAY")
|
mode = data.get("mode", "ARMED_AWAY")
|
||||||
pin = data.get("pin", "")
|
pin = data.get("pin", "")
|
||||||
# TODO: verify PIN against config hash
|
cfg = _get_cfg()
|
||||||
|
pin_hash = cfg.security.pin_hash
|
||||||
|
if pin_hash and not verify_pin(pin, pin_hash):
|
||||||
|
return jsonify({"error": "Invalid PIN"}), 401
|
||||||
return jsonify({"ok": True, "state": mode})
|
return jsonify({"ok": True, "state": mode})
|
||||||
|
|
||||||
|
|
||||||
@ -66,10 +70,30 @@ def arm_system():
|
|||||||
def disarm_system():
|
def disarm_system():
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
pin = data.get("pin", "")
|
pin = data.get("pin", "")
|
||||||
# TODO: verify PIN
|
cfg = _get_cfg()
|
||||||
|
pin_hash = cfg.security.pin_hash
|
||||||
|
if pin_hash and not verify_pin(pin, pin_hash):
|
||||||
|
return jsonify({"error": "Invalid PIN"}), 401
|
||||||
return jsonify({"ok": True, "state": "DISARMED"})
|
return jsonify({"ok": True, "state": "DISARMED"})
|
||||||
|
|
||||||
|
|
||||||
|
@system_bp.route("/api/reset-pin", methods=["POST"])
|
||||||
|
def reset_pin():
|
||||||
|
data = request.get_json() or {}
|
||||||
|
recovery_passphrase = data.get("recovery_passphrase", "")
|
||||||
|
new_pin = data.get("new_pin", "")
|
||||||
|
cfg = _get_cfg()
|
||||||
|
if not verify_pin(recovery_passphrase, cfg.security.recovery_passphrase_hash):
|
||||||
|
return jsonify({"error": "Invalid recovery passphrase"}), 401
|
||||||
|
new_security = cfg.security.model_copy(update={"pin_hash": hash_pin(new_pin)})
|
||||||
|
new_cfg = cfg.model_copy(update={"security": new_security})
|
||||||
|
try:
|
||||||
|
_save_and_reload(new_cfg)
|
||||||
|
except Exception:
|
||||||
|
current_app.config["VIGILAR_CONFIG"] = new_cfg
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
# --- Config Read API ---
|
# --- Config Read API ---
|
||||||
|
|
||||||
@system_bp.route("/api/config")
|
@system_bp.route("/api/config")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user