mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Merge pull request #500 from PR0M3TH3AN/codex/add-argon2-support-to-password-manager
Add argon2 KDF option
This commit is contained in:
@@ -2,6 +2,7 @@ aiohappyeyeballs==2.6.1
|
||||
aiohttp==3.12.13
|
||||
aiosignal==1.3.2
|
||||
attrs==25.3.0
|
||||
argon2-cffi==23.1.0
|
||||
base58==2.1.1
|
||||
bcrypt==4.3.0
|
||||
bech32==1.2.0
|
||||
|
@@ -45,6 +45,7 @@ class ConfigManager:
|
||||
"password_hash": "",
|
||||
"inactivity_timeout": INACTIVITY_TIMEOUT,
|
||||
"kdf_iterations": 100_000,
|
||||
"kdf_mode": "pbkdf2",
|
||||
"additional_backup_path": "",
|
||||
"backup_interval": 0,
|
||||
"secret_mode_enabled": False,
|
||||
@@ -60,6 +61,7 @@ class ConfigManager:
|
||||
data.setdefault("password_hash", "")
|
||||
data.setdefault("inactivity_timeout", INACTIVITY_TIMEOUT)
|
||||
data.setdefault("kdf_iterations", 100_000)
|
||||
data.setdefault("kdf_mode", "pbkdf2")
|
||||
data.setdefault("additional_backup_path", "")
|
||||
data.setdefault("backup_interval", 0)
|
||||
data.setdefault("secret_mode_enabled", False)
|
||||
@@ -155,6 +157,19 @@ class ConfigManager:
|
||||
config = self.load_config(require_pin=False)
|
||||
return int(config.get("kdf_iterations", 100_000))
|
||||
|
||||
def set_kdf_mode(self, mode: str) -> None:
|
||||
"""Persist the key derivation function mode."""
|
||||
if mode not in ("pbkdf2", "argon2"):
|
||||
raise ValueError("kdf_mode must be 'pbkdf2' or 'argon2'")
|
||||
config = self.load_config(require_pin=False)
|
||||
config["kdf_mode"] = mode
|
||||
self.save_config(config)
|
||||
|
||||
def get_kdf_mode(self) -> str:
|
||||
"""Retrieve the configured key derivation function."""
|
||||
config = self.load_config(require_pin=False)
|
||||
return config.get("kdf_mode", "pbkdf2")
|
||||
|
||||
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)
|
||||
|
@@ -35,6 +35,7 @@ from password_manager.entry_types import EntryType
|
||||
from utils.key_derivation import (
|
||||
derive_key_from_parent_seed,
|
||||
derive_key_from_password,
|
||||
derive_key_from_password_argon2,
|
||||
derive_index_key,
|
||||
EncryptionMode,
|
||||
)
|
||||
@@ -387,13 +388,21 @@ class PasswordManager:
|
||||
if password is None:
|
||||
password = prompt_existing_password("Enter your master password: ")
|
||||
|
||||
mode = (
|
||||
self.config_manager.get_kdf_mode()
|
||||
if getattr(self, "config_manager", None)
|
||||
else "pbkdf2"
|
||||
)
|
||||
iterations = (
|
||||
self.config_manager.get_kdf_iterations()
|
||||
if getattr(self, "config_manager", None)
|
||||
else 100_000
|
||||
)
|
||||
print("Deriving key...")
|
||||
seed_key = derive_key_from_password(password, iterations=iterations)
|
||||
if mode == "argon2":
|
||||
seed_key = derive_key_from_password_argon2(password)
|
||||
else:
|
||||
seed_key = derive_key_from_password(password, iterations=iterations)
|
||||
seed_mgr = EncryptionManager(seed_key, fingerprint_dir)
|
||||
print("Decrypting seed...")
|
||||
try:
|
||||
@@ -448,12 +457,20 @@ class PasswordManager:
|
||||
password = prompt_existing_password("Enter your master password: ")
|
||||
|
||||
try:
|
||||
mode = (
|
||||
self.config_manager.get_kdf_mode()
|
||||
if getattr(self, "config_manager", None)
|
||||
else "pbkdf2"
|
||||
)
|
||||
iterations = (
|
||||
self.config_manager.get_kdf_iterations()
|
||||
if getattr(self, "config_manager", None)
|
||||
else 100_000
|
||||
)
|
||||
seed_key = derive_key_from_password(password, iterations=iterations)
|
||||
if mode == "argon2":
|
||||
seed_key = derive_key_from_password_argon2(password)
|
||||
else:
|
||||
seed_key = derive_key_from_password(password, iterations=iterations)
|
||||
seed_mgr = EncryptionManager(seed_key, fingerprint_dir)
|
||||
self.parent_seed = seed_mgr.decrypt_parent_seed()
|
||||
seed_bytes = Bip39SeedGenerator(self.parent_seed).Generate()
|
||||
|
@@ -31,3 +31,4 @@ httpx>=0.28.1
|
||||
requests>=2.32
|
||||
python-multipart
|
||||
orjson
|
||||
argon2-cffi
|
||||
|
@@ -462,6 +462,7 @@ def config_set(ctx: typer.Context, key: str, value: str) -> None:
|
||||
[r.strip() for r in v.split(",") if r.strip()], require_pin=False
|
||||
),
|
||||
"kdf_iterations": lambda v: cfg.set_kdf_iterations(int(v)),
|
||||
"kdf_mode": lambda v: cfg.set_kdf_mode(v),
|
||||
"backup_interval": lambda v: cfg.set_backup_interval(float(v)),
|
||||
}
|
||||
|
||||
|
@@ -16,6 +16,7 @@ runner = CliRunner()
|
||||
("additional_backup_path", "", "set_additional_backup_path", None),
|
||||
("backup_interval", "5", "set_backup_interval", 5.0),
|
||||
("kdf_iterations", "123", "set_kdf_iterations", 123),
|
||||
("kdf_mode", "argon2", "set_kdf_mode", "argon2"),
|
||||
(
|
||||
"relays",
|
||||
"wss://a.com, wss://b.com",
|
||||
|
75
src/tests/test_kdf_modes.py
Normal file
75
src/tests/test_kdf_modes.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import bcrypt
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from types import SimpleNamespace
|
||||
|
||||
from utils.key_derivation import (
|
||||
derive_key_from_password,
|
||||
derive_key_from_password_argon2,
|
||||
derive_index_key,
|
||||
)
|
||||
from password_manager.encryption import EncryptionManager
|
||||
from password_manager.vault import Vault
|
||||
from password_manager.config_manager import ConfigManager
|
||||
from password_manager.manager import PasswordManager, EncryptionMode
|
||||
|
||||
TEST_SEED = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
TEST_PASSWORD = "pw"
|
||||
|
||||
|
||||
def _setup_profile(tmp: Path, mode: str):
|
||||
argon_kwargs = dict(time_cost=1, memory_cost=8, parallelism=1)
|
||||
if mode == "argon2":
|
||||
seed_key = derive_key_from_password_argon2(TEST_PASSWORD, **argon_kwargs)
|
||||
else:
|
||||
seed_key = derive_key_from_password(TEST_PASSWORD, iterations=1)
|
||||
EncryptionManager(seed_key, tmp).encrypt_parent_seed(TEST_SEED)
|
||||
|
||||
index_key = derive_index_key(TEST_SEED)
|
||||
enc_mgr = EncryptionManager(index_key, tmp)
|
||||
vault = Vault(enc_mgr, tmp)
|
||||
cfg_mgr = ConfigManager(vault, tmp)
|
||||
cfg = cfg_mgr.load_config(require_pin=False)
|
||||
cfg["password_hash"] = bcrypt.hashpw(
|
||||
TEST_PASSWORD.encode(), bcrypt.gensalt()
|
||||
).decode()
|
||||
cfg["kdf_mode"] = mode
|
||||
cfg["kdf_iterations"] = 1
|
||||
cfg_mgr.save_config(cfg)
|
||||
return cfg_mgr
|
||||
|
||||
|
||||
def _make_pm(tmp: Path, cfg: ConfigManager):
|
||||
pm = PasswordManager.__new__(PasswordManager)
|
||||
pm.encryption_mode = EncryptionMode.SEED_ONLY
|
||||
pm.config_manager = cfg
|
||||
pm.fingerprint_dir = tmp
|
||||
pm.current_fingerprint = "fp"
|
||||
pm.verify_password = lambda pw: True
|
||||
return pm
|
||||
|
||||
|
||||
def test_setup_encryption_manager_kdf_modes(monkeypatch):
|
||||
with TemporaryDirectory() as td:
|
||||
tmp = Path(td)
|
||||
argon_kwargs = dict(time_cost=1, memory_cost=8, parallelism=1)
|
||||
for mode in ("pbkdf2", "argon2"):
|
||||
path = tmp / mode
|
||||
path.mkdir()
|
||||
cfg = _setup_profile(path, mode)
|
||||
pm = _make_pm(path, cfg)
|
||||
monkeypatch.setattr(
|
||||
"password_manager.manager.prompt_existing_password",
|
||||
lambda *_: TEST_PASSWORD,
|
||||
)
|
||||
if mode == "argon2":
|
||||
monkeypatch.setattr(
|
||||
"password_manager.manager.derive_key_from_password_argon2",
|
||||
lambda pw: derive_key_from_password_argon2(pw, **argon_kwargs),
|
||||
)
|
||||
monkeypatch.setattr(PasswordManager, "initialize_bip85", lambda self: None)
|
||||
monkeypatch.setattr(
|
||||
PasswordManager, "initialize_managers", lambda self: None
|
||||
)
|
||||
assert pm.setup_encryption_manager(path, exit_on_fail=False)
|
||||
assert pm.parent_seed == TEST_SEED
|
@@ -2,6 +2,7 @@ import logging
|
||||
import pytest
|
||||
from utils.key_derivation import (
|
||||
derive_key_from_password,
|
||||
derive_key_from_password_argon2,
|
||||
derive_index_key_seed_only,
|
||||
derive_index_key,
|
||||
)
|
||||
@@ -33,3 +34,11 @@ def test_seed_only_key_deterministic():
|
||||
def test_derive_index_key_seed_only():
|
||||
seed = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
assert derive_index_key(seed) == derive_index_key_seed_only(seed)
|
||||
|
||||
|
||||
def test_argon2_key_deterministic():
|
||||
pw = "correct horse battery staple"
|
||||
k1 = derive_key_from_password_argon2(pw, time_cost=1, memory_cost=8, parallelism=1)
|
||||
k2 = derive_key_from_password_argon2(pw, time_cost=1, memory_cost=8, parallelism=1)
|
||||
assert k1 == k2
|
||||
assert len(k1) == 44
|
||||
|
@@ -97,6 +97,42 @@ def derive_key_from_password(password: str, iterations: int = 100_000) -> bytes:
|
||||
raise
|
||||
|
||||
|
||||
def derive_key_from_password_argon2(
|
||||
password: str,
|
||||
*,
|
||||
time_cost: int = 2,
|
||||
memory_cost: int = 64 * 1024,
|
||||
parallelism: int = 8,
|
||||
) -> bytes:
|
||||
"""Derive an encryption key from a password using Argon2id.
|
||||
|
||||
The defaults follow recommended parameters but omit a salt for deterministic
|
||||
output. Smaller values may be supplied for testing.
|
||||
"""
|
||||
|
||||
if not password:
|
||||
logger.error("Password cannot be empty.")
|
||||
raise ValueError("Password cannot be empty.")
|
||||
|
||||
normalized = unicodedata.normalize("NFKD", password).strip().encode("utf-8")
|
||||
try:
|
||||
from argon2.low_level import hash_secret_raw, Type
|
||||
|
||||
key = hash_secret_raw(
|
||||
secret=normalized,
|
||||
salt=b"\x00" * 16,
|
||||
time_cost=time_cost,
|
||||
memory_cost=memory_cost,
|
||||
parallelism=parallelism,
|
||||
hash_len=32,
|
||||
type=Type.ID,
|
||||
)
|
||||
return base64.urlsafe_b64encode(key)
|
||||
except Exception as e: # pragma: no cover - pass through errors
|
||||
logger.error(f"Error deriving key with Argon2id: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
def derive_key_from_parent_seed(parent_seed: str, fingerprint: str = None) -> bytes:
|
||||
"""
|
||||
Derives a 32-byte cryptographic key from a BIP-39 parent seed using HKDF.
|
||||
|
Reference in New Issue
Block a user