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.
- **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.
- **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.
- **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.
@@ -279,16 +279,17 @@ Back in the Settings menu you can:
* Select `3` to change your master password.
* Choose `4` to verify the script checksum.
* Choose `5` to back up the parent seed.
* Select `6` to export the database to an encrypted file.
* Choose `7` to import a database from a backup file.
* Select `8` to export all 2FA codes.
* Choose `9` to set an additional backup location.
* Select `10` to change the inactivity timeout.
* Choose `11` to toggle Secret Mode and set the clipboard clear delay.
* Select `12` to lock the vault and require re-entry of your password.
* Choose `13` to return to the main menu.
* Select `14` to view seed profile stats.
* Select `5` to generate a new script checksum.
* Choose `6` to back up the parent seed.
* Select `7` to export the database to an encrypted file.
* Choose `8` to import a database from a backup file.
* Select `9` to export all 2FA codes.
* Choose `10` to set an additional backup location.
* Select `11` to change the inactivity timeout.
* Choose `12` to lock the vault and require re-entry of your password.
* Select `13` 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

View File

@@ -7,7 +7,7 @@ SRC_DIR = PROJECT_ROOT / "src"
if str(SRC_DIR) not in sys.path:
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
@@ -15,11 +15,8 @@ def main() -> None:
"""Calculate checksum for the main script and write it to SCRIPT_CHECKSUM_FILE."""
initialize_app()
script_path = SRC_DIR / "password_manager" / "manager.py"
checksum = calculate_checksum(str(script_path))
if checksum is None:
raise SystemExit(f"Failed to calculate checksum for {script_path}")
SCRIPT_CHECKSUM_FILE.write_text(checksum)
if not update_checksum_file(str(script_path), str(SCRIPT_CHECKSUM_FILE)):
raise SystemExit(f"Failed to update checksum for {script_path}")
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("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