mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-09 15:58:48 +00:00
Merge pull request #741 from PR0M3TH3AN/codex/update-decryption-method-for-legacy-support
feat: support legacy index decryption fallback
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
|
import unicodedata
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import orjson as json_lib # type: ignore
|
import orjson as json_lib # type: ignore
|
||||||
@@ -25,11 +26,19 @@ from cryptography.fernet import Fernet, InvalidToken
|
|||||||
from termcolor import colored
|
from termcolor import colored
|
||||||
from utils.file_lock import exclusive_lock
|
from utils.file_lock import exclusive_lock
|
||||||
from mnemonic import Mnemonic
|
from mnemonic import Mnemonic
|
||||||
|
from utils.password_prompt import prompt_existing_password
|
||||||
|
|
||||||
# Instantiate the logger
|
# Instantiate the logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _derive_legacy_key_from_password(password: str, iterations: int = 100_000) -> bytes:
|
||||||
|
"""Derive legacy Fernet key using password only (no fingerprint)."""
|
||||||
|
normalized = unicodedata.normalize("NFKD", password).strip().encode("utf-8")
|
||||||
|
key = hashlib.pbkdf2_hmac("sha256", normalized, b"", iterations, dklen=32)
|
||||||
|
return base64.urlsafe_b64encode(key)
|
||||||
|
|
||||||
|
|
||||||
class EncryptionManager:
|
class EncryptionManager:
|
||||||
"""
|
"""
|
||||||
Manages encryption and decryption, handling migration from legacy Fernet
|
Manages encryption and decryption, handling migration from legacy Fernet
|
||||||
@@ -268,12 +277,12 @@ class EncryptionManager:
|
|||||||
"""
|
"""
|
||||||
if relative_path is None:
|
if relative_path is None:
|
||||||
relative_path = Path("seedpass_entries_db.json.enc")
|
relative_path = Path("seedpass_entries_db.json.enc")
|
||||||
try:
|
|
||||||
decrypted_data = self.decrypt_data(encrypted_data)
|
def _process(decrypted: bytes) -> dict:
|
||||||
if USE_ORJSON:
|
if USE_ORJSON:
|
||||||
data = json_lib.loads(decrypted_data)
|
data = json_lib.loads(decrypted)
|
||||||
else:
|
else:
|
||||||
data = json_lib.loads(decrypted_data.decode("utf-8"))
|
data = json_lib.loads(decrypted.decode("utf-8"))
|
||||||
existing_file = self.resolve_relative_path(relative_path)
|
existing_file = self.resolve_relative_path(relative_path)
|
||||||
if merge and existing_file.exists():
|
if merge and existing_file.exists():
|
||||||
current = self.load_json_data(relative_path)
|
current = self.load_json_data(relative_path)
|
||||||
@@ -289,11 +298,52 @@ class EncryptionManager:
|
|||||||
current.get("schema_version", 0), data.get("schema_version", 0)
|
current.get("schema_version", 0), data.get("schema_version", 0)
|
||||||
)
|
)
|
||||||
data = current
|
data = current
|
||||||
|
return data
|
||||||
|
|
||||||
|
try:
|
||||||
|
decrypted_data = self.decrypt_data(encrypted_data)
|
||||||
|
data = _process(decrypted_data)
|
||||||
self.save_json_data(data, relative_path) # This always saves in V2 format
|
self.save_json_data(data, relative_path) # This always saves in V2 format
|
||||||
self.update_checksum(relative_path)
|
self.update_checksum(relative_path)
|
||||||
logger.info("Index file from Nostr was processed and saved successfully.")
|
logger.info("Index file from Nostr was processed and saved successfully.")
|
||||||
print(colored("Index file updated from Nostr successfully.", "green"))
|
print(colored("Index file updated from Nostr successfully.", "green"))
|
||||||
return True
|
return True
|
||||||
|
except InvalidToken as e:
|
||||||
|
try:
|
||||||
|
password = prompt_existing_password(
|
||||||
|
"Enter your master password for legacy decryption: "
|
||||||
|
)
|
||||||
|
legacy_key = _derive_legacy_key_from_password(password)
|
||||||
|
legacy_mgr = EncryptionManager(legacy_key, self.fingerprint_dir)
|
||||||
|
decrypted_data = legacy_mgr.decrypt_data(encrypted_data)
|
||||||
|
data = _process(decrypted_data)
|
||||||
|
self.save_json_data(data, relative_path)
|
||||||
|
self.update_checksum(relative_path)
|
||||||
|
logger.warning(
|
||||||
|
"Index decrypted using legacy password-only key derivation."
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
colored(
|
||||||
|
"Warning: index decrypted with legacy key; it will be re-encrypted.",
|
||||||
|
"yellow",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception as e2:
|
||||||
|
if strict:
|
||||||
|
logger.error(
|
||||||
|
f"Failed legacy decryption attempt: {e2}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
colored(
|
||||||
|
f"Error: Failed to decrypt and save data from Nostr: {e2}",
|
||||||
|
"red",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
logger.warning(f"Failed to decrypt index from Nostr: {e2}")
|
||||||
|
return False
|
||||||
except Exception as e: # pragma: no cover - error handling
|
except Exception as e: # pragma: no cover - error handling
|
||||||
if strict:
|
if strict:
|
||||||
logger.error(
|
logger.error(
|
||||||
|
35
src/tests/test_nostr_legacy_decrypt_fallback.py
Normal file
35
src/tests/test_nostr_legacy_decrypt_fallback.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import json
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import unicodedata
|
||||||
|
import logging
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
|
||||||
|
from helpers import create_vault, TEST_PASSWORD
|
||||||
|
import seedpass.core.encryption as enc_module
|
||||||
|
|
||||||
|
|
||||||
|
def _fast_legacy_key(password: str, iterations: int = 100_000) -> bytes:
|
||||||
|
normalized = unicodedata.normalize("NFKD", password).strip().encode("utf-8")
|
||||||
|
key = hashlib.pbkdf2_hmac("sha256", normalized, b"", 1, dklen=32)
|
||||||
|
return base64.urlsafe_b64encode(key)
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_password_only_fallback(monkeypatch, tmp_path, caplog):
|
||||||
|
# Speed up legacy key derivation
|
||||||
|
monkeypatch.setattr(
|
||||||
|
enc_module, "_derive_legacy_key_from_password", _fast_legacy_key
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
enc_module, "prompt_existing_password", lambda *_a, **_k: TEST_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
vault, enc_mgr = create_vault(tmp_path)
|
||||||
|
data = {"schema_version": 4, "entries": {}}
|
||||||
|
legacy_key = _fast_legacy_key(TEST_PASSWORD)
|
||||||
|
encrypted = Fernet(legacy_key).encrypt(json.dumps(data).encode())
|
||||||
|
|
||||||
|
caplog.set_level(logging.WARNING)
|
||||||
|
assert enc_mgr.decrypt_and_save_index_from_nostr(encrypted)
|
||||||
|
assert vault.load_index() == data
|
||||||
|
assert any("legacy password-only" in rec.message for rec in caplog.records)
|
Reference in New Issue
Block a user