Automate script checksum handling

This commit is contained in:
thePR0M3TH3AN
2025-07-04 12:38:56 -04:00
parent 648bc9363a
commit b3047484df
8 changed files with 115 additions and 40 deletions

View File

@@ -617,16 +617,17 @@ def handle_settings(password_manager: PasswordManager) -> None:
print("2. Nostr")
print("3. Change password")
print("4. Verify Script Checksum")
print("5. Backup Parent Seed")
print("6. Export database")
print("7. Import database")
print("8. Export 2FA codes")
print("9. Set additional backup location")
print("10. Set inactivity timeout")
print("11. Lock Vault")
print("12. Stats")
print("13. Toggle Secret Mode")
print("14. Back")
print("5. Generate Script Checksum")
print("6. Backup Parent Seed")
print("7. Export database")
print("8. Import database")
print("9. Export 2FA codes")
print("10. Set additional backup location")
print("11. Set inactivity timeout")
print("12. Lock Vault")
print("13. Stats")
print("14. Toggle Secret Mode")
print("15. Back")
choice = input("Select an option: ").strip()
if choice == "1":
handle_profiles_menu(password_manager)
@@ -637,28 +638,30 @@ def handle_settings(password_manager: PasswordManager) -> None:
elif choice == "4":
password_manager.handle_verify_checksum()
elif choice == "5":
password_manager.handle_backup_reveal_parent_seed()
password_manager.handle_update_script_checksum()
elif choice == "6":
password_manager.handle_export_database()
password_manager.handle_backup_reveal_parent_seed()
elif choice == "7":
password_manager.handle_export_database()
elif choice == "8":
path = input("Enter path to backup file: ").strip()
if path:
password_manager.handle_import_database(Path(path))
elif choice == "8":
password_manager.handle_export_totp_codes()
elif choice == "9":
handle_set_additional_backup_location(password_manager)
password_manager.handle_export_totp_codes()
elif choice == "10":
handle_set_inactivity_timeout(password_manager)
handle_set_additional_backup_location(password_manager)
elif choice == "11":
handle_set_inactivity_timeout(password_manager)
elif choice == "12":
password_manager.lock_vault()
print(colored("Vault locked. Please re-enter your password.", "yellow"))
password_manager.unlock_vault()
elif choice == "12":
handle_display_stats(password_manager)
elif choice == "13":
handle_toggle_secret_mode(password_manager)
handle_display_stats(password_manager)
elif choice == "14":
handle_toggle_secret_mode(password_manager)
elif choice == "15":
break
else:
print(colored("Invalid choice.", "red"))

View File

