mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-07 14:58:56 +00:00
Include fingerprint salt in password key derivation
This commit is contained in:
@@ -79,7 +79,7 @@ def initialize_profile(
|
||||
profile_dir = APP_DIR / fingerprint
|
||||
profile_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
seed_key = derive_key_from_password(DEFAULT_PASSWORD)
|
||||
seed_key = derive_key_from_password(DEFAULT_PASSWORD, fingerprint)
|
||||
seed_mgr = EncryptionManager(seed_key, profile_dir)
|
||||
seed_file = profile_dir / "parent_seed.enc"
|
||||
clear_path = profile_dir / "seed_phrase.txt"
|
||||
|
@@ -510,10 +510,13 @@ class PasswordManager:
|
||||
else 50_000
|
||||
)
|
||||
print("Deriving key...")
|
||||
salt_fp = fingerprint_dir.name
|
||||
if mode == "argon2":
|
||||
seed_key = derive_key_from_password_argon2(password)
|
||||
seed_key = derive_key_from_password_argon2(password, salt_fp)
|
||||
else:
|
||||
seed_key = derive_key_from_password(password, iterations=iterations)
|
||||
seed_key = derive_key_from_password(
|
||||
password, salt_fp, iterations=iterations
|
||||
)
|
||||
seed_mgr = EncryptionManager(seed_key, fingerprint_dir)
|
||||
print("Decrypting seed...")
|
||||
try:
|
||||
@@ -578,10 +581,13 @@ class PasswordManager:
|
||||
if getattr(self, "config_manager", None)
|
||||
else 50_000
|
||||
)
|
||||
salt_fp = fingerprint_dir.name
|
||||
if mode == "argon2":
|
||||
seed_key = derive_key_from_password_argon2(password)
|
||||
seed_key = derive_key_from_password_argon2(password, salt_fp)
|
||||
else:
|
||||
seed_key = derive_key_from_password(password, iterations=iterations)
|
||||
seed_key = derive_key_from_password(
|
||||
password, salt_fp, iterations=iterations
|
||||
)
|
||||
seed_mgr = EncryptionManager(seed_key, fingerprint_dir)
|
||||
self.parent_seed = seed_mgr.decrypt_parent_seed()
|
||||
seed_bytes = Bip39SeedGenerator(self.parent_seed).Generate()
|
||||
@@ -746,14 +752,6 @@ class PasswordManager:
|
||||
if password is None:
|
||||
password = prompt_existing_password("Enter your login password: ")
|
||||
|
||||
# Derive encryption key from password
|
||||
iterations = (
|
||||
self.config_manager.get_kdf_iterations()
|
||||
if getattr(self, "config_manager", None)
|
||||
else 50_000
|
||||
)
|
||||
key = derive_key_from_password(password, iterations=iterations)
|
||||
|
||||
# Initialize FingerprintManager if not already initialized
|
||||
if not self.fingerprint_manager:
|
||||
self.initialize_fingerprint_manager()
|
||||
@@ -792,6 +790,16 @@ class PasswordManager:
|
||||
print(colored("Error: Seed profile directory not found.", "red"))
|
||||
sys.exit(1)
|
||||
|
||||
# Derive encryption key from password using selected fingerprint
|
||||
iterations = (
|
||||
self.config_manager.get_kdf_iterations()
|
||||
if getattr(self, "config_manager", None)
|
||||
else 50_000
|
||||
)
|
||||
key = derive_key_from_password(
|
||||
password, selected_fingerprint, iterations=iterations
|
||||
)
|
||||
|
||||
# Initialize EncryptionManager with key and fingerprint_dir
|
||||
self.encryption_manager = EncryptionManager(key, fingerprint_dir)
|
||||
self.vault = Vault(self.encryption_manager, fingerprint_dir)
|
||||
@@ -927,7 +935,9 @@ class PasswordManager:
|
||||
if getattr(self, "config_manager", None)
|
||||
else 50_000
|
||||
)
|
||||
seed_key = derive_key_from_password(password, iterations=iterations)
|
||||
seed_key = derive_key_from_password(
|
||||
password, fingerprint, iterations=iterations
|
||||
)
|
||||
|
||||
self.encryption_manager = EncryptionManager(index_key, fingerprint_dir)
|
||||
seed_mgr = EncryptionManager(seed_key, fingerprint_dir)
|
||||
@@ -1077,7 +1087,9 @@ class PasswordManager:
|
||||
if getattr(self, "config_manager", None)
|
||||
else 50_000
|
||||
)
|
||||
seed_key = derive_key_from_password(password, iterations=iterations)
|
||||
seed_key = derive_key_from_password(
|
||||
password, fingerprint_dir.name, iterations=iterations
|
||||
)
|
||||
|
||||
self.encryption_manager = EncryptionManager(index_key, fingerprint_dir)
|
||||
seed_mgr = EncryptionManager(seed_key, fingerprint_dir)
|
||||
@@ -4206,7 +4218,9 @@ class PasswordManager:
|
||||
if confirm_action("Encrypt export with a password? (Y/N): "):
|
||||
password = prompt_new_password()
|
||||
iterations = self.config_manager.get_kdf_iterations()
|
||||
key = derive_key_from_password(password, iterations=iterations)
|
||||
key = derive_key_from_password(
|
||||
password, self.current_fingerprint, iterations=iterations
|
||||
)
|
||||
enc_mgr = EncryptionManager(key, dest.parent)
|
||||
data_bytes = enc_mgr.encrypt_data(json_data.encode("utf-8"))
|
||||
dest = dest.with_suffix(dest.suffix + ".enc")
|
||||
@@ -4421,7 +4435,9 @@ class PasswordManager:
|
||||
new_key = derive_index_key(self.parent_seed)
|
||||
|
||||
iterations = self.config_manager.get_kdf_iterations()
|
||||
seed_key = derive_key_from_password(new_password, iterations=iterations)
|
||||
seed_key = derive_key_from_password(
|
||||
new_password, self.current_fingerprint, iterations=iterations
|
||||
)
|
||||
seed_mgr = EncryptionManager(seed_key, self.fingerprint_dir)
|
||||
|
||||
new_enc_mgr = EncryptionManager(new_key, self.fingerprint_dir)
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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})
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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(
|
||||
|
@@ -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
|
||||
|
@@ -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")
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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()
|
||||
|
||||
|
@@ -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"
|
||||
|
@@ -45,7 +45,9 @@ DEFAULT_ENCRYPTION_MODE = EncryptionMode.SEED_ONLY
|
||||
TOTP_PURPOSE = 39
|
||||
|
||||
|
||||
def derive_key_from_password(password: str, iterations: int = 100_000) -> bytes:
|
||||
def derive_key_from_password(
|
||||
password: str, fingerprint: Union[str, bytes], iterations: int = 100_000
|
||||
) -> bytes:
|
||||
"""
|
||||
Derives a Fernet-compatible encryption key from the provided password using PBKDF2-HMAC-SHA256.
|
||||
|
||||
@@ -55,7 +57,9 @@ def derive_key_from_password(password: str, iterations: int = 100_000) -> bytes:
|
||||
|
||||
Parameters:
|
||||
password (str): The user's password.
|
||||
iterations (int, optional): Number of iterations for the PBKDF2 algorithm. Defaults to 100,000.
|
||||
fingerprint (str | bytes): Seed fingerprint or precomputed salt.
|
||||
iterations (int, optional): Number of iterations for the PBKDF2 algorithm.
|
||||
Defaults to 100,000.
|
||||
|
||||
Returns:
|
||||
bytes: A URL-safe base64-encoded encryption key suitable for Fernet.
|
||||
@@ -74,13 +78,19 @@ def derive_key_from_password(password: str, iterations: int = 100_000) -> bytes:
|
||||
normalized_password = unicodedata.normalize("NFKD", password).strip()
|
||||
password_bytes = normalized_password.encode("utf-8")
|
||||
|
||||
# Derive a deterministic salt from the fingerprint
|
||||
if isinstance(fingerprint, bytes):
|
||||
salt = fingerprint
|
||||
else:
|
||||
salt = hashlib.sha256(fingerprint.encode()).digest()[:16]
|
||||
|
||||
try:
|
||||
# Derive the key using PBKDF2-HMAC-SHA256
|
||||
logger.debug("Starting key derivation from password.")
|
||||
key = hashlib.pbkdf2_hmac(
|
||||
hash_name="sha256",
|
||||
password=password_bytes,
|
||||
salt=b"", # No salt for deterministic key derivation
|
||||
salt=salt,
|
||||
iterations=iterations,
|
||||
dklen=32, # 256-bit key for Fernet
|
||||
)
|
||||
@@ -99,6 +109,7 @@ def derive_key_from_password(password: str, iterations: int = 100_000) -> bytes:
|
||||
|
||||
def derive_key_from_password_argon2(
|
||||
password: str,
|
||||
fingerprint: Union[str, bytes],
|
||||
*,
|
||||
time_cost: int = 2,
|
||||
memory_cost: int = 64 * 1024,
|
||||
@@ -118,9 +129,14 @@ def derive_key_from_password_argon2(
|
||||
try:
|
||||
from argon2.low_level import hash_secret_raw, Type
|
||||
|
||||
if isinstance(fingerprint, bytes):
|
||||
salt = fingerprint
|
||||
else:
|
||||
salt = hashlib.sha256(fingerprint.encode()).digest()[:16]
|
||||
|
||||
key = hash_secret_raw(
|
||||
secret=normalized,
|
||||
salt=b"\x00" * 16,
|
||||
salt=salt,
|
||||
time_cost=time_cost,
|
||||
memory_cost=memory_cost,
|
||||
parallelism=parallelism,
|
||||
|
Reference in New Issue
Block a user