mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +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,
|
"inactivity_timeout": INACTIVITY_TIMEOUT,
|
||||||
"kdf_iterations": 50_000,
|
"kdf_iterations": 50_000,
|
||||||
"kdf_mode": "pbkdf2",
|
"kdf_mode": "pbkdf2",
|
||||||
|
"argon2_time_cost": 2,
|
||||||
"additional_backup_path": "",
|
"additional_backup_path": "",
|
||||||
"backup_interval": 0,
|
"backup_interval": 0,
|
||||||
"secret_mode_enabled": False,
|
"secret_mode_enabled": False,
|
||||||
@@ -76,6 +77,7 @@ class ConfigManager:
|
|||||||
data.setdefault("inactivity_timeout", INACTIVITY_TIMEOUT)
|
data.setdefault("inactivity_timeout", INACTIVITY_TIMEOUT)
|
||||||
data.setdefault("kdf_iterations", 50_000)
|
data.setdefault("kdf_iterations", 50_000)
|
||||||
data.setdefault("kdf_mode", "pbkdf2")
|
data.setdefault("kdf_mode", "pbkdf2")
|
||||||
|
data.setdefault("argon2_time_cost", 2)
|
||||||
data.setdefault("additional_backup_path", "")
|
data.setdefault("additional_backup_path", "")
|
||||||
data.setdefault("backup_interval", 0)
|
data.setdefault("backup_interval", 0)
|
||||||
data.setdefault("secret_mode_enabled", False)
|
data.setdefault("secret_mode_enabled", False)
|
||||||
@@ -196,6 +198,19 @@ class ConfigManager:
|
|||||||
config = self.load_config(require_pin=False)
|
config = self.load_config(require_pin=False)
|
||||||
return config.get("kdf_mode", "pbkdf2")
|
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:
|
def set_additional_backup_path(self, path: Optional[str]) -> None:
|
||||||
"""Persist an optional additional backup path in the config."""
|
"""Persist an optional additional backup path in the config."""
|
||||||
config = self.load_config(require_pin=False)
|
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 logging
|
||||||
import traceback
|
import traceback
|
||||||
import hmac
|
import hmac
|
||||||
|
import time
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
from bip_utils import Bip39SeedGenerator
|
from bip_utils import Bip39SeedGenerator
|
||||||
@@ -236,3 +237,55 @@ def derive_totp_secret(seed: str, index: int) -> str:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to derive TOTP secret: {e}", exc_info=True)
|
logger.error(f"Failed to derive TOTP secret: {e}", exc_info=True)
|
||||||
raise
|
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