@@ -34,7 +34,13 @@ from utils.key_derivation import (
derive_index_key,
EncryptionMode,
)
from utils.checksum import calculate_checksum, verify_checksum, json_checksum
from utils.checksum import (
calculate_checksum,
verify_checksum,
json_checksum,
initialize_checksum,
update_checksum_file,
)
from utils.password_prompt import (
prompt_for_password,
prompt_existing_password,
@@ -89,6 +95,7 @@ class PasswordManager:
def __init__(self) -> None:
"""Initialize the PasswordManager."""
initialize_app()
self.ensure_script_checksum()
self.encryption_mode: EncryptionMode = EncryptionMode.SEED_ONLY
self.encryption_manager: Optional[EncryptionManager] = None
self.entry_manager: Optional[EntryManager] = None
@@ -119,6 +126,23 @@ class PasswordManager:
# Set the current fingerprint directory
self.fingerprint_dir = self.fingerprint_manager.get_current_fingerprint_dir()
def ensure_script_checksum(self) -> None:
"""Initialize or verify the checksum of the manager script."""
script_path = Path(__file__).resolve()
if not SCRIPT_CHECKSUM_FILE.exists():
initialize_checksum(str(script_path), SCRIPT_CHECKSUM_FILE)
return
checksum = calculate_checksum(str(script_path))
if checksum and not verify_checksum(checksum, SCRIPT_CHECKSUM_FILE):
logging.warning("Script checksum mismatch detected on startup")
print(
colored(
"Warning: script checksum mismatch. "
"Run 'Generate Script Checksum' in Settings if you've updated the app.",
"red",
)
)
@property
def parent_seed(self) -> Optional[str]:
"""Return the decrypted parent seed if set."""
@@ -1473,7 +1497,7 @@ class PasswordManager:
except FileNotFoundError:
print(
colored(
"Checksum file missing. Run scripts/update_checksum.py to generate it.",
"Checksum file missing. Run scripts/update_checksum.py or choose 'Generate Script Checksum' in Settings.",
"yellow",
)
)
@@ -1495,6 +1519,26 @@ class PasswordManager:
logging.error(f"Error during checksum verification: {e}", exc_info=True)
print(colored(f"Error: Failed to verify checksum: {e}", "red"))
def handle_update_script_checksum(self) -> None:
"""Generate a new checksum for the manager script."""
if not confirm_action("Generate new script checksum? (Y/N): "):
print(colored("Operation cancelled.", "yellow"))
return
try:
script_path = Path(__file__).resolve()
if update_checksum_file(str(script_path), str(SCRIPT_CHECKSUM_FILE)):
print(
colored(
f"Checksum updated at '{SCRIPT_CHECKSUM_FILE}'.",
"green",
)
)
else:
print(colored("Failed to update checksum.", "red"))
except Exception as e:
logging.error(f"Error updating checksum: {e}", exc_info=True)
print(colored(f"Error: Failed to update checksum: {e}", "red"))
def get_encrypted_data(self) -> Optional[bytes]:
"""
Retrieves the encrypted password index data.

View File

@@ -57,7 +57,7 @@ def test_handle_verify_checksum_missing(monkeypatch, tmp_path, capsys):
monkeypatch.setattr("password_manager.manager.verify_checksum", raise_missing)
pm.handle_verify_checksum()
out = capsys.readouterr().out.lower()
assert "update_checksum.py" in out
assert "generate script checksum" in out
def test_backup_and_restore_database(monkeypatch, capsys):

View File

@@ -93,7 +93,7 @@ def test_settings_menu_additional_backup(monkeypatch):
tmp_path = Path(tmpdir)
pm, cfg_mgr, fp_mgr = setup_pm(tmp_path, monkeypatch)
inputs = iter(["9", "14"])
inputs = iter(["10", "15"])
with patch("main.handle_set_additional_backup_location") as handler:
with patch("builtins.input", side_effect=lambda *_: next(inputs)):
main.handle_settings(pm)

View File

@@ -21,6 +21,8 @@ try:
verify_checksum,
json_checksum,
canonical_json_dumps,
initialize_checksum,
update_checksum_file,
)
from .password_prompt import prompt_for_password
from .input_utils import timed_input
@@ -45,6 +47,8 @@ __all__ = [
"verify_checksum",
"json_checksum",
"canonical_json_dumps",
"initialize_checksum",
"update_checksum_file",
"exclusive_lock",
"shared_lock",
"prompt_for_password",

View File

@@ -198,3 +198,29 @@ def initialize_checksum(file_path: str, checksum_file_path: str) -> bool:
)
)
return False
def update_checksum_file(file_path: str, checksum_file_path: str) -> bool:
"""Update ``checksum_file_path`` with the SHA-256 checksum of ``file_path``."""
checksum = calculate_checksum(file_path)
if checksum is None:
return False
try:
with open(checksum_file_path, "w") as f:
f.write(checksum)
logging.debug(
f"Updated checksum for '{file_path}' to '{checksum}' at '{checksum_file_path}'."
)
return True
except Exception as exc:
logging.error(
f"Failed to update checksum file '{checksum_file_path}': {exc}",
exc_info=True,
)
print(
colored(
f"Error: Failed to update checksum file '{checksum_file_path}': {exc}",
"red",
)
)
return False