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

@@ -43,7 +43,7 @@ SeedPass now uses the `portalocker` library for cross-platform file locking. No
- **Encrypted Storage:** All seeds, login passwords, and sensitive index data are encrypted locally. - **Encrypted Storage:** All seeds, login passwords, and sensitive index data are encrypted locally.
- **Nostr Integration:** Post and retrieve your encrypted password index to/from the Nostr network. - **Nostr Integration:** Post and retrieve your encrypted password index to/from the Nostr network.
- **Chunked Snapshots:** Encrypted vaults are compressed and split into 50KB chunks published as `kind 30071` events with a `kind 30070` manifest and `kind 30072` deltas. - **Chunked Snapshots:** Encrypted vaults are compressed and split into 50KB chunks published as `kind 30071` events with a `kind 30070` manifest and `kind 30072` deltas.
- **Checksum Verification:** Ensure the integrity of the script with checksum verification. - **Automatic Checksum Generation:** The script generates and verifies a SHA-256 checksum to detect tampering.
- **Multiple Seed Profiles:** Manage separate seed profiles and switch between them seamlessly. - **Multiple Seed Profiles:** Manage separate seed profiles and switch between them seamlessly.
- **Interactive TUI:** Navigate through menus to add, retrieve, and modify entries as well as configure Nostr settings. - **Interactive TUI:** Navigate through menus to add, retrieve, and modify entries as well as configure Nostr settings.
- **SeedPass 2FA:** Generate TOTP codes with a real-time countdown progress bar. - **SeedPass 2FA:** Generate TOTP codes with a real-time countdown progress bar.
@@ -279,16 +279,17 @@ Back in the Settings menu you can:
* Select `3` to change your master password. * Select `3` to change your master password.
* Choose `4` to verify the script checksum. * Choose `4` to verify the script checksum.
* Choose `5` to back up the parent seed. * Select `5` to generate a new script checksum.
* Select `6` to export the database to an encrypted file. * Choose `6` to back up the parent seed.
* Choose `7` to import a database from a backup file. * Select `7` to export the database to an encrypted file.
* Select `8` to export all 2FA codes. * Choose `8` to import a database from a backup file.
* Choose `9` to set an additional backup location. * Select `9` to export all 2FA codes.
* Select `10` to change the inactivity timeout. * Choose `10` to set an additional backup location.
* Choose `11` to toggle Secret Mode and set the clipboard clear delay. * Select `11` to change the inactivity timeout.
* Select `12` to lock the vault and require re-entry of your password. * Choose `12` to lock the vault and require re-entry of your password.
* Choose `13` to return to the main menu. * Select `13` to view seed profile stats.
* Select `14` to view seed profile stats. * Choose `14` to toggle Secret Mode and set the clipboard clear delay.
* Select `15` to return to the main menu.
## Running Tests ## Running Tests

View File

@@ -7,7 +7,7 @@ SRC_DIR = PROJECT_ROOT / "src"
if str(SRC_DIR) not in sys.path: if str(SRC_DIR) not in sys.path:
sys.path.insert(0, str(SRC_DIR)) sys.path.insert(0, str(SRC_DIR))
from utils.checksum import calculate_checksum from utils.checksum import update_checksum_file
from constants import SCRIPT_CHECKSUM_FILE, initialize_app from constants import SCRIPT_CHECKSUM_FILE, initialize_app
@@ -15,11 +15,8 @@ def main() -> None:
"""Calculate checksum for the main script and write it to SCRIPT_CHECKSUM_FILE.""" """Calculate checksum for the main script and write it to SCRIPT_CHECKSUM_FILE."""
initialize_app() initialize_app()
script_path = SRC_DIR / "password_manager" / "manager.py" script_path = SRC_DIR / "password_manager" / "manager.py"
checksum = calculate_checksum(str(script_path)) if not update_checksum_file(str(script_path), str(SCRIPT_CHECKSUM_FILE)):
if checksum is None: raise SystemExit(f"Failed to update checksum for {script_path}")
raise SystemExit(f"Failed to calculate checksum for {script_path}")
SCRIPT_CHECKSUM_FILE.write_text(checksum)
print(f"Updated checksum written to {SCRIPT_CHECKSUM_FILE}") print(f"Updated checksum written to {SCRIPT_CHECKSUM_FILE}")

View File

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

View File

@@ -34,7 +34,13 @@ from utils.key_derivation import (
derive_index_key, derive_index_key,
EncryptionMode, 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 ( from utils.password_prompt import (
prompt_for_password, prompt_for_password,
prompt_existing_password, prompt_existing_password,
@@ -89,6 +95,7 @@ class PasswordManager:
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the PasswordManager.""" """Initialize the PasswordManager."""
initialize_app() initialize_app()
self.ensure_script_checksum()
self.encryption_mode: EncryptionMode = EncryptionMode.SEED_ONLY self.encryption_mode: EncryptionMode = EncryptionMode.SEED_ONLY
self.encryption_manager: Optional[EncryptionManager] = None self.encryption_manager: Optional[EncryptionManager] = None
self.entry_manager: Optional[EntryManager] = None self.entry_manager: Optional[EntryManager] = None
@@ -119,6 +126,23 @@ class PasswordManager:
# Set the current fingerprint directory # Set the current fingerprint directory
self.fingerprint_dir = self.fingerprint_manager.get_current_fingerprint_dir() 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 @property
def parent_seed(self) -> Optional[str]: def parent_seed(self) -> Optional[str]:
"""Return the decrypted parent seed if set.""" """Return the decrypted parent seed if set."""
@@ -1473,7 +1497,7 @@ class PasswordManager:
except FileNotFoundError: except FileNotFoundError:
print( print(
colored( 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", "yellow",
) )
) )
@@ -1495,6 +1519,26 @@ class PasswordManager:
logging.error(f"Error during checksum verification: {e}", exc_info=True) logging.error(f"Error during checksum verification: {e}", exc_info=True)
print(colored(f"Error: Failed to verify checksum: {e}", "red")) 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]: def get_encrypted_data(self) -> Optional[bytes]:
""" """
Retrieves the encrypted password index data. 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) monkeypatch.setattr("password_manager.manager.verify_checksum", raise_missing)
pm.handle_verify_checksum() pm.handle_verify_checksum()
out = capsys.readouterr().out.lower() 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): 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) tmp_path = Path(tmpdir)
pm, cfg_mgr, fp_mgr = setup_pm(tmp_path, monkeypatch) 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("main.handle_set_additional_backup_location") as handler:
with patch("builtins.input", side_effect=lambda *_: next(inputs)): with patch("builtins.input", side_effect=lambda *_: next(inputs)):
main.handle_settings(pm) main.handle_settings(pm)

View File

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

View File

@@ -198,3 +198,29 @@ def initialize_checksum(file_path: str, checksum_file_path: str) -> bool:
) )
) )
return False 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