Include fingerprint salt in password key derivation

This commit is contained in:
thePR0M3TH3AN
2025-08-03 09:37:59 -04:00
parent 2794b67d83
commit 5423c41b06
13 changed files with 102 additions and 44 deletions

View File

@@ -11,6 +11,7 @@ from utils.key_derivation import (
derive_index_key,
derive_key_from_password,
)
from utils.fingerprint import generate_fingerprint
TEST_SEED = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
TEST_PASSWORD = "pw"
@@ -22,7 +23,8 @@ def create_vault(
password: str = TEST_PASSWORD,
) -> tuple[Vault, EncryptionManager]:
"""Create a Vault initialized for tests."""
seed_key = derive_key_from_password(password)
fp = generate_fingerprint(seed)
seed_key = derive_key_from_password(password, fp)
seed_mgr = EncryptionManager(seed_key, dir_path)
seed_mgr.encrypt_parent_seed(seed)

View File

@@ -3,6 +3,7 @@ from pathlib import Path
from multiprocessing import Process, Queue
import pytest
from helpers import TEST_SEED, TEST_PASSWORD
from utils.fingerprint import generate_fingerprint
sys.path.append(str(Path(__file__).resolve().parents[1]))
@@ -51,7 +52,8 @@ def _backup(index_key: bytes, dir_path: Path, loops: int, out: Queue) -> None:
@pytest.mark.parametrize("_", range(3))
def test_concurrency_stress(tmp_path: Path, loops: int, _):
index_key = derive_index_key(TEST_SEED)
seed_key = derive_key_from_password(TEST_PASSWORD)
fp = generate_fingerprint(TEST_SEED)
seed_key = derive_key_from_password(TEST_PASSWORD, fp)
EncryptionManager(seed_key, tmp_path).encrypt_parent_seed(TEST_SEED)
enc = EncryptionManager(index_key, tmp_path)
Vault(enc, tmp_path).save_index({"counter": 0})

View File

