mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-07 14:58:56 +00:00
Add Argon2 calibration and encryption fuzz tests
This commit is contained in:
30
docs/security.md
Normal file
30
docs/security.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Security Testing and Calibration
|
||||
|
||||
This project includes fuzz tests and a calibration routine to tune Argon2 parameters for your hardware.
|
||||
|
||||
## Running Fuzz Tests
|
||||
|
||||
The fuzz tests exercise encryption and decryption with random data using [Hypothesis](https://hypothesis.readthedocs.io/).
|
||||
Activate the project's virtual environment and run:
|
||||
|
||||
```bash
|
||||
pytest src/tests/test_encryption_fuzz.py
|
||||
```
|
||||
|
||||
Running the entire test suite will also execute these fuzz tests.
|
||||
|
||||
## Calibrating Argon2 Time Cost
|
||||
|
||||
Argon2 performance varies by device. To calibrate the `time_cost` parameter, run the helper function:
|
||||
|
||||
```bash
|
||||
python - <<'PY'
|
||||
from seedpass.core.config_manager import ConfigManager
|
||||
from utils.key_derivation import calibrate_argon2_time_cost
|
||||
|
||||
# assuming ``cfg`` is a ConfigManager for your profile
|
||||
calibrate_argon2_time_cost(cfg)
|
||||
PY
|
||||
```
|
||||
|
||||
The selected `time_cost` is stored in the profile's configuration and used for subsequent key derivations.
|
@@ -47,6 +47,7 @@ class ConfigManager:
|
||||
"inactivity_timeout": INACTIVITY_TIMEOUT,
|
||||
"kdf_iterations": 50_000,
|
||||
"kdf_mode": "pbkdf2",
|
||||
"argon2_time_cost": 2,
|
||||
"additional_backup_path": "",
|
||||
"backup_interval": 0,
|
||||
"secret_mode_enabled": False,
|
||||
@@ -76,6 +77,7 @@ class ConfigManager:
|
||||
data.setdefault("inactivity_timeout", INACTIVITY_TIMEOUT)
|
||||
data.setdefault("kdf_iterations", 50_000)
|
||||
data.setdefault("kdf_mode", "pbkdf2")
|
||||
data.setdefault("argon2_time_cost", 2)
|
||||
data.setdefault("additional_backup_path", "")
|
||||
data.setdefault("backup_interval", 0)
|
||||
data.setdefault("secret_mode_enabled", False)
|
||||
@@ -196,6 +198,19 @@ class ConfigManager:
|
||||
config = self.load_config(require_pin=False)
|
||||
return config.get("kdf_mode", "pbkdf2")
|
||||
|
||||
def set_argon2_time_cost(self, time_cost: int) -> None:
|
||||
"""Persist the Argon2 ``time_cost`` parameter."""
|
||||
if time_cost <= 0:
|
||||
raise ValueError("time_cost must be positive")
|
||||
config = self.load_config(require_pin=False)
|
||||
config["argon2_time_cost"] = int(time_cost)
|
||||
self.save_config(config)
|
||||
|
||||
def get_argon2_time_cost(self) -> int:
|
||||
"""Retrieve the Argon2 ``time_cost`` setting."""
|
||||
config = self.load_config(require_pin=False)
|
||||
return int(config.get("argon2_time_cost", 2))
|
||||
|
||||
def set_additional_backup_path(self, path: Optional[str]) -> None:
|
||||
"""Persist an optional additional backup path in the config."""
|
||||
config = self.load_config(require_pin=False)
|
||||
|
37
src/tests/test_encryption_fuzz.py
Normal file
37
src/tests/test_encryption_fuzz.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from cryptography.fernet import Fernet
|
||||
from hypothesis import given, strategies as st, settings, HealthCheck
|
||||
|
||||
from seedpass.core.encryption import EncryptionManager
|
||||
|
||||
|
||||
@given(blob=st.binary())
|
||||
@settings(
|
||||
deadline=None,
|
||||
max_examples=25,
|
||||
suppress_health_check=[HealthCheck.function_scoped_fixture],
|
||||
)
|
||||
def test_encrypt_decrypt_roundtrip(blob: bytes, tmp_path: Path) -> None:
|
||||
"""Ensure arbitrary data round-trips through EncryptionManager."""
|
||||
key = Fernet.generate_key()
|
||||
mgr = EncryptionManager(key, tmp_path)
|
||||
encrypted = mgr.encrypt_data(blob)
|
||||
assert mgr.decrypt_data(encrypted) == blob
|
||||
|
||||
|
||||
@given(blob=st.binary())
|
||||
@settings(
|
||||
deadline=None,
|
||||
max_examples=25,
|
||||
suppress_health_check=[HealthCheck.function_scoped_fixture],
|
||||
)
|
||||
def test_corrupted_ciphertext_fails(blob: bytes, tmp_path: Path) -> None:
|
||||
"""Corrupted ciphertext should not decrypt successfully."""
|
||||
key = Fernet.generate_key()
|
||||
mgr = EncryptionManager(key, tmp_path)
|
||||
encrypted = bytearray(mgr.encrypt_data(blob))
|
||||
if encrypted:
|
||||
encrypted[0] ^= 0xFF
|
||||
with pytest.raises(Exception):
|
||||
mgr.decrypt_data(bytes(encrypted))
|
@@ -21,6 +21,7 @@ import unicodedata
|
||||
import logging
|
||||
import traceback
|
||||
import hmac
|
||||
import time
|
||||
from enum import Enum
|
||||
from typing import Optional, Union
|
||||
from bip_utils import Bip39SeedGenerator
|
||||
@@ -236,3 +237,55 @@ def derive_totp_secret(seed: str, index: int) -> str:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to derive TOTP secret: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
def calibrate_argon2_time_cost(
|
||||
cfg_mgr: "ConfigManager",
|
||||
*,
|
||||
target_ms: float = 500.0,
|
||||
max_time_cost: int = 6,
|
||||
) -> int:
|
||||
"""Calibrate Argon2 ``time_cost`` based on device performance.
|
||||
|
||||
Runs :func:`derive_key_from_password_argon2` with increasing ``time_cost``
|
||||
until the runtime meets or exceeds ``target_ms``. The chosen ``time_cost``
|
||||
is stored in ``cfg_mgr`` via ``set_argon2_time_cost`` and returned.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg_mgr:
|
||||
Instance of :class:`~seedpass.core.config_manager.ConfigManager` used to
|
||||
persist the calibrated ``time_cost``.
|
||||
target_ms:
|
||||
Desired minimum execution time in milliseconds for one Argon2 hash.
|
||||
max_time_cost:
|
||||
Upper bound for ``time_cost`` to prevent excessively long calibration.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
Selected ``time_cost`` value.
|
||||
"""
|
||||
|
||||
password = "benchmark"
|
||||
fingerprint = b"argon2-calibration"
|
||||
time_cost = 1
|
||||
elapsed_ms = 0.0
|
||||
while time_cost <= max_time_cost:
|
||||
start = time.perf_counter()
|
||||
derive_key_from_password_argon2(
|
||||
password,
|
||||
fingerprint,
|
||||
time_cost=time_cost,
|
||||
memory_cost=8,
|
||||
parallelism=1,
|
||||
)
|
||||
elapsed_ms = (time.perf_counter() - start) * 1000
|
||||
if elapsed_ms >= target_ms:
|
||||
break
|
||||
time_cost += 1
|
||||
|
||||
cfg_mgr.set_argon2_time_cost(time_cost)
|
||||
if cfg_mgr.load_config(require_pin=False).get("verbose_timing"):
|
||||
logger.info("Calibrated Argon2 time_cost=%s (%.2f ms)", time_cost, elapsed_ms)
|
||||
return time_cost
|
||||
|
Reference in New Issue
Block a user