mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +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 = APP_DIR / fingerprint
|
||||||
profile_dir.mkdir(parents=True, exist_ok=True)
|
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_mgr = EncryptionManager(seed_key, profile_dir)
|
||||||
seed_file = profile_dir / "parent_seed.enc"
|
seed_file = profile_dir / "parent_seed.enc"
|
||||||
clear_path = profile_dir / "seed_phrase.txt"
|
clear_path = profile_dir / "seed_phrase.txt"
|
||||||
|
@@ -510,10 +510,13 @@ class PasswordManager:
|
|||||||
else 50_000
|
else 50_000
|
||||||
)
|
)
|
||||||
print("Deriving key...")
|
print("Deriving key...")
|
||||||
|
salt_fp = fingerprint_dir.name
|
||||||
if mode == "argon2":
|
if mode == "argon2":
|
||||||
seed_key = derive_key_from_password_argon2(password)
|
seed_key = derive_key_from_password_argon2(password, salt_fp)
|
||||||
else:
|
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)
|
seed_mgr = EncryptionManager(seed_key, fingerprint_dir)
|
||||||
print("Decrypting seed...")
|
print("Decrypting seed...")
|
||||||
try:
|
try:
|
||||||
@@ -578,10 +581,13 @@ class PasswordManager:
|
|||||||
if getattr(self, "config_manager", None)
|
if getattr(self, "config_manager", None)
|
||||||
else 50_000
|
else 50_000
|
||||||
)
|
)
|
||||||
|
salt_fp = fingerprint_dir.name
|
||||||
if mode == "argon2":
|
if mode == "argon2":
|
||||||
seed_key = derive_key_from_password_argon2(password)
|
seed_key = derive_key_from_password_argon2(password, salt_fp)
|
||||||
else:
|
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)
|
seed_mgr = EncryptionManager(seed_key, fingerprint_dir)
|
||||||
self.parent_seed = seed_mgr.decrypt_parent_seed()
|
self.parent_seed = seed_mgr.decrypt_parent_seed()
|
||||||
seed_bytes = Bip39SeedGenerator(self.parent_seed).Generate()
|
seed_bytes = Bip39SeedGenerator(self.parent_seed).Generate()
|
||||||
@@ -746,14 +752,6 @@ class PasswordManager:
|
|||||||
if password is None:
|
if password is None:
|
||||||
password = prompt_existing_password("Enter your login password: ")
|
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
|
# Initialize FingerprintManager if not already initialized
|
||||||
if not self.fingerprint_manager:
|
if not self.fingerprint_manager:
|
||||||
self.initialize_fingerprint_manager()
|
self.initialize_fingerprint_manager()
|
||||||
@@ -792,6 +790,16 @@ class PasswordManager:
|
|||||||
print(colored("Error: Seed profile directory not found.", "red"))
|
print(colored("Error: Seed profile directory not found.", "red"))
|
||||||
sys.exit(1)
|
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
|
# Initialize EncryptionManager with key and fingerprint_dir
|
||||||
self.encryption_manager = EncryptionManager(key, fingerprint_dir)
|
self.encryption_manager = EncryptionManager(key, fingerprint_dir)
|
||||||
self.vault = Vault(self.encryption_manager, fingerprint_dir)
|
self.vault = Vault(self.encryption_manager, fingerprint_dir)
|
||||||
@@ -927,7 +935,9 @@ class PasswordManager:
|
|||||||
if getattr(self, "config_manager", None)
|
if getattr(self, "config_manager", None)
|
||||||
else 50_000
|
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)
|
self.encryption_manager = EncryptionManager(index_key, fingerprint_dir)
|
||||||
seed_mgr = EncryptionManager(seed_key, fingerprint_dir)
|
seed_mgr = EncryptionManager(seed_key, fingerprint_dir)
|
||||||
@@ -1077,7 +1087,9 @@ class PasswordManager:
|
|||||||
if getattr(self, "config_manager", None)
|
if getattr(self, "config_manager", None)
|
||||||
else 50_000
|
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)
|
self.encryption_manager = EncryptionManager(index_key, fingerprint_dir)
|
||||||
seed_mgr = EncryptionManager(seed_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): "):
|
if confirm_action("Encrypt export with a password? (Y/N): "):
|
||||||
password = prompt_new_password()
|
password = prompt_new_password()
|
||||||
iterations = self.config_manager.get_kdf_iterations()
|
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)
|
enc_mgr = EncryptionManager(key, dest.parent)
|
||||||
data_bytes = enc_mgr.encrypt_data(json_data.encode("utf-8"))
|
data_bytes = enc_mgr.encrypt_data(json_data.encode("utf-8"))
|
||||||
dest = dest.with_suffix(dest.suffix + ".enc")
|
dest = dest.with_suffix(dest.suffix + ".enc")
|
||||||
@@ -4421,7 +4435,9 @@ class PasswordManager:
|
|||||||
new_key = derive_index_key(self.parent_seed)
|
new_key = derive_index_key(self.parent_seed)
|
||||||
|
|
||||||
iterations = self.config_manager.get_kdf_iterations()
|
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)
|
seed_mgr = EncryptionManager(seed_key, self.fingerprint_dir)
|
||||||
|
|
||||||
new_enc_mgr = EncryptionManager(new_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_index_key,
|
||||||
derive_key_from_password,
|
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_SEED = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
TEST_PASSWORD = "pw"
|
TEST_PASSWORD = "pw"
|
||||||
@@ -22,7 +23,8 @@ def create_vault(
|
|||||||
password: str = TEST_PASSWORD,
|
password: str = TEST_PASSWORD,
|
||||||
) -> tuple[Vault, EncryptionManager]:
|
) -> tuple[Vault, EncryptionManager]:
|
||||||
"""Create a Vault initialized for tests."""
|
"""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 = EncryptionManager(seed_key, dir_path)
|
||||||
seed_mgr.encrypt_parent_seed(seed)
|
seed_mgr.encrypt_parent_seed(seed)
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ from pathlib import Path
|
|||||||
from multiprocessing import Process, Queue
|
from multiprocessing import Process, Queue
|
||||||
import pytest
|
import pytest
|
||||||
from helpers import TEST_SEED, TEST_PASSWORD
|
from helpers import TEST_SEED, TEST_PASSWORD
|
||||||
|
from utils.fingerprint import generate_fingerprint
|
||||||
|
|
||||||
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
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))
|
@pytest.mark.parametrize("_", range(3))
|
||||||
def test_concurrency_stress(tmp_path: Path, loops: int, _):
|
def test_concurrency_stress(tmp_path: Path, loops: int, _):
|
||||||
index_key = derive_index_key(TEST_SEED)
|
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)
|
EncryptionManager(seed_key, tmp_path).encrypt_parent_seed(TEST_SEED)
|
||||||
enc = EncryptionManager(index_key, tmp_path)
|
enc = EncryptionManager(index_key, tmp_path)
|
||||||
Vault(enc, tmp_path).save_index({"counter": 0})
|
Vault(enc, tmp_path).save_index({"counter": 0})
|
||||||
|
@@ -9,6 +9,7 @@ from utils.key_derivation import (
|
|||||||
derive_key_from_password_argon2,
|
derive_key_from_password_argon2,
|
||||||
derive_index_key,
|
derive_index_key,
|
||||||
)
|
)
|
||||||
|
from utils.fingerprint import generate_fingerprint
|
||||||
from seedpass.core.encryption import EncryptionManager
|
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):
|
def test_fuzz_key_round_trip(password, seed_bytes, config, mode, tmp_path: Path):
|
||||||
"""Ensure EncryptionManager round-trips arbitrary data."""
|
"""Ensure EncryptionManager round-trips arbitrary data."""
|
||||||
seed_phrase = Mnemonic("english").to_mnemonic(seed_bytes)
|
seed_phrase = Mnemonic("english").to_mnemonic(seed_bytes)
|
||||||
|
fp = generate_fingerprint(seed_phrase)
|
||||||
if mode == "argon2":
|
if mode == "argon2":
|
||||||
key = derive_key_from_password_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:
|
else:
|
||||||
key = derive_key_from_password(password, iterations=1)
|
key = derive_key_from_password(password, fp, iterations=1)
|
||||||
|
|
||||||
enc_mgr = EncryptionManager(key, tmp_path)
|
enc_mgr = EncryptionManager(key, tmp_path)
|
||||||
|
|
||||||
|
@@ -4,6 +4,7 @@ from tempfile import TemporaryDirectory
|
|||||||
import pytest
|
import pytest
|
||||||
import sys
|
import sys
|
||||||
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
|
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
|
||||||
|
from utils.fingerprint import generate_fingerprint
|
||||||
|
|
||||||
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
@@ -16,7 +17,8 @@ PASSWORD = "passw0rd"
|
|||||||
|
|
||||||
|
|
||||||
def setup_vault(tmp: Path) -> Vault:
|
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 = EncryptionManager(seed_key, tmp)
|
||||||
seed_mgr.encrypt_parent_seed(SEED)
|
seed_mgr.encrypt_parent_seed(SEED)
|
||||||
|
|
||||||
|
@@ -19,10 +19,11 @@ TEST_PASSWORD = "pw"
|
|||||||
|
|
||||||
def _setup_profile(tmp: Path, mode: str):
|
def _setup_profile(tmp: Path, mode: str):
|
||||||
argon_kwargs = dict(time_cost=1, memory_cost=8, parallelism=1)
|
argon_kwargs = dict(time_cost=1, memory_cost=8, parallelism=1)
|
||||||
|
fp = tmp.name
|
||||||
if mode == "argon2":
|
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:
|
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)
|
EncryptionManager(seed_key, tmp).encrypt_parent_seed(TEST_SEED)
|
||||||
|
|
||||||
index_key = derive_index_key(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.encryption_mode = EncryptionMode.SEED_ONLY
|
||||||
pm.config_manager = cfg
|
pm.config_manager = cfg
|
||||||
pm.fingerprint_dir = tmp
|
pm.fingerprint_dir = tmp
|
||||||
pm.current_fingerprint = "fp"
|
pm.current_fingerprint = tmp.name
|
||||||
pm.verify_password = lambda pw: True
|
pm.verify_password = lambda pw: True
|
||||||
return pm
|
return pm
|
||||||
|
|
||||||
@@ -65,7 +66,9 @@ def test_setup_encryption_manager_kdf_modes(monkeypatch):
|
|||||||
if mode == "argon2":
|
if mode == "argon2":
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"seedpass.core.manager.derive_key_from_password_argon2",
|
"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(PasswordManager, "initialize_bip85", lambda self: None)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
|
@@ -10,8 +10,9 @@ from utils.key_derivation import (
|
|||||||
|
|
||||||
def test_derive_key_deterministic():
|
def test_derive_key_deterministic():
|
||||||
password = "correct horse battery staple"
|
password = "correct horse battery staple"
|
||||||
key1 = derive_key_from_password(password, iterations=1)
|
fp = "fp"
|
||||||
key2 = derive_key_from_password(password, iterations=1)
|
key1 = derive_key_from_password(password, fp, iterations=1)
|
||||||
|
key2 = derive_key_from_password(password, fp, iterations=1)
|
||||||
assert key1 == key2
|
assert key1 == key2
|
||||||
assert len(key1) == 44
|
assert len(key1) == 44
|
||||||
logging.info("Deterministic key derivation succeeded")
|
logging.info("Deterministic key derivation succeeded")
|
||||||
@@ -19,7 +20,7 @@ def test_derive_key_deterministic():
|
|||||||
|
|
||||||
def test_derive_key_empty_password_error():
|
def test_derive_key_empty_password_error():
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
derive_key_from_password("")
|
derive_key_from_password("", "fp")
|
||||||
logging.info("Empty password correctly raised ValueError")
|
logging.info("Empty password correctly raised ValueError")
|
||||||
|
|
||||||
|
|
||||||
@@ -38,7 +39,12 @@ def test_derive_index_key_seed_only():
|
|||||||
|
|
||||||
def test_argon2_key_deterministic():
|
def test_argon2_key_deterministic():
|
||||||
pw = "correct horse battery staple"
|
pw = "correct horse battery staple"
|
||||||
k1 = derive_key_from_password_argon2(pw, time_cost=1, memory_cost=8, parallelism=1)
|
fp = "fp"
|
||||||
k2 = derive_key_from_password_argon2(pw, time_cost=1, memory_cost=8, parallelism=1)
|
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 k1 == k2
|
||||||
assert len(k1) == 44
|
assert len(k1) == 44
|
||||||
|
@@ -14,19 +14,22 @@ from seedpass.core.backup import BackupManager
|
|||||||
from seedpass.core.config_manager import ConfigManager
|
from seedpass.core.config_manager import ConfigManager
|
||||||
from seedpass.core.manager import PasswordManager, EncryptionMode
|
from seedpass.core.manager import PasswordManager, EncryptionMode
|
||||||
from utils.key_derivation import derive_index_key, derive_key_from_password
|
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"
|
SEED = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
|
|
||||||
|
|
||||||
def test_password_change_and_unlock(monkeypatch):
|
def test_password_change_and_unlock(monkeypatch):
|
||||||
with TemporaryDirectory() as tmpdir:
|
with TemporaryDirectory() as tmpdir:
|
||||||
fp = Path(tmpdir)
|
fp_name = generate_fingerprint(SEED)
|
||||||
|
fp = Path(tmpdir) / fp_name
|
||||||
|
fp.mkdir()
|
||||||
old_pw = "oldpw"
|
old_pw = "oldpw"
|
||||||
new_pw = "newpw"
|
new_pw = "newpw"
|
||||||
|
|
||||||
# initial encryption setup
|
# initial encryption setup
|
||||||
index_key = derive_index_key(SEED)
|
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)
|
enc_mgr = EncryptionManager(index_key, fp)
|
||||||
seed_mgr = EncryptionManager(seed_key, fp)
|
seed_mgr = EncryptionManager(seed_key, fp)
|
||||||
vault = Vault(enc_mgr, fp)
|
vault = Vault(enc_mgr, fp)
|
||||||
@@ -54,7 +57,7 @@ def test_password_change_and_unlock(monkeypatch):
|
|||||||
pm.vault = vault
|
pm.vault = vault
|
||||||
pm.password_generator = SimpleNamespace(encryption_manager=enc_mgr)
|
pm.password_generator = SimpleNamespace(encryption_manager=enc_mgr)
|
||||||
pm.fingerprint_dir = fp
|
pm.fingerprint_dir = fp
|
||||||
pm.current_fingerprint = "fp"
|
pm.current_fingerprint = fp_name
|
||||||
pm.parent_seed = SEED
|
pm.parent_seed = SEED
|
||||||
pm.nostr_client = SimpleNamespace(
|
pm.nostr_client = SimpleNamespace(
|
||||||
publish_snapshot=lambda *a, **k: (None, "abcd")
|
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.config_manager import ConfigManager
|
||||||
from seedpass.core.portable_backup import export_backup, import_backup
|
from seedpass.core.portable_backup import export_backup, import_backup
|
||||||
from utils.key_derivation import derive_index_key, derive_key_from_password
|
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"
|
SEED = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
@@ -22,7 +23,8 @@ PASSWORD = "passw0rd"
|
|||||||
|
|
||||||
|
|
||||||
def setup_vault(tmp: Path):
|
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 = EncryptionManager(seed_key, tmp)
|
||||||
seed_mgr.encrypt_parent_seed(SEED)
|
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:
|
with TemporaryDirectory() as td, TemporaryDirectory() as extra:
|
||||||
tmp = Path(td)
|
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 = EncryptionManager(seed_key, tmp)
|
||||||
seed_mgr.encrypt_parent_seed(SEED)
|
seed_mgr.encrypt_parent_seed(SEED)
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ from pathlib import Path
|
|||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from helpers import TEST_PASSWORD
|
from helpers import TEST_PASSWORD
|
||||||
from utils.key_derivation import derive_key_from_password
|
from utils.key_derivation import derive_key_from_password
|
||||||
|
from utils.fingerprint import generate_fingerprint
|
||||||
from mnemonic import Mnemonic
|
from mnemonic import Mnemonic
|
||||||
|
|
||||||
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
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():
|
def test_seed_encryption_round_trip():
|
||||||
with TemporaryDirectory() as tmpdir:
|
with TemporaryDirectory() as tmpdir:
|
||||||
key = derive_key_from_password(TEST_PASSWORD)
|
|
||||||
enc_mgr = EncryptionManager(key, Path(tmpdir))
|
|
||||||
|
|
||||||
seed = Mnemonic("english").generate(strength=128)
|
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)
|
enc_mgr.encrypt_parent_seed(seed)
|
||||||
decrypted = enc_mgr.decrypt_parent_seed()
|
decrypted = enc_mgr.decrypt_parent_seed()
|
||||||
|
|
||||||
|
@@ -4,6 +4,7 @@ from cryptography.fernet import Fernet
|
|||||||
|
|
||||||
from helpers import TEST_PASSWORD, TEST_SEED
|
from helpers import TEST_PASSWORD, TEST_SEED
|
||||||
from utils.key_derivation import derive_key_from_password
|
from utils.key_derivation import derive_key_from_password
|
||||||
|
from utils.fingerprint import generate_fingerprint
|
||||||
|
|
||||||
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
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:
|
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)
|
fernet = Fernet(key)
|
||||||
encrypted = fernet.encrypt(TEST_SEED.encode())
|
encrypted = fernet.encrypt(TEST_SEED.encode())
|
||||||
legacy_file = tmp_path / "parent_seed.enc"
|
legacy_file = tmp_path / "parent_seed.enc"
|
||||||
|
@@ -45,7 +45,9 @@ DEFAULT_ENCRYPTION_MODE = EncryptionMode.SEED_ONLY
|
|||||||
TOTP_PURPOSE = 39
|
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.
|
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:
|
Parameters:
|
||||||
password (str): The user's password.
|
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:
|
Returns:
|
||||||
bytes: A URL-safe base64-encoded encryption key suitable for Fernet.
|
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()
|
normalized_password = unicodedata.normalize("NFKD", password).strip()
|
||||||
password_bytes = normalized_password.encode("utf-8")
|
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:
|
try:
|
||||||
# Derive the key using PBKDF2-HMAC-SHA256
|
# Derive the key using PBKDF2-HMAC-SHA256
|
||||||
logger.debug("Starting key derivation from password.")
|
logger.debug("Starting key derivation from password.")
|
||||||
key = hashlib.pbkdf2_hmac(
|
key = hashlib.pbkdf2_hmac(
|
||||||
hash_name="sha256",
|
hash_name="sha256",
|
||||||
password=password_bytes,
|
password=password_bytes,
|
||||||
salt=b"", # No salt for deterministic key derivation
|
salt=salt,
|
||||||
iterations=iterations,
|
iterations=iterations,
|
||||||
dklen=32, # 256-bit key for Fernet
|
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(
|
def derive_key_from_password_argon2(
|
||||||
password: str,
|
password: str,
|
||||||
|
fingerprint: Union[str, bytes],
|
||||||
*,
|
*,
|
||||||
time_cost: int = 2,
|
time_cost: int = 2,
|
||||||
memory_cost: int = 64 * 1024,
|
memory_cost: int = 64 * 1024,
|
||||||
@@ -118,9 +129,14 @@ def derive_key_from_password_argon2(
|
|||||||
try:
|
try:
|
||||||
from argon2.low_level import hash_secret_raw, Type
|
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(
|
key = hash_secret_raw(
|
||||||
secret=normalized,
|
secret=normalized,
|
||||||
salt=b"\x00" * 16,
|
salt=salt,
|
||||||
time_cost=time_cost,
|
time_cost=time_cost,
|
||||||
memory_cost=memory_cost,
|
memory_cost=memory_cost,
|
||||||
parallelism=parallelism,
|
parallelism=parallelism,
|
||||||
|
Reference in New Issue
Block a user