From 3f2a59c11e97b90da8d39e8d8c5ecef22d84b572 Mon Sep 17 00:00:00 2001 From: "Aaron D. Lee" Date: Fri, 3 Apr 2026 17:37:42 -0400 Subject: [PATCH] feat(F4): add PIN hashing utilities with PBKDF2-SHA256 --- tests/unit/test_pin.py | 39 +++++++++++++++++++++++++++++++++++++++ vigilar/alerts/pin.py | 22 ++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 tests/unit/test_pin.py create mode 100644 vigilar/alerts/pin.py diff --git a/tests/unit/test_pin.py b/tests/unit/test_pin.py new file mode 100644 index 0000000..852ce10 --- /dev/null +++ b/tests/unit/test_pin.py @@ -0,0 +1,39 @@ +"""Tests for PIN hashing and verification.""" + +from vigilar.alerts.pin import hash_pin, verify_pin + + +def test_hash_pin_returns_formatted_string(): + result = hash_pin("1234") + parts = result.split("$") + assert len(parts) == 3 + assert parts[0] == "pbkdf2_sha256" + assert len(parts[1]) == 32 # 16 bytes hex = 32 chars + assert len(parts[2]) == 64 # 32 bytes hex = 64 chars + + +def test_verify_pin_correct(): + stored = hash_pin("5678") + assert verify_pin("5678", stored) is True + + +def test_verify_pin_wrong(): + stored = hash_pin("5678") + assert verify_pin("0000", stored) is False + + +def test_verify_pin_empty_hash_returns_true(): + assert verify_pin("1234", "") is True + assert verify_pin("", "") is True + + +def test_hash_pin_different_salts(): + h1 = hash_pin("1234") + h2 = hash_pin("1234") + assert h1 != h2 + + +def test_verify_pin_handles_unicode(): + stored = hash_pin("p@ss!") + assert verify_pin("p@ss!", stored) is True + assert verify_pin("p@ss?", stored) is False diff --git a/vigilar/alerts/pin.py b/vigilar/alerts/pin.py new file mode 100644 index 0000000..afd660a --- /dev/null +++ b/vigilar/alerts/pin.py @@ -0,0 +1,22 @@ +"""PIN hashing and verification using PBKDF2-SHA256.""" + +import hashlib +import os + + +def hash_pin(pin: str) -> str: + salt = os.urandom(16) + dk = hashlib.pbkdf2_hmac("sha256", pin.encode(), salt, iterations=600_000) + return f"pbkdf2_sha256${salt.hex()}${dk.hex()}" + + +def verify_pin(pin: str, stored_hash: str) -> bool: + if not stored_hash: + return True + parts = stored_hash.split("$") + if len(parts) != 3 or parts[0] != "pbkdf2_sha256": + return False + salt = bytes.fromhex(parts[1]) + expected = parts[2] + dk = hashlib.pbkdf2_hmac("sha256", pin.encode(), salt, iterations=600_000) + return dk.hex() == expected