@@ -9,6 +9,7 @@ from utils.key_derivation import (
derive_key_from_password_argon2,
derive_index_key,
)
from utils.fingerprint import generate_fingerprint
from seedpass.core.encryption import EncryptionManager
@@ -33,12 +34,13 @@ cfg_values = st.one_of(
def test_fuzz_key_round_trip(password, seed_bytes, config, mode, tmp_path: Path):
"""Ensure EncryptionManager round-trips arbitrary data."""
seed_phrase = Mnemonic("english").to_mnemonic(seed_bytes)
fp = generate_fingerprint(seed_phrase)
if mode == "argon2":
key = derive_key_from_password_argon2(
password, time_cost=1, memory_cost=8, parallelism=1
password, fp, time_cost=1, memory_cost=8, parallelism=1
)
else:
key = derive_key_from_password(password, iterations=1)
key = derive_key_from_password(password, fp, iterations=1)
enc_mgr = EncryptionManager(key, tmp_path)

View File

@@ -4,6 +4,7 @@ from tempfile import TemporaryDirectory
import pytest
import sys
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
from utils.fingerprint import generate_fingerprint
sys.path.append(str(Path(__file__).resolve().parents[1]))
@@ -16,7 +17,8 @@ PASSWORD = "passw0rd"
def setup_vault(tmp: Path) -> Vault:
seed_key = derive_key_from_password(PASSWORD)
fp = generate_fingerprint(SEED)
seed_key = derive_key_from_password(PASSWORD, fp)
seed_mgr = EncryptionManager(seed_key, tmp)
seed_mgr.encrypt_parent_seed(SEED)

View File

@@ -19,10 +19,11 @@ TEST_PASSWORD = "pw"
def _setup_profile(tmp: Path, mode: str):
argon_kwargs = dict(time_cost=1, memory_cost=8, parallelism=1)
fp = tmp.name
if mode == "argon2":
seed_key = derive_key_from_password_argon2(TEST_PASSWORD, **argon_kwargs)
seed_key = derive_key_from_password_argon2(TEST_PASSWORD, fp, **argon_kwargs)
else:
seed_key = derive_key_from_password(TEST_PASSWORD, iterations=1)
seed_key = derive_key_from_password(TEST_PASSWORD, fp, iterations=1)
EncryptionManager(seed_key, tmp).encrypt_parent_seed(TEST_SEED)
index_key = derive_index_key(TEST_SEED)
@@ -44,7 +45,7 @@ def _make_pm(tmp: Path, cfg: ConfigManager):
pm.encryption_mode = EncryptionMode.SEED_ONLY
pm.config_manager = cfg
pm.fingerprint_dir = tmp
pm.current_fingerprint = "fp"
pm.current_fingerprint = tmp.name
pm.verify_password = lambda pw: True
return pm
@@ -65,7 +66,9 @@ def test_setup_encryption_manager_kdf_modes(monkeypatch):
if mode == "argon2":
monkeypatch.setattr(
"seedpass.core.manager.derive_key_from_password_argon2",
lambda pw: derive_key_from_password_argon2(pw, **argon_kwargs),
lambda pw, fp: derive_key_from_password_argon2(
pw, fp, **argon_kwargs
),
)
monkeypatch.setattr(PasswordManager, "initialize_bip85", lambda self: None)
monkeypatch.setattr(

View File

@@ -10,8 +10,9 @@ from utils.key_derivation import (
def test_derive_key_deterministic():
password = "correct horse battery staple"
key1 = derive_key_from_password(password, iterations=1)
key2 = derive_key_from_password(password, iterations=1)
fp = "fp"
key1 = derive_key_from_password(password, fp, iterations=1)
key2 = derive_key_from_password(password, fp, iterations=1)
assert key1 == key2
assert len(key1) == 44
logging.info("Deterministic key derivation succeeded")
@@ -19,7 +20,7 @@ def test_derive_key_deterministic():
def test_derive_key_empty_password_error():
with pytest.raises(ValueError):
derive_key_from_password("")
derive_key_from_password("", "fp")
logging.info("Empty password correctly raised ValueError")
@@ -38,7 +39,12 @@ def test_derive_index_key_seed_only():
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)
fp = "fp"
k1 = derive_key_from_password_argon2(
pw, fp, time_cost=1, memory_cost=8, parallelism=1
)
k2 = derive_key_from_password_argon2(
pw, fp, time_cost=1, memory_cost=8, parallelism=1
)
assert k1 == k2
assert len(k1) == 44

View File

@@ -14,19 +14,22 @@ from seedpass.core.backup import BackupManager
from seedpass.core.config_manager import ConfigManager
from seedpass.core.manager import PasswordManager, EncryptionMode
from utils.key_derivation import derive_index_key, derive_key_from_password
from utils.fingerprint import generate_fingerprint
SEED = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
def test_password_change_and_unlock(monkeypatch):
with TemporaryDirectory() as tmpdir:
fp = Path(tmpdir)
fp_name = generate_fingerprint(SEED)
fp = Path(tmpdir) / fp_name
fp.mkdir()
old_pw = "oldpw"
new_pw = "newpw"
# initial encryption setup
index_key = derive_index_key(SEED)
seed_key = derive_key_from_password(old_pw)
seed_key = derive_key_from_password(old_pw, fp_name)
enc_mgr = EncryptionManager(index_key, fp)
seed_mgr = EncryptionManager(seed_key, fp)
vault = Vault(enc_mgr, fp)
@@ -54,7 +57,7 @@ def test_password_change_and_unlock(monkeypatch):
pm.vault = vault
pm.password_generator = SimpleNamespace(encryption_manager=enc_mgr)
pm.fingerprint_dir = fp
pm.current_fingerprint = "fp"
pm.current_fingerprint = fp_name
pm.parent_seed = SEED
pm.nostr_client = SimpleNamespace(
publish_snapshot=lambda *a, **k: (None, "abcd")

View File

@@ -15,6 +15,7 @@ from seedpass.core.backup import BackupManager
from seedpass.core.config_manager import ConfigManager
from seedpass.core.portable_backup import export_backup, import_backup
from utils.key_derivation import derive_index_key, derive_key_from_password
from utils.fingerprint import generate_fingerprint
SEED = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
@@ -22,7 +23,8 @@ PASSWORD = "passw0rd"
def setup_vault(tmp: Path):
seed_key = derive_key_from_password(PASSWORD)
fp = generate_fingerprint(SEED)
seed_key = derive_key_from_password(PASSWORD, fp)
seed_mgr = EncryptionManager(seed_key, tmp)
seed_mgr.encrypt_parent_seed(SEED)
@@ -126,7 +128,8 @@ def test_export_creates_additional_backup_and_import(monkeypatch):
with TemporaryDirectory() as td, TemporaryDirectory() as extra:
tmp = Path(td)
seed_key = derive_key_from_password(PASSWORD)
fp = generate_fingerprint(SEED)
seed_key = derive_key_from_password(PASSWORD, fp)
seed_mgr = EncryptionManager(seed_key, tmp)
seed_mgr.encrypt_parent_seed(SEED)

View File

@@ -3,6 +3,7 @@ from pathlib import Path
from tempfile import TemporaryDirectory
from helpers import TEST_PASSWORD
from utils.key_derivation import derive_key_from_password
from utils.fingerprint import generate_fingerprint
from mnemonic import Mnemonic
sys.path.append(str(Path(__file__).resolve().parents[1]))
@@ -13,10 +14,10 @@ from seedpass.core.manager import PasswordManager, EncryptionMode
def test_seed_encryption_round_trip():
with TemporaryDirectory() as tmpdir:
key = derive_key_from_password(TEST_PASSWORD)
enc_mgr = EncryptionManager(key, Path(tmpdir))
seed = Mnemonic("english").generate(strength=128)
fp = generate_fingerprint(seed)
key = derive_key_from_password(TEST_PASSWORD, fp)
enc_mgr = EncryptionManager(key, Path(tmpdir))
enc_mgr.encrypt_parent_seed(seed)
decrypted = enc_mgr.decrypt_parent_seed()

View File

@@ -4,6 +4,7 @@ from cryptography.fernet import Fernet
from helpers import TEST_PASSWORD, TEST_SEED
from utils.key_derivation import derive_key_from_password
from utils.fingerprint import generate_fingerprint
sys.path.append(str(Path(__file__).resolve().parents[1]))
@@ -11,7 +12,8 @@ from seedpass.core.encryption import EncryptionManager
def test_parent_seed_migrates_from_fernet(tmp_path: Path) -> None:
key = derive_key_from_password(TEST_PASSWORD)
fp = generate_fingerprint(TEST_SEED)
key = derive_key_from_password(TEST_PASSWORD, fp)
fernet = Fernet(key)
encrypted = fernet.encrypt(TEST_SEED.encode())
legacy_file = tmp_path / "parent_seed.enc"